Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/swift-badgers-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: ensure `building` resolves correctly to allow avoiding build-time explicit environment variable validation
16 changes: 15 additions & 1 deletion documentation/docs/20-core-concepts/70-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,21 @@ export const variables = defineEnvVars({
});
```

If a value is invalid, the app will fail to start (or build).
If a value is invalid, the app will fail to start (or build). To opt out of one or the other, use [`building`]($app-env#building) from `$app/env` along with a validator that accepts an optional value:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';
+++import { building } from '$app/env'+++
import * as v from 'valibot';

export const variables = defineEnvVars({
SECRET: {
// optional when building but required when starting the app
+++schema: building ? v.optional(v.string()) : v.string()+++
}
});
```

You can use validators to make values optional, or transform them (such as turning a string into a boolean, or parsing JSON) — see your validation library's documentation to learn how.

Expand Down
23 changes: 13 additions & 10 deletions packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export async function load_explicit_env(kit, file, mode) {
/** @type {Record<string, EnvVarConfig<any>>} */
let variables;

/** @type {import('../runtime/app/env/internal.js')} */ (
await server.ssrLoadModule(`${runtime_directory}/app/env/internal.js`)
).set_building();

try {
({ variables } = await server.ssrLoadModule(file));

Expand Down Expand Up @@ -157,17 +161,14 @@ export function create_dynamic_module(type, dev_values, disabled) {

/**
* Creates the `__sveltekit/env` module
* @param {Record<string, EnvVarConfig<any>> | null} variables
* @param {Record<string, EnvVarConfig<any> | undefined> | null} variables
* @param {Record<string, string>} env
* @param {string | null} entry
*/
export function create_sveltekit_env(variables, env, entry) {
const imports = entry
? [
`import { variables } from ${JSON.stringify(entry)};`,
`import { validate, handle_issues } from '@sveltejs/kit/internal/env';`
]
: [`const variables = {};`, `const handle_issues = () => {};`];
? [`import { validate, handle_issues } from '@sveltejs/kit/internal/env';`]
: [`const handle_issues = () => {};`];

const declarations = [];
const setters = [];
Expand All @@ -176,7 +177,7 @@ export function create_sveltekit_env(variables, env, entry) {
const issues = {};

for (const [name, config] of Object.entries(variables ?? {})) {
if (config.static) {
if (config?.static) {
if (config.public) {
const value = validate(variables ?? {}, env[name], name, issues);
declarations.push(`explicit_public_env.${name} = ${devalue.uneval(value)};`);
Expand All @@ -186,7 +187,7 @@ export function create_sveltekit_env(variables, env, entry) {
`const ${name} = validate(variables, env.${name}, ${JSON.stringify(name)}, issues);`
);

if (config.public) {
if (config?.public) {
setters.push(`explicit_public_env.${name} = ${name};`);
setters.push(`rendered_env.${name} = ${name};`);
} else {
Expand All @@ -201,14 +202,16 @@ export function create_sveltekit_env(variables, env, entry) {
GENERATED_COMMENT,
imports.join('\n'),
`const issues = {};`,
'export { variables }',
'export const dynamic_private_env = {};',
'export const explicit_public_env = {};',
'export const rendered_env = {};',
...declarations,
`handle_issues(issues);`,
// avoid `variables` from being imported before `set_building` is called
// because that causes expressions with `building` to resolve incorrectly
dedent`
export function set_env(env) {
export async function set_env(env) {
${entry ? `const { variables } = (await import(${JSON.stringify(entry)}));` : ''}
const issues = {};
${setters.join('\n')}
handle_issues(issues);
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function analyse({
const public_env = filter_env(env, public_prefix, private_prefix);
internal.set_private_env(private_env);
internal.set_public_env(public_env);
internal.set_env(env);
await internal.set_env(env);
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));

Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
const public_env = filter_env(env, public_prefix, private_prefix);
internal.set_private_env(private_env);
internal.set_public_env(public_env);
internal.set_env(env);
await internal.set_env(env);
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`));

Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/exports/internal/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const ASYNC_VALIDATOR = {
};

/**
* @param {Record<string, EnvVarConfig<any>>} variables
* @param {Record<string, EnvVarConfig<any> | undefined>} variables
* @param {string | undefined} value
* @param {string} name
* @param {Record<string, StandardSchemaV1.Issue[]>} issues
Expand Down
17 changes: 13 additions & 4 deletions packages/kit/src/exports/vite/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,19 @@ export async function preview(vite, vite_config, svelte_config) {
set_assets(assets);

const server = new Server(manifest);
await server.init({
env: loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''),
read: (file) => createReadableStream(`${dir}/${file}`)
});

try {
await server.init({
env: loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''),
read: (file) => createReadableStream(`${dir}/${file}`)
});
} catch (error) {
// Vite erases the error message when starting the preview server so we store
// it in the stack instead. This ensures errors thrown using `stackless`
// are still readable
if (error instanceof Error) error.stack = error.message;
throw error;
}

const emulator = await svelte_config.kit.adapter?.emulate?.();

Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class Server {

set_private_env(filter_env(env, env_private_prefix, env_public_prefix));
set_public_env(filter_env(env, env_public_prefix, env_private_prefix));
set_env(env);
await set_env(env);

if (read) {
// Wrap the read function to handle MaybePromise<ReadableStream>
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/types/ambient-private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ declare module '__sveltekit/env' {
// exported environment variables are defined in env.d.ts

/** Populate exported environment variables */
export function set_env(environment: Record<string, string>): void;
export function set_env(environment: Record<string, string>): Promise<void>;

/** public env vars */
export const explicit_public_env: Record<string, any>;
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface ServerModule {
export interface ServerInternalModule {
set_assets(path: string): void;
set_building(): void;
set_env(environment: Record<string, string>): void;
set_env(environment: Record<string, string>): Promise<void>;
set_manifest(manifest: SSRManifest): void;
set_prerendering(): void;
set_private_env(environment: Record<string, string>): void;
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/test/apps/options-2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"dev": "RUNTIME_ONLY=secret vite dev",
"build": "vite build",
"preview": "vite preview",
"preview": "RUNTIME_ONLY=secret vite preview",
"prepare": "svelte-kit sync",
"check": "svelte-kit sync && tsc && svelte-check",
"test": "pnpm test:dev && pnpm test:build",
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/test/apps/options-2/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import process from 'node:process';
import { building } from '$app/env';
import * as v from 'valibot';

export const variables = {
Expand All @@ -13,5 +14,8 @@ export const variables = {
},
PRIVATE_VALIDATED_DEFAULT_ENV: {
schema: v.optional(v.picklist(['foo', 'bar']), 'foo')
},
RUNTIME_ONLY: {
schema: building ? v.optional(v.string()) : v.string()
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RUNTIME_ONLY } from '$app/env/private';

export function load() {
return {
runtime_environment_variable: !!RUNTIME_ONLY
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
export let data;
</script>

<p>runtime-only environment variable exists: {data.runtime_environment_variable}</p>
23 changes: 22 additions & 1 deletion packages/kit/test/apps/options-2/test/env.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import { execFile } from 'node:child_process';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { expect } from '@playwright/test';
Expand Down Expand Up @@ -56,7 +57,8 @@ test.describe('$app/env', () => {
JSON.stringify({
PRIVATE_EXPLICIT_ENV: 'secret resolved at runtime',
PRIVATE_STATIC_EXPLICIT_ENV: 'secret resolved at build time',
PRIVATE_VALIDATED_DEFAULT_ENV: 'foo'
PRIVATE_VALIDATED_DEFAULT_ENV: 'foo',
RUNTIME_ONLY: 'secret'
})
);

Expand Down Expand Up @@ -84,4 +86,23 @@ test.describe('$app/env', () => {
expect(serviceWorker).not.toContain('importScripts("/basepath/_app/env.script.js");');
expect(fs.existsSync(`${output}/prerendered/dependencies/_app/env.script.js`)).toBeFalsy();
});

test('runtime-only validation', async ({ page, javaScriptEnabled }) => {
test.skip(javaScriptEnabled);

await page.goto('/basepath/env/runtime-only');
await expect(page.locator('p')).toHaveText('runtime-only environment variable exists: true');

// test validation runs at runtime
if (!process.env.DEV) {
const app_dir = fileURLToPath(new URL('..', import.meta.url));
const output = await new Promise((resolve) => {
execFile('pnpm', ['vite', 'preview'], { cwd: app_dir }, (_, stdout, stderr) =>
resolve(stdout + stderr)
);
});
expect(output).toContain('Invalid environment variables');
expect(output).toContain('RUNTIME_ONLY');
}
});
});
Loading