Skip to content

Commit b9c4ade

Browse files
lxsmnsycbrenelzatilafassina
authored
feat: add env:* (#2110)
Co-authored-by: Brenley Dueck <brenleydueck@gmail.com> Co-authored-by: Atila Fassina <atila@fassina.eu>
1 parent 2885905 commit b9c4ade

File tree

7 files changed

+192
-2
lines changed

7 files changed

+192
-2
lines changed

.changeset/smooth-foxes-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/start": minor
3+
---
4+
5+
feat: add `env:*` runtime environment variables support

apps/tests/src/e2e/server-function.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,10 @@ test.describe("server-function", () => {
8181
await page.goto("http://localhost:3000/server-function-blob");
8282
await expect(page.locator("#server-fn-test")).toContainText('{"result":true}');
8383
});
84+
85+
// TODO not sure if this is the correct place
86+
test("should build with a env:server", async ({ page }) => {
87+
await page.goto("http://localhost:3000/server-env");
88+
await expect(page.locator("#server-fn-test")).toContainText('{"result":true}');
89+
});
8490
});

apps/tests/src/env.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare module "env:server" {
2+
export const SERVER_EXAMPLE: string;
3+
}
4+
declare module "env:server/runtime" {
5+
const env: {
6+
NODE_ENV: string;
7+
};
8+
9+
export default env;
10+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { SERVER_EXAMPLE } from "env:server";
2+
import env from "env:server/runtime";
3+
import { createEffect, createSignal } from "solid-js";
4+
5+
async function getServerCompiledEnv() {
6+
"use server";
7+
8+
return await Promise.resolve(SERVER_EXAMPLE);
9+
}
10+
11+
async function getServerRuntimeEnv() {
12+
"use server";
13+
14+
return await Promise.resolve(env.NODE_ENV);
15+
}
16+
17+
async function checkServerEnvOnClient() {
18+
try {
19+
await import("env:server");
20+
return false;
21+
} catch {
22+
return true;
23+
}
24+
}
25+
26+
export default function App() {
27+
const [output, setOutput] = createSignal<{ result?: boolean }>({});
28+
29+
createEffect(async () => {
30+
const resultA = await getServerCompiledEnv();
31+
const resultB = await getServerRuntimeEnv();
32+
const checkImport = await checkServerEnvOnClient();
33+
setOutput(prev => ({ ...prev, result: !!resultA && !!resultB && checkImport }));
34+
});
35+
36+
return (
37+
<main>
38+
<span id="server-fn-test">{JSON.stringify(output())}</span>
39+
</main>
40+
);
41+
}

apps/tests/vite.config.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
import { defineConfig } from "vite";
2-
import { solidStart } from "../../packages/start/src/config";
32
import { nitroV2Plugin } from "../../packages/start-nitro-v2-vite-plugin/src";
3+
import { solidStart } from "../../packages/start/src/config";
44

55
export default defineConfig({
66
server: {
77
port: 3000,
88
},
9-
plugins: [solidStart(), nitroV2Plugin()],
9+
plugins: [
10+
solidStart({
11+
env: {
12+
server: {
13+
load() {
14+
return {
15+
SERVER_EXAMPLE: "This is a server example.",
16+
};
17+
},
18+
},
19+
client: {
20+
load() {
21+
return {
22+
CLIENT_EXAMPLE: "This is a client example.",
23+
};
24+
},
25+
},
26+
},
27+
}),
28+
nitroV2Plugin(),
29+
],
1030
});

packages/start/src/config/env.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { loadEnv, type Plugin } from "vite";
2+
3+
const LOADERS = {
4+
node: `export default key => process.env[key];`,
5+
"cloudflare-workers": `import { env } from 'cloudflare:workers';export default key => env[key];`,
6+
"netlify-edge": `export default key => Netlify.env.get(key);`,
7+
};
8+
9+
export interface EnvPluginOptions {
10+
server?: {
11+
runtime?: keyof typeof LOADERS | (string & {});
12+
load?: (mode: string) => Record<string, string>;
13+
prefix?: string;
14+
};
15+
client?: {
16+
load?: (mode: string) => Record<string, string>;
17+
prefix?: string;
18+
};
19+
}
20+
21+
const SERVER_ENV = "env:server";
22+
const CLIENT_ENV = "env:client";
23+
24+
const SERVER_RUNTIME_ENV = `${SERVER_ENV}/runtime`;
25+
26+
const SERVER_RUNTIME_LOADER = `${SERVER_RUNTIME_ENV}/loader`;
27+
28+
const DEFAULT_SERVER_PREFIX = "SERVER_";
29+
const DEFAULT_CLIENT_PREFIX = "CLIENT_";
30+
31+
const SERVER_ONLY_MODULE = `throw new Error('Attempt to load server-only environment variables in client runtime.');`;
32+
33+
const SERVER_RUNTIME_CODE = `import load from '${SERVER_RUNTIME_LOADER}';
34+
35+
export default new Proxy({}, {
36+
get(_, key) {
37+
return load(key);
38+
},
39+
})`;
40+
41+
function convertObjectToModule(object: Record<string, string>): string {
42+
let result = "";
43+
for (const key in object) {
44+
result += `export const ${key} = ${JSON.stringify(object[key])};`;
45+
}
46+
return result;
47+
}
48+
49+
export function envPlugin(options?: EnvPluginOptions): Plugin {
50+
const currentOptions = options ?? {};
51+
let env: string;
52+
const serverPrefix = currentOptions.server?.prefix ?? DEFAULT_SERVER_PREFIX;
53+
const clientPrefix = currentOptions.client?.prefix ?? DEFAULT_CLIENT_PREFIX;
54+
const runtime = options?.server?.runtime ?? "node";
55+
const runtimeCode = runtime in LOADERS ? LOADERS[runtime as keyof typeof LOADERS] : runtime;
56+
57+
return {
58+
name: "solid-start:env",
59+
enforce: "pre",
60+
configResolved(config) {
61+
env = config.mode !== "production" ? "development" : "production";
62+
},
63+
resolveId(id) {
64+
if (
65+
id === SERVER_ENV ||
66+
id === CLIENT_ENV ||
67+
id === SERVER_RUNTIME_ENV ||
68+
id === SERVER_RUNTIME_LOADER
69+
) {
70+
return id;
71+
}
72+
return undefined;
73+
},
74+
load(id, opts) {
75+
if (id === SERVER_ENV) {
76+
if (!opts?.ssr) {
77+
return SERVER_ONLY_MODULE;
78+
}
79+
const vars = currentOptions.server?.load
80+
? currentOptions.server.load(env)
81+
: loadEnv(env, '.', serverPrefix);
82+
return convertObjectToModule(vars);
83+
}
84+
if (id === CLIENT_ENV) {
85+
const vars = currentOptions.client?.load
86+
? currentOptions.client.load(env)
87+
: loadEnv(env, '.', clientPrefix);
88+
return convertObjectToModule(vars);
89+
}
90+
if (id === SERVER_RUNTIME_LOADER) {
91+
if (!opts?.ssr) {
92+
return SERVER_ONLY_MODULE;
93+
}
94+
return runtimeCode;
95+
}
96+
if (id === SERVER_RUNTIME_ENV) {
97+
if (!opts?.ssr) {
98+
return SERVER_ONLY_MODULE;
99+
}
100+
return SERVER_RUNTIME_CODE;
101+
}
102+
return undefined;
103+
},
104+
};
105+
}

packages/start/src/config/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import solid, { type Options as SolidOptions } from "vite-plugin-solid";
88

99
import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.ts";
1010
import { devServer } from "./dev-server.ts";
11+
import { type EnvPluginOptions, envPlugin } from "./env.ts";
1112
import { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.ts";
1213
import { fsRoutes } from "./fs-routes/index.ts";
1314
import type { BaseFileSystemRouter } from "./fs-routes/router.ts";
@@ -32,6 +33,7 @@ export interface SolidStartOptions {
3233
*/
3334
mode?: "js" | "json";
3435
};
36+
env?: EnvPluginOptions;
3537
}
3638

3739
const absolute = (path: string, root: string) =>
@@ -175,6 +177,7 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
175177
},
176178
}),
177179
lazy(),
180+
envPlugin(options?.env),
178181
// Must be placed after fsRoutes, as treeShake will remove the
179182
// server fn exports added in by this plugin
180183
TanStackServerFnPlugin({

0 commit comments

Comments
 (0)