Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/eager-env-init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@sveltejs/kit': patch
'@sveltejs/adapter-node': patch
'@sveltejs/adapter-cloudflare': patch
'@sveltejs/adapter-vercel': patch
'@sveltejs/adapter-netlify': patch
---

fix: populate env vars before `instrumentation.server.js` is evaluated

Previously, `$app/env/private` and `$app/env/public` dynamic variable values were only
populated when `Server.init()` called `set_env()`. If any module reading these values
was evaluated before `Server.init()` ran (e.g. via bundler chunk colocation with
`instrumentation.server.js`), the values would be silently `undefined`.

`builder.instrument()` now accepts an `env` option. When provided, the generated
facade creates a separate init module that imports `set_env` and calls it with the
platform's env before instrumentation is imported. This ensures dynamic env vars are
populated (and validated) before any instrumentation or application code is evaluated.

Adapters that have env available at module-load time pass the appropriate expression:
- adapter-node, adapter-vercel (serverless): `process.env`
- adapter-cloudflare: `env` from `cloudflare:workers`
- adapter-vercel (edge), adapter-netlify (serverless): env init via `generateText`

If required env vars are missing, `set_env` will throw — this is intentional, as the
app cannot function without them.
6 changes: 5 additions & 1 deletion packages/adapter-cloudflare/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ export default function (options = {}) {
if (builder.hasServerInstrumentationFile()) {
builder.instrument({
entrypoint: worker_dest,
instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`
instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`,
env: {
imports: [`import { env } from 'cloudflare:workers';`],
expression: 'env'
}
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/adapter-netlify/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ export const config = {
function generate_traced_module(config) {
return ({ instrumentation, start }) => {
return `\
import '../server/${instrumentation}';
const { set_env } = await import('../server/env.js');
set_env(process.env);
await import('../server/${instrumentation}');
const { default: _0 } = await import('../server/${start}');
export { _0 as default };

Expand Down
1 change: 1 addition & 0 deletions packages/adapter-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export default function (opts = {}) {
builder.instrument({
entrypoint: `${out}/index.js`,
instrumentation: `${out}/instrumentation.server.js`,
env: 'process.env',
module: {
exports: ['path', 'host', 'port', 'server']
}
Expand Down
7 changes: 5 additions & 2 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ const plugin = function (defaults = {}) {
if (builder.hasServerInstrumentationFile()) {
builder.instrument({
entrypoint: `${tmp}/index.js`,
instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`
instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`,
env: 'process.env'
});
}

Expand Down Expand Up @@ -826,7 +827,9 @@ function is_prerendered(route) {
*/
function generate_traced_edge_module({ instrumentation, start }) {
return `\
import './${instrumentation}';
const { set_env } = await import('./env.js');
set_env(process.env);
await import('./${instrumentation}');
const promise = import('./${start}');

/**
Expand Down
47 changes: 34 additions & 13 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export function create_builder({
entrypoint,
instrumentation,
start = join(dirname(entrypoint), 'start.js'),
env,
module = {
exports: ['default']
}
Expand Down Expand Up @@ -268,7 +269,9 @@ export function create_builder({
: create_instrumentation_facade({
instrumentation: relative_instrumentation,
start: relative_start,
exports: module.exports
exports: module.exports,
env,
entrypoint
});

rimraf(entrypoint);
Expand Down Expand Up @@ -301,29 +304,47 @@ async function compress_file(file, format = 'gz') {

/**
* Given a list of exports, generate a facade that:
* - Imports and calls `set_env` with the platform's env (if provided), so that dynamic env
* vars are populated before instrumentation or any application code is evaluated
* - Imports the instrumentation file
* - Imports `exports` from the entrypoint (dynamically, if `tla` is true)
* - Imports `exports` from the entrypoint (dynamically)
* - Re-exports `exports` from the entrypoint
*
* @param {{ instrumentation: string; start: string; exports: string[] }} opts
* @param {{ instrumentation: string; start: string; exports: string[]; env?: string | { imports: string[], expression: string }; entrypoint: string }} opts
* @returns {string}
*/
function create_instrumentation_facade({ instrumentation, start, exports }) {
const import_instrumentation = `import './${instrumentation}';`;
function create_instrumentation_facade({ instrumentation, start, exports, env, entrypoint }) {
const parts = [];

if (env) {
// Generate a separate init module that imports `set_env` and calls it with the
// platform's env expression. Static imports are evaluated in order, so importing
// this module before instrumentation ensures env is populated before any
// instrumentation code (or transitively imported app modules) run.
const env_init_name = '__sveltekit_env_init.js';
const env_init_path = join(dirname(entrypoint), env_init_name);
const imports = typeof env === 'string' ? [] : env.imports;
const expression = typeof env === 'string' ? env : env.expression;
write(
env_init_path,
`${imports.join('\n')}\nimport { set_env } from './env.js';\nset_env(${expression});\n`
);
parts.push(`import './${env_init_name}';`);
}

parts.push(`import './${instrumentation}';`);

const { namespace, declarations, reexports } = create_exported_declarations(
exports,
(name, ns) => `${ns}.${name}`,
'__mod'
);

const parts = [
`const ${namespace} = await import('./${start}');`,
declarations.join('\n'),
reexports.length > 0 ? `export { ${reexports.join(', ')} };` : ''
]
.filter(Boolean)
.join('\n');
parts.push(`const ${namespace} = await import('./${start}');`);
parts.push(declarations.join('\n'));
if (reexports.length > 0) {
parts.push(`export { ${reexports.join(', ')} };`);
}

return `${import_instrumentation}\n${parts}`;
return parts.filter(Boolean).join('\n');
}
16 changes: 1 addition & 15 deletions packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ export async function load_explicit_env(kit, file, root, mode) {
* @param {Record<string, EnvVarConfig<any> | undefined> | null} variables
* @param {Record<string, string>} env
* @param {string | null} entry
* @param {boolean} is_dev
*/
export function create_sveltekit_env(variables, env, entry, is_dev) {
export function create_sveltekit_env(variables, env, entry) {
const imports = entry
? [
`import { variables } from ${JSON.stringify(entry)};`,
Expand Down Expand Up @@ -150,19 +149,6 @@ export function create_sveltekit_env(variables, env, entry, is_dev) {
}`
];

// In dev, initialise the env immediately. Tools like `vite-node` load modules
// through the Vite config but don't run the SvelteKit dev server, which is what
// normally calls `set_env`. Without this, dynamic env vars imported from
// `$app/env/public` and `$app/env/private` would be `undefined` in such contexts.
if (is_dev) {
/** @type {Record<string, string>} */
const dev_env = {};
for (const name of Object.keys(variables ?? {})) {
if (name in env) dev_env[name] = env[name];
}
blocks.push(`set_env(${devalue.uneval(dev_env)});`);
}

const module = blocks.join('\n\n');

return module;
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ export interface Builder {
* `entrypoint` which imports `instrumentation` and then dynamically imports `start`. This allows
* the module hooks necessary for instrumentation libraries to be loaded prior to any application code.
*
* If `env` is provided, a separate init module is generated that imports `set_env` from the env
* module and calls it with the provided expression. This module is imported before `instrumentation`,
* ensuring dynamic env vars are populated (and validated) before any instrumentation or application
* code is evaluated. The expression should evaluate to a `Record<string, string>` — e.g. `'process.env'`
* for Node-like platforms, or an import from `'cloudflare:workers'` for Cloudflare Workers.
*
* Caveats:
* - "Live exports" will not work. If your adapter uses live exports, your users will need to manually import the server instrumentation on startup.
* - If `tla` is `false`, OTEL auto-instrumentation may not work properly. Use it if your environment supports it.
Expand All @@ -232,6 +238,7 @@ export interface Builder {
* @param options.entrypoint the path to the entrypoint to trace.
* @param options.instrumentation the path to the instrumentation file.
* @param options.start the name of the start file. This is what `entrypoint` will be renamed to.
* @param options.env a JS expression that evaluates to the env object, or an object with `imports` (array of import statement strings) and `expression` (a JS expression), used to populate dynamic env vars before instrumentation runs.
* @param options.module configuration for the resulting entrypoint module.
* @param options.module.exports
* @param options.module.generateText a function that receives the relative paths to the instrumentation and start files, and generates the text of the module to be traced. If not provided, the default implementation will be used, which uses top-level await.
Expand All @@ -241,6 +248,7 @@ export interface Builder {
entrypoint: string;
instrumentation: string;
start?: string;
env?: string | { imports: string[]; expression: string };
module?:
| {
exports: string[];
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ function kit({ svelte_config }) {
return create_service_worker_module(svelte_config);

case sveltekit_env:
return create_sveltekit_env(explicit_env_config, env, explicit_env_entry, !is_build);
return create_sveltekit_env(explicit_env_config, env, explicit_env_entry);

case sveltekit_env_public_client:
return create_sveltekit_env_public(
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ declare module '@sveltejs/kit' {
* `entrypoint` which imports `instrumentation` and then dynamically imports `start`. This allows
* the module hooks necessary for instrumentation libraries to be loaded prior to any application code.
*
* If `env` is provided, a separate init module is generated that imports `set_env` from the env
* module and calls it with the provided expression. This module is imported before `instrumentation`,
* ensuring dynamic env vars are populated (and validated) before any instrumentation or application
* code is evaluated. The expression should evaluate to a `Record<string, string>` — e.g. `'process.env'`
* for Node-like platforms, or an import from `'cloudflare:workers'` for Cloudflare Workers.
*
* Caveats:
* - "Live exports" will not work. If your adapter uses live exports, your users will need to manually import the server instrumentation on startup.
* - If `tla` is `false`, OTEL auto-instrumentation may not work properly. Use it if your environment supports it.
Expand All @@ -205,6 +211,7 @@ declare module '@sveltejs/kit' {
* @param options.entrypoint the path to the entrypoint to trace.
* @param options.instrumentation the path to the instrumentation file.
* @param options.start the name of the start file. This is what `entrypoint` will be renamed to.
* @param options.env a JS expression that evaluates to the env object, or an object with `imports` (array of import statement strings) and `expression` (a JS expression), used to populate dynamic env vars before instrumentation runs.
* @param options.module configuration for the resulting entrypoint module.
* @param options.module.generateText a function that receives the relative paths to the instrumentation and start files, and generates the text of the module to be traced. If not provided, the default implementation will be used, which uses top-level await.
* @since 2.31.0
Expand All @@ -213,6 +220,7 @@ declare module '@sveltejs/kit' {
entrypoint: string;
instrumentation: string;
start?: string;
env?: string | { imports: string[]; expression: string };
module?:
| {
exports: string[];
Expand Down
Loading