diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index c5bb48a02..556c7ab94 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -81,4 +81,10 @@ test.describe("server-function", () => { await page.goto("http://localhost:3000/server-function-blob"); await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); }); + + // TODO not sure if this is the correct place + test("should build with a env:server", async ({ page }) => { + await page.goto("http://localhost:3000/server-env"); + await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); + }); }); diff --git a/apps/tests/src/env.d.ts b/apps/tests/src/env.d.ts new file mode 100644 index 000000000..42e11435f --- /dev/null +++ b/apps/tests/src/env.d.ts @@ -0,0 +1,10 @@ +declare module "env:server" { + export const SERVER_EXAMPLE: string; +} +declare module "env:server/runtime" { + const env: { + NODE_ENV: string; + }; + + export default env; +} diff --git a/apps/tests/src/routes/server-env.tsx b/apps/tests/src/routes/server-env.tsx new file mode 100644 index 000000000..db8f63325 --- /dev/null +++ b/apps/tests/src/routes/server-env.tsx @@ -0,0 +1,41 @@ +import { SERVER_EXAMPLE } from "env:server"; +import env from "env:server/runtime"; +import { createEffect, createSignal } from "solid-js"; + +async function getServerCompiledEnv() { + "use server"; + + return await Promise.resolve(SERVER_EXAMPLE); +} + +async function getServerRuntimeEnv() { + "use server"; + + return await Promise.resolve(env.NODE_ENV); +} + +async function checkServerEnvOnClient() { + try { + await import("env:server"); + return false; + } catch { + return true; + } +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const resultA = await getServerCompiledEnv(); + const resultB = await getServerRuntimeEnv(); + const checkImport = await checkServerEnvOnClient(); + setOutput(prev => ({ ...prev, result: !!resultA && !!resultB && checkImport })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} diff --git a/apps/tests/vite.config.ts b/apps/tests/vite.config.ts index 4149be667..92f27738b 100644 --- a/apps/tests/vite.config.ts +++ b/apps/tests/vite.config.ts @@ -1,10 +1,30 @@ import { defineConfig } from "vite"; -import { solidStart } from "../../packages/start/src/config"; import { nitroV2Plugin } from "../../packages/start-nitro-v2-vite-plugin/src"; +import { solidStart } from "../../packages/start/src/config"; export default defineConfig({ server: { port: 3000, }, - plugins: [solidStart(), nitroV2Plugin()], + plugins: [ + solidStart({ + env: { + server: { + load() { + return { + SERVER_EXAMPLE: "This is a server example.", + }; + }, + }, + client: { + load() { + return { + CLIENT_EXAMPLE: "This is a client example.", + }; + }, + }, + }, + }), + nitroV2Plugin(), + ], }); diff --git a/packages/start/src/config/env.ts b/packages/start/src/config/env.ts new file mode 100644 index 000000000..fb98c200e --- /dev/null +++ b/packages/start/src/config/env.ts @@ -0,0 +1,105 @@ +import { loadEnv, type Plugin } from "vite"; + +const LOADERS = { + node: `export default key => process.env[key];`, + "cloudflare-workers": `import { env } from 'cloudflare:workers';export default key => env[key];`, + "netlify-edge": `export default key => Netlify.env.get(key);`, +}; + +export interface EnvPluginOptions { + server?: { + runtime?: keyof typeof LOADERS | (string & {}); + load?: (mode: string) => Record; + prefix?: string; + }; + client?: { + load?: (mode: string) => Record; + prefix?: string; + }; +} + +const SERVER_ENV = "env:server"; +const CLIENT_ENV = "env:client"; + +const SERVER_RUNTIME_ENV = `${SERVER_ENV}/runtime`; + +const SERVER_RUNTIME_LOADER = `${SERVER_RUNTIME_ENV}/loader`; + +const DEFAULT_SERVER_PREFIX = "SERVER_"; +const DEFAULT_CLIENT_PREFIX = "CLIENT_"; + +const SERVER_ONLY_MODULE = `throw new Error('Attempt to load server-only environment variables in client runtime.');`; + +const SERVER_RUNTIME_CODE = `import load from '${SERVER_RUNTIME_LOADER}'; + +export default new Proxy({}, { + get(_, key) { + return load(key); + }, +})`; + +function convertObjectToModule(object: Record): string { + let result = ""; + for (const key in object) { + result += `export const ${key} = ${JSON.stringify(object[key])};`; + } + return result; +} + +export function envPlugin(options?: EnvPluginOptions): Plugin { + const currentOptions = options ?? {}; + let env: string; + const serverPrefix = currentOptions.server?.prefix ?? DEFAULT_SERVER_PREFIX; + const clientPrefix = currentOptions.client?.prefix ?? DEFAULT_CLIENT_PREFIX; + const runtime = options?.server?.runtime ?? "node"; + const runtimeCode = runtime in LOADERS ? LOADERS[runtime as keyof typeof LOADERS] : runtime; + + return { + name: "solid-start:env", + enforce: "pre", + configResolved(config) { + env = config.mode !== "production" ? "development" : "production"; + }, + resolveId(id) { + if ( + id === SERVER_ENV || + id === CLIENT_ENV || + id === SERVER_RUNTIME_ENV || + id === SERVER_RUNTIME_LOADER + ) { + return id; + } + return undefined; + }, + load(id, opts) { + if (id === SERVER_ENV) { + if (!opts?.ssr) { + return SERVER_ONLY_MODULE; + } + const vars = currentOptions.server?.load + ? currentOptions.server.load(env) + : loadEnv(env, '.', serverPrefix); + return convertObjectToModule(vars); + } + if (id === CLIENT_ENV) { + const vars = currentOptions.client?.load + ? currentOptions.client.load(env) + : loadEnv(env, '.', clientPrefix); + return convertObjectToModule(vars); + } + if (id === SERVER_RUNTIME_LOADER) { + if (!opts?.ssr) { + return SERVER_ONLY_MODULE; + } + return runtimeCode; + } + if (id === SERVER_RUNTIME_ENV) { + if (!opts?.ssr) { + return SERVER_ONLY_MODULE; + } + return SERVER_RUNTIME_CODE; + } + return undefined; + }, + }; +} diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index b8cf5bf58..653018db4 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -8,6 +8,7 @@ import solid, { type Options as SolidOptions } from "vite-plugin-solid"; import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.ts"; import { devServer } from "./dev-server.ts"; +import { type EnvPluginOptions, envPlugin } from "./env.ts"; import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.ts"; import { fsRoutes } from "./fs-routes/index.ts"; import type { BaseFileSystemRouter } from "./fs-routes/router.ts"; @@ -32,6 +33,7 @@ export interface SolidStartOptions { */ mode?: "js" | "json"; }; + env?: EnvPluginOptions; } const absolute = (path: string, root: string) => @@ -175,6 +177,7 @@ export function solidStart(options?: SolidStartOptions): Array { }, }), lazy(), + envPlugin(options?.env), // Must be placed after fsRoutes, as treeShake will remove the // server fn exports added in by this plugin TanStackServerFnPlugin({