From 49c96fecb79c7ea902e9ce22ebb574752ae5e99e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Jul 2026 19:40:50 -0400 Subject: [PATCH 1/3] feat: add `optional` option to `EnvVarConfig` for optional environment variables --- .changeset/optional-env-vars.md | 5 + .../70-environment-variables.md | 18 ++++ packages/kit/src/core/env.js | 2 +- packages/kit/src/exports/internal/env.js | 6 +- packages/kit/src/exports/internal/env.spec.js | 91 +++++++++++++++++++ packages/kit/src/exports/public.d.ts | 7 ++ packages/kit/types/index.d.ts | 7 ++ 7 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 .changeset/optional-env-vars.md create mode 100644 packages/kit/src/exports/internal/env.spec.js diff --git a/.changeset/optional-env-vars.md b/.changeset/optional-env-vars.md new file mode 100644 index 000000000000..3c5f625761ed --- /dev/null +++ b/.changeset/optional-env-vars.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `optional` option to `EnvVarConfig` for optional environment variables diff --git a/documentation/docs/20-core-concepts/70-environment-variables.md b/documentation/docs/20-core-concepts/70-environment-variables.md index d785468585c0..5740aefa5874 100644 --- a/documentation/docs/20-core-concepts/70-environment-variables.md +++ b/documentation/docs/20-core-concepts/70-environment-variables.md @@ -139,6 +139,24 @@ export const variables = defineEnvVars({ 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. +### Optional variables + +By default, every variable is required: if a value is missing when the app runs, validation will fail. To allow a variable to be missing, set `optional: true`: + +```ts +/// file: src/env.ts +import { defineEnvVars } from '@sveltejs/kit/hooks'; + +export const variables = defineEnvVars({ + GOOGLE_ANALYTICS_ID: { + public: true, + +++optional: true+++ + } +}); +``` + +When a variable is optional, its type is `string | undefined` (or, if a `schema` is provided, the schema's output type `| undefined`), and validation is skipped when the value is missing. This is equivalent to using a schema that accepts an optional value, but without the boilerplate. + ### Static variables 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: diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 63c493e61875..ff73a392e86e 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -313,7 +313,7 @@ export function create_explicit_env_types(variables, relative, type) { const type = config.schema ? `import('@sveltejs/kit/internal/types').StandardSchemaV1.InferOutput` : 'string'; - return `${comment}export const ${name}: ${type};`; + return `${comment}export const ${name}: ${config.optional ? `${type} | undefined` : type};`; }); return dedent` diff --git a/packages/kit/src/exports/internal/env.js b/packages/kit/src/exports/internal/env.js index b34a8636ef74..c5173705375a 100644 --- a/packages/kit/src/exports/internal/env.js +++ b/packages/kit/src/exports/internal/env.js @@ -4,7 +4,7 @@ import { stackless } from '../../utils/error.js'; const MISSING = { - message: `Value is missing. If it is optional, add a Standard Schema validator declaring it as such.` + message: `Value is missing. If it is optional, set \`optional: true\` or add a Standard Schema validator declaring it as such.` }; const BAD_VALIDATOR = { @@ -26,6 +26,10 @@ export function validate(variables, value, name, issues) { const config = variables[name] ?? {}; const validator = config.schema; + if (config.optional && !value) { + return undefined; + } + if (!validator) { if (!value) issues[name] = [MISSING]; return value; diff --git a/packages/kit/src/exports/internal/env.spec.js b/packages/kit/src/exports/internal/env.spec.js new file mode 100644 index 000000000000..f0320d582c28 --- /dev/null +++ b/packages/kit/src/exports/internal/env.spec.js @@ -0,0 +1,91 @@ +import { assert, describe, test } from 'vitest'; +import { validate, handle_issues } from './env.js'; + +/** + * @param {Record} variables + * @param {string | undefined} value + * @param {string} name + */ +function run(variables, value, name) { + /** @type {Record} */ + const issues = {}; + const result = validate(variables, value, name, issues); + return { result, issues }; +} + +describe('validate', () => { + test('records a MISSING issue when a required variable without a schema is undefined', () => { + const { result, issues } = run({ FOO: {} }, undefined, 'FOO'); + assert.equal(result, undefined); + assert.deepEqual(issues, { FOO: [issues.FOO[0]] }); + assert.match(issues.FOO[0].message, /Value is missing/); + }); + + test('does not record an issue when an optional variable without a schema is undefined', () => { + const { result, issues } = run({ FOO: { optional: true } }, undefined, 'FOO'); + assert.equal(result, undefined); + assert.deepEqual(issues, {}); + }); + + test('does not record an issue when an optional variable without a schema is an empty string', () => { + const { result, issues } = run({ FOO: { optional: true } }, '', 'FOO'); + assert.equal(result, undefined); + assert.deepEqual(issues, {}); + }); + + test('returns the value unchanged when a required variable without a schema is present', () => { + const { result, issues } = run({ FOO: {} }, 'bar', 'FOO'); + assert.equal(result, 'bar'); + assert.deepEqual(issues, {}); + }); + + test('returns the value unchanged when an optional variable is present', () => { + const { result, issues } = run({ FOO: { optional: true } }, 'bar', 'FOO'); + assert.equal(result, 'bar'); + assert.deepEqual(issues, {}); + }); + + test('skips the schema validator when an optional variable is undefined', () => { + /** @type {any} */ + const validator = { + '~standard': { + validate(/** @type {any} */ value) { + if (value === undefined) return { issues: [{ message: 'nope' }] }; + return { value }; + } + } + }; + const { result, issues } = run( + { FOO: { optional: true, schema: validator } }, + undefined, + 'FOO' + ); + assert.equal(result, undefined); + assert.deepEqual(issues, {}); + }); + + test('runs the schema validator when an optional variable is present', () => { + /** @type {any} */ + const validator = { + '~standard': { + validate(/** @type {any} */ value) { + return { value: `validated:${value}` }; + } + } + }; + const { result, issues } = run({ FOO: { optional: true, schema: validator } }, 'bar', 'FOO'); + assert.equal(result, 'validated:bar'); + assert.deepEqual(issues, {}); + }); + + test('handle_issues throws when there are issues', () => { + assert.throws( + () => handle_issues({ FOO: [{ message: 'bad' }] }), + /Invalid environment variables/ + ); + }); + + test('handle_issues is a no-op when there are no issues', () => { + handle_issues({}); + }); +}); diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index cab6332bac14..b2c1b55a5aa7 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -2472,6 +2472,13 @@ export interface EnvVarConfig { * A description of the variable that will be used for inline documentation on hover. */ description?: string; + /** + * Whether the variable is required. + * - if `true`, validation will not fail when the value is `undefined`. Its type will be `T | undefined` + * - if `false`, the value must be present (and pass validation, if a `schema` is provided) + * @default false + */ + optional?: boolean; } export * from './index.js'; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 6f5d75500c29..75af5f1e910b 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2443,6 +2443,13 @@ declare module '@sveltejs/kit' { * A description of the variable that will be used for inline documentation on hover. */ description?: string; + /** + * Whether the variable is required. + * - if `true`, validation will not fail when the value is `undefined`. Its type will be `T | undefined` + * - if `false`, the value must be present (and pass validation, if a `schema` is provided) + * @default false + */ + optional?: boolean; } interface AdapterEntry { /** From d7be1f17ac16b9ad4eee02de54216100cbed5c71 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 13 Jul 2026 20:16:14 -0400 Subject: [PATCH 2/3] prettier --- packages/kit/src/exports/internal/env.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/kit/src/exports/internal/env.spec.js b/packages/kit/src/exports/internal/env.spec.js index f0320d582c28..7ff95661a4a7 100644 --- a/packages/kit/src/exports/internal/env.spec.js +++ b/packages/kit/src/exports/internal/env.spec.js @@ -49,10 +49,10 @@ describe('validate', () => { /** @type {any} */ const validator = { '~standard': { - validate(/** @type {any} */ value) { - if (value === undefined) return { issues: [{ message: 'nope' }] }; - return { value }; - } + validate(/** @type {any} */ value) { + if (value === undefined) return { issues: [{ message: 'nope' }] }; + return { value }; + } } }; const { result, issues } = run( @@ -68,9 +68,9 @@ describe('validate', () => { /** @type {any} */ const validator = { '~standard': { - validate(/** @type {any} */ value) { - return { value: `validated:${value}` }; - } + validate(/** @type {any} */ value) { + return { value: `validated:${value}` }; + } } }; const { result, issues } = run({ FOO: { optional: true, schema: validator } }, 'bar', 'FOO'); From adacf99c94a25b44c141e82ad128ebb830ca31a4 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:36:57 +0000 Subject: [PATCH 3/3] Fix: JSDoc summary line for the `optional` property reads "Whether the variable is required.", which is inverted and contradicts the property name and its own bullet points. This commit fixes the issue reported at packages/kit/src/exports/public.d.ts:2478 ## Bug In `packages/kit/src/exports/public.d.ts` (line ~2476) the JSDoc for the `optional?: boolean` property begins with: ``` Whether the variable is required. ``` This summary is semantically inverted: - The property is named `optional`. - The bullet points state that `true` allows the value to be `undefined` (i.e. makes it optional) and `false` requires the value to be present. - `@default false`. So a value of `true` makes the variable **optional**, not required. The summary line describes the opposite of what the property does. Anyone hovering over `optional` in their editor gets a misleading one-line description. ## Fix Changed the summary line to `Whether the variable is optional.` in both: - `packages/kit/src/exports/public.d.ts` (source) - `packages/kit/types/index.d.ts` (generated output, kept in sync as required by the guideline to commit generated types) The bullet points and `@default` were already correct and were left unchanged. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/kit/src/exports/public.d.ts | 2 +- packages/kit/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index b2c1b55a5aa7..e074ba817036 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -2473,7 +2473,7 @@ export interface EnvVarConfig { */ description?: string; /** - * Whether the variable is required. + * Whether the variable is optional. * - if `true`, validation will not fail when the value is `undefined`. Its type will be `T | undefined` * - if `false`, the value must be present (and pass validation, if a `schema` is provided) * @default false diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 75af5f1e910b..e720b5e56be5 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2444,7 +2444,7 @@ declare module '@sveltejs/kit' { */ description?: string; /** - * Whether the variable is required. + * Whether the variable is optional. * - if `true`, validation will not fail when the value is `undefined`. Its type will be `T | undefined` * - if `false`, the value must be present (and pass validation, if a `schema` is provided) * @default false