Skip to content
Closed
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/optional-env-vars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `optional` option to `EnvVarConfig` for optional environment variables
18 changes: 18 additions & 0 deletions documentation/docs/20-core-concepts/70-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ export function create_explicit_env_types(variables, relative, type) {
const type = config.schema
? `import('@sveltejs/kit/internal/types').StandardSchemaV1.InferOutput<typeof import('${relative}').variables.${name}.schema>`
: 'string';
return `${comment}export const ${name}: ${type};`;
return `${comment}export const ${name}: ${config.optional ? `${type} | undefined` : type};`;
});

return dedent`
Expand Down
6 changes: 5 additions & 1 deletion packages/kit/src/exports/internal/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand Down
91 changes: 91 additions & 0 deletions packages/kit/src/exports/internal/env.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { assert, describe, test } from 'vitest';
import { validate, handle_issues } from './env.js';

/**
* @param {Record<string, any>} variables
* @param {string | undefined} value
* @param {string} name
*/
function run(variables, value, name) {
/** @type {Record<string, any[]>} */
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({});
});
});
7 changes: 7 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2472,6 +2472,13 @@ export interface EnvVarConfig<T> {
* A description of the variable that will be used for inline documentation on hover.
*/
description?: string;
/**
* 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)
Comment thread
vercel[bot] marked this conversation as resolved.
* @default false
*/
optional?: boolean;
}

export * from './index.js';
7 changes: 7 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
*/
optional?: boolean;
}
interface AdapterEntry {
/**
Expand Down
Loading