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/env-availability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': major
---

breaking: replace `EnvVarConfig.static` with `EnvVarConfig.availability`
20 changes: 12 additions & 8 deletions documentation/docs/20-core-concepts/70-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,31 @@ export const variables = defineEnvVars({
});
```

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:
If a value is invalid, the app will fail to start (or build). If the variable is only available at run time (for example a secret that isn't set during the build), use `availability: 'runtime'` so that it is only validated when the app starts:

```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()+++
// required when the app starts, but not validated during the build
+++availability: 'runtime',+++
schema: 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.

### Static variables
### Availability

By default, variables are dynamic. If a variable is configured with `static: true`, it will be inlined into your application code, enabling optimisations like dead-code elimination:
The `availability` property controls when a variable's value is available, and therefore when it is validated. It has three possible values:

- `'dynamic'` (default) — the value is validated and used during runtime and build time.
- `'inline'` — the value is inlined into your application code at build time, enabling optimisations like dead-code elimination. It is validated at build time.
- `'runtime'` — the value is validated and used during runtime only. The value is `undefined` during the build, so the variable is typed as `T | undefined`.

```ts
/// file: src/env.ts
Expand All @@ -151,7 +155,7 @@ import * as v from 'valibot';
export const variables = defineEnvVars({
SHOW_DEBUG_OVERLAY: {
public: true,
+++static: true,+++
+++availability: 'inline',+++

// coerce to true/false
schema: v.pipe(
Expand All @@ -162,7 +166,7 @@ export const variables = defineEnvVars({
});
```

Because this variable is `static`, the `<DebugOverlay>` component shown here will be excluded from the JavaScript bundle unless `SHOW_DEBUG_OVERLAY` is truthy:
Because this variable is `inline`, the `<DebugOverlay>` component shown here will be excluded from the JavaScript bundle unless `SHOW_DEBUG_OVERLAY` is truthy:

```svelte
<script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function load() {
<p>This staging environment was deployed from {data.deploymentGitBranch}.</p>
```

Since all of these variables are unchanged between build time and run time when building on Vercel, we recommend configuring the variable with `static: true` — which will statically replace the variables, enabling optimisations like dead code elimination.
Since all of these variables are unchanged between build time and run time when building on Vercel, we recommend configuring the variable with `availability: 'inline'` — which will statically replace the variables, enabling optimisations like dead code elimination.

## Skew protection

Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-static/test/apps/prerendered/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { defineEnvVars } from '@sveltejs/kit/hooks';
export const variables = defineEnvVars({
PUBLIC_ANSWER: {
public: true,
static: true
availability: 'inline'
}
});
2 changes: 1 addition & 1 deletion packages/adapter-static/test/apps/spa/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { defineEnvVars } from '@sveltejs/kit/hooks';
export const variables = defineEnvVars({
PUBLIC_VALUE: {
public: true,
static: true
availability: 'inline'
}
});
5 changes: 4 additions & 1 deletion packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ export function create_builder({
const issues = {};

for (const [name, config] of Object.entries(variables)) {
if (config.static || !config.public) continue;
const availability = config.availability ?? 'dynamic';
// only `dynamic` public vars are included in the prerendered env.js.
// `inline` is inlined, and `runtime` isn't available at build time
if (!config.public || availability !== 'dynamic') continue;
Comment thread
dummdidumm marked this conversation as resolved.
values[name] = validate(variables, env[name], name, issues);
}

Expand Down
53 changes: 38 additions & 15 deletions packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,33 +99,46 @@ export async function load_explicit_env(kit, file, root, mode) {
export function create_sveltekit_env(variables, env, entry, is_dev) {
const imports = entry
? [
`import { building } from '$app/env/internal';`,
`import { variables } from ${JSON.stringify(entry)};`,
`import { validate, handle_issues } from '@sveltejs/kit/internal/env';`
]
: [`const variables = {};`, `const handle_issues = () => {};`];

/** @type {string[]} */
const declarations = [];
/** @type {string[]} */
const setters = [];
/** @type {string[]} */
const runtime_setters = [];

/** @type {Record<string, StandardSchemaV1.Issue[]>} */
const issues = {};

for (const [name, config] of Object.entries(variables ?? {})) {
Comment thread
dummdidumm marked this conversation as resolved.
if (config?.static) {
if (config.public) {
if (/** @type {any} */ (config)?.static) {
throw new Error('`static: true` has been removed. Use `availability: "inline"` instead.');
}

const availability = config?.availability ?? 'dynamic';

if (availability === 'inline') {
if (config?.public) {
const value = validate(variables ?? {}, env[name], name, issues);
declarations.push(`explicit_public_env.${name} = ${devalue.uneval(value)};`);
}
} else {
setters.push(
const target = availability === 'runtime' ? runtime_setters : setters;

target.push(
`const ${name} = validate(variables, env.${name}, ${JSON.stringify(name)}, issues);`
);

if (config?.public) {
setters.push(`explicit_public_env.${name} = ${name};`);
setters.push(`rendered_env.${name} = ${name};`);
target.push(`explicit_public_env.${name} = ${name};`);
target.push(`rendered_env.${name} = ${name};`);
} else {
setters.push(`dynamic_private_env.${name} = ${name};`);
target.push(`dynamic_private_env.${name} = ${name};`);
}
}
}
Expand All @@ -146,6 +159,11 @@ export function create_sveltekit_env(variables, env, entry, is_dev) {
export function set_env(env) {
const issues = {};
${setters.join('\n')}
${
runtime_setters.length > 0
? `if (!building) {\n\t\t\t\t\t${runtime_setters.join('\n')}\n\t\t\t\t}`
: ''
}
handle_issues(issues);
}`
];
Expand Down Expand Up @@ -187,9 +205,10 @@ export function create_sveltekit_env_private(variables, env) {
for (const [name, config] of Object.entries(variables)) {
if (config.public) continue;

const value = config.static
? devalue.uneval(validate(variables, env[name], name, issues))
: `env.${name}`;
const value =
config.availability === 'inline'
? devalue.uneval(validate(variables, env[name], name, issues))
: `env.${name}`;

exports.push(`export const ${name} = ${value};\n`);
}
Expand Down Expand Up @@ -219,9 +238,10 @@ export function create_sveltekit_env_public(variables, env, prelude) {
for (const [name, config] of Object.entries(variables)) {
if (!config.public) continue;

const value = config.static
? devalue.uneval(validate(variables, env[name], name, issues))
: `env.${name}`;
const value =
config.availability === 'inline'
? devalue.uneval(validate(variables, env[name], name, issues))
: `env.${name}`;

exports.push(`export const ${name} = ${value};\n`);
}
Expand All @@ -243,7 +263,7 @@ export function create_sveltekit_env_public(variables, env, prelude) {
*/
export function create_sveltekit_env_service_worker(variables, env, global, base, app_dir) {
const has_dynamic_public_env = Object.values(variables ?? {}).some(
(config) => config.public && !config.static
(config) => config.public && config.availability !== 'inline'
);

if (!has_dynamic_public_env) {
Expand Down Expand Up @@ -310,9 +330,12 @@ export function create_explicit_env_types(variables, relative, type) {
.filter(([_, config]) => !!config.public === (type === 'public'))
.map(([name, config]) => {
const comment = config.description ? `${create_jsdoc(config.description)}\n` : '';
const maybe_undefined = config.availability === 'runtime';
const type = config.schema
? `import('@sveltejs/kit/internal/types').StandardSchemaV1.InferOutput<typeof import('${relative}').variables.${name}.schema>`
: 'string';
? `import('@sveltejs/kit/internal/types').StandardSchemaV1.InferOutput<typeof import('${relative}').variables.${name}.schema>${maybe_undefined ? ' | undefined' : ''}`
: maybe_undefined
? 'string | undefined'
: 'string';
return `${comment}export const ${name}: ${type};`;
});

Expand Down
11 changes: 6 additions & 5 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2452,12 +2452,13 @@ export interface EnvVarConfig<T> {
*/
public?: boolean;
/**
* Whether the value is determined at build time or when the app runs.
* - if `true`, the build time value is inlined into the bundle. This enables optimisations like dead-code elimination
* - if `false`, the value is read from the environment when the app starts
* @default false
* When the variable's value is available, and therefore when it is validated.
* - `'dynamic'` — the value is validated and used during runtime and build time.
* - `'inline'` — the value is inlined into your application code at build time, enabling optimisations like dead-code elimination. It is validated at build time.
* - `'runtime'` — the value is validated and used during runtime only. The value is `undefined` during the build, so the variable is typed as `T | undefined`.
* @default 'dynamic'
*/
static?: boolean;
availability?: 'inline' | 'dynamic' | 'runtime';
/**
* A [Standard Schema](https://standardschema.dev/) validator that is applied to the value when the app starts.
* The validator can output any value — not necessarily a string — but public, non-static values must be
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1655,11 +1655,11 @@ function kit({ svelte_config }) {
find_deps(vite_manifest, posixify(path.relative(root, entry)), add_dynamic_css, root);

const has_explicit_dynamic_public_env = Object.values(explicit_env_config ?? {}).some(
(variable) => variable.public && !variable.static
(variable) => variable.public && variable.availability !== 'inline'
);

// the app only depends on runtime public env if it imports `$app/env/public`
// *and* at least one public env var is actually dynamic (non-static)
// *and* at least one public env var is actually dynamic (not inlined at build time)
const uses_env_dynamic_public =
has_explicit_dynamic_public_env &&
client_chunks.some(
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/test/apps/async/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import { defineEnvVars } from '@sveltejs/kit/hooks';
export const variables = defineEnvVars({
PRIVATE_STATIC: {
public: false,
static: true
availability: 'inline'
},
PRIVATE_DYNAMIC: {
public: false,
static: false
availability: 'dynamic'
},
PUBLIC_STATIC: {
public: true,
static: true
availability: 'inline'
},
PUBLIC_DYNAMIC: {
public: true,
static: false
availability: 'dynamic'
}
});
14 changes: 7 additions & 7 deletions packages/kit/test/apps/basics/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@ import { defineEnvVars } from '@sveltejs/kit/hooks';
export const variables = defineEnvVars({
PRIVATE_STATIC: {
public: false,
static: true
availability: 'inline'
},
PRIVATE_DYNAMIC: {
public: false,
static: false
availability: 'dynamic'
},
PUBLIC_STATIC: {
public: true,
static: true
availability: 'inline'
},
PUBLIC_DYNAMIC: {
public: true,
static: false
availability: 'dynamic'
},
PUBLIC_THEME: {
public: true,
static: false
availability: 'dynamic'
},
PUBLIC_PRERENDERING: {
public: true,
static: false
availability: 'dynamic'
},
SOME_JSON: {
public: false,
static: true
availability: 'inline'
}
});
8 changes: 4 additions & 4 deletions packages/kit/test/apps/options-2/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import process from 'node:process';
import { building } from '$app/env';
import * as v from 'valibot';

export const variables = {
MESSAGE: {
public: true,
description: 'Public env var loaded from the shared test env directory',
static: !process.env.DYNAMIC_PUBLIC_ENV
availability: process.env.DYNAMIC_PUBLIC_ENV ? 'dynamic' : 'inline'
},
PRIVATE_EXPLICIT_ENV: {},
PRIVATE_STATIC_EXPLICIT_ENV: {
static: true
availability: 'inline'
},
PRIVATE_VALIDATED_DEFAULT_ENV: {
schema: v.optional(v.picklist(['foo', 'bar']), 'foo')
},
RUNTIME_ONLY: {
schema: building ? v.optional(v.string()) : v.string()
availability: 'runtime',
schema: v.string()
}
};
2 changes: 1 addition & 1 deletion packages/kit/test/apps/options/source/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export const variables = defineEnvVars({
public: true
},
TOP_SECRET_SHH_PLS: {
static: true
availability: 'inline'
}
});
4 changes: 2 additions & 2 deletions packages/kit/test/prerendering/basics/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { defineEnvVars } from '@sveltejs/kit/hooks';
export const variables = defineEnvVars({
PUBLIC_STATIC: {
public: true,
static: true
availability: 'inline'
},
PRIVATE_STATIC: {
public: false,
static: true
availability: 'inline'
}
});
11 changes: 6 additions & 5 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2422,12 +2422,13 @@ declare module '@sveltejs/kit' {
*/
public?: boolean;
/**
* Whether the value is determined at build time or when the app runs.
* - if `true`, the build time value is inlined into the bundle. This enables optimisations like dead-code elimination
* - if `false`, the value is read from the environment when the app starts
* @default false
* When the variable's value is available, and therefore when it is validated.
* - `'dynamic'` — the value is validated and used during runtime and build time.
* - `'inline'` — the value is inlined into your application code at build time, enabling optimisations like dead-code elimination. It is validated at build time.
* - `'runtime'` — the value is validated and used during runtime only. The value is `undefined` during the build, so the variable is typed as `T | undefined`.
* @default 'dynamic'
*/
static?: boolean;
availability?: 'inline' | 'dynamic' | 'runtime';
/**
* A [Standard Schema](https://standardschema.dev/) validator that is applied to the value when the app starts.
* The validator can output any value — not necessarily a string — but public, non-static values must be
Expand Down
Loading