diff --git a/.changeset/thick-dolls-run.md b/.changeset/thick-dolls-run.md
new file mode 100644
index 000000000000..ed529a3f1984
--- /dev/null
+++ b/.changeset/thick-dolls-run.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': minor
+---
+
+feat: explicit env vars
diff --git a/documentation/docs/10-getting-started/30-project-structure.md b/documentation/docs/10-getting-started/30-project-structure.md
index 719a0a88d5b4..948ed337a377 100644
--- a/documentation/docs/10-getting-started/30-project-structure.md
+++ b/documentation/docs/10-getting-started/30-project-structure.md
@@ -48,7 +48,7 @@ The `src` directory contains the meat of your project. Everything except `src/ro
- `%sveltekit.body%` — the markup for a rendered page. This should live inside a `
` or other element, rather than directly inside ``, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process. SvelteKit will warn you in development if this is not the case
- `%sveltekit.assets%` — either [`paths.assets`](configuration#paths), if specified, or a relative path to [`paths.base`](configuration#paths)
- `%sveltekit.nonce%` — a [CSP](configuration#csp) nonce for manually included links and scripts, if used
- - `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](configuration#env) (usually `PUBLIC_`). It will fallback to `''` if not matched.
+ - `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](configuration#env) (usually `PUBLIC_`), or be defined as a public variable in `src/env` if using [`experimental.explicitEnvironmentVariables`](environment-variables). It will fallback to `''` if not matched.
- `%sveltekit.version%` — the app version, which can be specified with the [`version`](configuration#version) configuration
- `error.html` is the page that is rendered when everything else fails. It can contain the following placeholders:
- `%sveltekit.status%` — the HTTP status
diff --git a/documentation/docs/20-core-concepts/70-environment-variables.md b/documentation/docs/20-core-concepts/70-environment-variables.md
new file mode 100644
index 000000000000..013389b05c20
--- /dev/null
+++ b/documentation/docs/20-core-concepts/70-environment-variables.md
@@ -0,0 +1,205 @@
+---
+title: Environment variables
+---
+
+Environment variables are values your app needs that exist separately from the app's source code. They allow you to use sensitive information like API keys and database credentials without storing them in version control.
+
+During development, and at build time, variables defined in a `.env` or `.env.local` file will be added to the environment:
+
+```env
+API_KEY=19f401ba-e8b0-48c4-8c77-b0ebb26d97fe
+```
+
+By default, every environment variable is implicitly available inside your app via the following modules:
+
+- [`$env/static/private`]($env-static-private)
+- [`$env/static/public`]($env-static-public)
+- [`$env/dynamic/private`]($env-dynamic-private)
+- [`$env/dynamic/public`]($env-dynamic-public)
+
+## Explicit environment variables
+
+As of SvelteKit 2.62, you can opt into _explicit_ environment variables, in which case you instead import environment variables from these modules:
+
+- [`$app/env/private`]($app-env-private)
+- [`$app/env/public`]($app-env-public)
+
+Additionally, the [`$app/environment`]($app-environment) module is renamed to [`$app/env`]($app-env).
+
+> [!NOTE] Explicit environment variables will become the default in SvelteKit 3. The `$env/*` modules, along with `$app/environment`, will be removed.
+
+### Setup
+
+To opt in, update your configuration...
+
+```js
+/// file: svelte.config.js
+export default {
+ kit: {
+ experimental: {
+ +++explicitEnvironmentVariables: true+++
+ }
+ }
+};
+```
+
+...and add a `src/env.ts` (or `src/env.js`) file that exports a `variables` object:
+
+```ts
+/// file: src/env.ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
+
+export const variables = defineEnvVars({
+ // ...
+});
+```
+
+Each value in the object passed to [`defineEnvVars`](@sveltejs-kit-hooks#defineEnvVars) is an [`EnvVarConfig`](@sveltejs-kit#EnvVarConfig) object that configures the environment variable.
+
+> [!NOTE] `defineEnvVars` returns its argument unaltered — it exists purely to help with type safety.
+
+### Private variables
+
+By default, all variables are considered private. For example, you don't want to reveal your `API_KEY`:
+
+```ts
+/// file: src/env.ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
+
+export const variables = defineEnvVars({
+ +++API_KEY: {}+++
+});
+```
+
+> [!NOTE] Since no configuration is needed for this variable, we can use an empty object (`{}`).
+
+Now that `API_KEY` is defined, it can be imported into app code via `$app/env/private`:
+
+```js
+import { API_KEY } from '$app/env/private';
+```
+
+The `$app/env/private` module cannot be imported into code that runs in the browser, so that you can't accidentally reveal your secrets in a JavaScript bundle.
+
+### Public variables
+
+Some variables are perfectly safe — necessary, even — to expose to the browser. For these, we can specify `public: true`:
+
+```ts
+/// file: src/env.ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
+
+export const variables = defineEnvVars({
+ GOOGLE_ANALYTICS_ID: {
+ +++public: true+++
+ }
+});
+```
+
+`GOOGLE_ANALYTICS_ID` can now be imported from `$app/env/public`, or used in your `app.html` template as `%sveltekit.env.GOOGLE_ANALYTICS_ID%`:
+
+```html
+
+
+
+
+
+
+
+ %sveltekit.head%
+
++++
+ +++
+
+
+
%sveltekit.body%
+
+
+```
+
+### Validation
+
+You can specify a [Standard Schema](https://standardschema.dev/) validator such as [Zod](https://zod.dev/) or [Valibot](https://valibot.dev/) to check that an environment variable value is correct:
+
+```ts
+/// file: src/env.ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
++++import * as v from 'valibot';+++
+
+export const variables = defineEnvVars({
+ GOOGLE_ANALYTICS_ID: {
+ public: true,
+ +++validate: v.pipe(v.string(), v.regex(/G-[A-Z0-9]+/))+++
+ }
+});
+```
+
+If a value is invalid, the app will fail to start (or build).
+
+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
+
+If a variable is configured with `static: true`, it will be inlined into your application code, enabling optimisations like dead-code elimination:
+
+```ts
+/// file: src/env.ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
+import * as v from 'valibot';
+
+export const variables = defineEnvVars({
+ SHOW_DEBUG_OVERLAY: {
+ public: true,
+ +++static: true,+++
+
+ // coerce to true/false
+ validate: v.pipe(
+ v.optional(v.string(), ''),
+ v.transform((str) => str !== '')
+ )
+ }
+});
+```
+
+Because this variable is `static`, the `` component shown here will be excluded from the JavaScript bundle unless `SHOW_DEBUG_OVERLAY` is truthy:
+
+```svelte
+
+
+{#if SHOW_DEBUG_OVERLAY}
+
+{/if}
+```
+
+But if the variable is set before building the app...
+
+```bash
+SHOW_DEBUG_OVERLAY=true npm run build
+```
+
+...then the component will be included and shown.
+
+### Documenting variables
+
+You can document the purpose of an environment variable by adding a `description`:
+
+```ts
+/// file: src/env.ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
+
+export const variables = defineEnvVars({
+ CACHE_TTL_SECONDS: {
+ description: 'How long to cache responses, in seconds'
+ }
+});
+```
+
+Hovering over `CACHE_TTL_SECONDS` in your app code will show the description.
diff --git a/documentation/docs/98-reference/19-$app-env.md b/documentation/docs/98-reference/19-$app-env.md
new file mode 100644
index 000000000000..88531a242957
--- /dev/null
+++ b/documentation/docs/98-reference/19-$app-env.md
@@ -0,0 +1,7 @@
+---
+title: $app/env
+---
+
+> [!NOTE] This is an alias of [`$app/environment`]($app-environment), used when [explicit environment variables](environment-variables#Explicit-environment-variables) are enabled.
+
+> MODULE: $app/env
diff --git a/documentation/docs/98-reference/20-$app-env-private.md b/documentation/docs/98-reference/20-$app-env-private.md
new file mode 100644
index 000000000000..51c2ea22124d
--- /dev/null
+++ b/documentation/docs/98-reference/20-$app-env-private.md
@@ -0,0 +1,9 @@
+---
+title: $app/env/private
+---
+
+Private [environment variables](environment-variables) defined in `src/env.ts` (or `src/env.js`).
+
+To use this module, you must enable the `experimental.explicitEnvironmentVariables` flag in your project configuration.
+
+
diff --git a/documentation/docs/98-reference/20-$app-env-public.md b/documentation/docs/98-reference/20-$app-env-public.md
new file mode 100644
index 000000000000..6845c96858a9
--- /dev/null
+++ b/documentation/docs/98-reference/20-$app-env-public.md
@@ -0,0 +1,9 @@
+---
+title: $app/env/public
+---
+
+Public [environment variables](environment-variables) defined in `src/env.ts` (or `src/env.js`).
+
+To use this module, you must enable the `experimental.explicitEnvironmentVariables` flag in your project configuration.
+
+
diff --git a/packages/kit/kit.vitest.config.js b/packages/kit/kit.vitest.config.js
index 92b0acc01cbc..e006f20ee7f4 100644
--- a/packages/kit/kit.vitest.config.js
+++ b/packages/kit/kit.vitest.config.js
@@ -27,10 +27,11 @@ export default defineConfig({
alias: {
// Order matters: vite prefix-matches with trailing-slash, so longer keys must
// come first to avoid `$app/paths` matching `$app/paths/internal/client`.
+ '$app/env/internal': mock('app-env-internal'),
+ '$app/env': mock('app-env'),
'$app/paths/internal/client': mock('app-paths-internal-client'),
'$app/paths/internal/server': mock('app-paths-internal-server'),
'$app/paths': mock('app-paths'),
- '__sveltekit/environment': mock('sveltekit-environment'),
'__sveltekit/paths': mock('sveltekit-paths')
},
projects: [
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 84af819acd60..2b334ec8341a 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -94,6 +94,10 @@
"#app/paths": {
"browser": "./src/runtime/app/paths/client.js",
"default": "./src/runtime/app/paths/server.js"
+ },
+ "#app/env/public": {
+ "browser": "./src/runtime/app/env/public/client.js",
+ "default": "./src/runtime/app/env/public/server.js"
}
},
"exports": {
@@ -106,6 +110,13 @@
"types": "./types/index.d.ts",
"import": "./src/exports/internal/index.js"
},
+ "./internal/env": {
+ "types": "./types/index.d.ts",
+ "import": "./src/exports/internal/env.js"
+ },
+ "./internal/types": {
+ "import": "./src/exports/internal/types.js"
+ },
"./internal/server": {
"types": "./types/index.d.ts",
"import": "./src/exports/internal/server.js"
diff --git a/packages/kit/scripts/generate-dts.js b/packages/kit/scripts/generate-dts.js
index b470d6d63494..75f000f90bbf 100644
--- a/packages/kit/scripts/generate-dts.js
+++ b/packages/kit/scripts/generate-dts.js
@@ -9,6 +9,7 @@ await createBundle({
'@sveltejs/kit/node': 'src/exports/node/index.js',
'@sveltejs/kit/node/polyfills': 'src/exports/node/polyfills.js',
'@sveltejs/kit/vite': 'src/exports/vite/index.js',
+ '$app/env': 'src/runtime/app/env/types.d.ts',
'$app/environment': 'src/runtime/app/environment/types.d.ts',
'$app/forms': 'src/runtime/app/forms.js',
'$app/navigation': 'src/runtime/app/navigation.js',
diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js
index 98f2cf03d2b2..12210697e5c0 100755
--- a/packages/kit/src/cli.js
+++ b/packages/kit/src/cli.js
@@ -4,6 +4,7 @@ import { parseArgs } from 'node:util';
import colors from 'kleur';
import { load_config } from './core/config/index.js';
import { coalesce_to_error } from './utils/error.js';
+import { resolve_explicit_env_entry } from './core/env.js';
/** @param {unknown} e */
function handle_error(e) {
@@ -77,6 +78,9 @@ if (command === 'sync') {
const config = await load_config();
const sync = await import('./core/sync/sync.js');
sync.all_types(config, values.mode);
+
+ const explicit_env_entry = resolve_explicit_env_entry(config.kit);
+ await sync.env(config.kit, explicit_env_entry, values.mode);
} catch (error) {
handle_error(error);
}
diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js
index bb8f358d93a0..fb618f2f22be 100644
--- a/packages/kit/src/core/adapt/builder.js
+++ b/packages/kit/src/core/adapt/builder.js
@@ -1,8 +1,10 @@
+/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
/** @import { Builder } from '@sveltejs/kit' */
/** @import { ResolvedConfig } from 'vite' */
-/** @import { RouteDefinition } from '@sveltejs/kit' */
+/** @import { RouteDefinition, EnvVarConfig } from '@sveltejs/kit' */
/** @import { RouteData, ValidatedConfig, BuildData, ServerMetadata, ServerMetadataRoute, Prerendered, PrerenderMap, Logger, RemoteChunk } from 'types' */
import colors from 'kleur';
+import * as devalue from 'devalue';
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs';
import { extname, resolve, join, dirname, relative } from 'node:path';
import { pipeline } from 'node:stream';
@@ -17,6 +19,7 @@ import { write } from '../sync/utils.js';
import { list_files } from '../utils.js';
import { find_server_assets } from '../generate_manifest/find_server_assets.js';
import { reserved } from '../env.js';
+import { handle_issues, validate } from '../../exports/internal/env.js';
const pipe = promisify(pipeline);
const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.wasm', '.txt'];
@@ -32,7 +35,8 @@ const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.w
* prerender_map: PrerenderMap;
* log: Logger;
* vite_config: ResolvedConfig;
- * remotes: RemoteChunk[]
+ * remotes: RemoteChunk[];
+ * explicit_env_config: Record> | null;
* }} opts
* @returns {Builder}
*/
@@ -45,7 +49,8 @@ export function create_builder({
prerender_map,
log,
vite_config,
- remotes
+ remotes,
+ explicit_env_config
}) {
/** @type {Map} */
const lookup = new Map();
@@ -168,7 +173,7 @@ export function create_builder({
const fallback = await generate_fallback({
manifest_path,
- env: { ...env.private, ...env.public },
+ env: env.all,
out_dir: config.kit.outDir,
origin: config.kit.prerender.origin,
assets: config.kit.files.assets
@@ -193,7 +198,23 @@ export function create_builder({
const dest = `${config.kit.outDir}/output/prerendered/dependencies/${config.kit.appDir}/env.js`;
const env = get_env(config.kit.env, vite_config.mode);
- write(dest, `export const env=${JSON.stringify(env.public)}`);
+ const values = config.kit.experimental.explicitEnvironmentVariables ? {} : env.public;
+
+ if (config.kit.experimental.explicitEnvironmentVariables) {
+ const variables = explicit_env_config ?? {};
+
+ /** @type {Record} */
+ const issues = {};
+
+ for (const [name, config] of Object.entries(variables)) {
+ if (config.static || !config.public) continue;
+ values[name] = validate(variables, env.all[name], name, issues);
+ }
+
+ handle_issues(issues);
+ }
+
+ write(dest, `export const env=${devalue.uneval(values)}`);
},
generateManifest({ relativePath, routes: subset }) {
diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js
index 48d14116369e..c4a9ea2af613 100644
--- a/packages/kit/src/core/adapt/index.js
+++ b/packages/kit/src/core/adapt/index.js
@@ -10,6 +10,7 @@ import { create_builder } from './builder.js';
* @param {import('types').Logger} log
* @param {import('types').RemoteChunk[]} remotes
* @param {import('vite').ResolvedConfig} vite_config
+ * @param {Record> | null} explicit_env_config
*/
export async function adapt(
config,
@@ -19,7 +20,8 @@ export async function adapt(
prerender_map,
log,
remotes,
- vite_config
+ vite_config,
+ explicit_env_config
) {
// This is only called when adapter is truthy, so the cast is safe
const { name, adapt } = /** @type {import('@sveltejs/kit').Adapter} */ (config.kit.adapter);
@@ -35,7 +37,8 @@ export async function adapt(
prerender_map,
log,
remotes,
- vite_config
+ vite_config,
+ explicit_env_config
});
await adapt(builder);
diff --git a/packages/kit/src/core/config/index.js b/packages/kit/src/core/config/index.js
index 1ef17025d164..e1bc73d0d394 100644
--- a/packages/kit/src/core/config/index.js
+++ b/packages/kit/src/core/config/index.js
@@ -33,11 +33,13 @@ export function load_template(cwd, { kit }) {
}
});
- for (const match of contents.matchAll(/%sveltekit\.env\.([^%]+)%/g)) {
- if (!match[1].startsWith(env.publicPrefix)) {
- throw new Error(
- `Environment variables in ${relative} must start with ${env.publicPrefix} (saw %sveltekit.env.${match[1]}%)`
- );
+ if (!kit.experimental.explicitEnvironmentVariables) {
+ for (const match of contents.matchAll(/%sveltekit\.env\.([^%]+)%/g)) {
+ if (!match[1].startsWith(env.publicPrefix)) {
+ throw new Error(
+ `Environment variables in ${relative} must start with ${env.publicPrefix} (saw %sveltekit.env.${match[1]}%)`
+ );
+ }
}
}
diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js
index 3ce48925de48..914423bdf194 100644
--- a/packages/kit/src/core/config/index.spec.js
+++ b/packages/kit/src/core/config/index.spec.js
@@ -80,6 +80,7 @@ const get_defaults = (prefix = '') => ({
experimental: {
tracing: { server: false },
instrumentation: { server: false },
+ explicitEnvironmentVariables: false,
remoteFunctions: false,
forkPreloads: false,
handleRenderingErrors: false
@@ -108,6 +109,7 @@ const get_defaults = (prefix = '') => ({
resolution: 'client'
},
serviceWorker: {
+ options: undefined,
register: true
},
typescript: {},
diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js
index ac30ce4fbd94..612fa471fb61 100644
--- a/packages/kit/src/core/config/options.js
+++ b/packages/kit/src/core/config/options.js
@@ -139,6 +139,7 @@ const options = object(
instrumentation: object({
server: boolean(false)
}),
+ explicitEnvironmentVariables: boolean(false),
remoteFunctions: boolean(false),
forkPreloads: boolean(false),
handleRenderingErrors: boolean(false)
diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js
index 455eb0f44b68..080e50787e8e 100644
--- a/packages/kit/src/core/env.js
+++ b/packages/kit/src/core/env.js
@@ -1,19 +1,126 @@
+/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
+/** @import { EnvVarConfig } from '@sveltejs/kit' */
+/** @import { ValidatedKitConfig } from 'types' */
+import path from 'node:path';
+import process from 'node:process';
+import * as vite from 'vite';
+import * as devalue from 'devalue';
import { GENERATED_COMMENT } from '../constants.js';
import { dedent } from './sync/utils.js';
-import { runtime_base } from './utils.js';
+import { runtime_base, runtime_directory } from './utils.js';
+import { resolve_entry } from '../utils/filesystem.js';
+import { handle_issues, validate } from '../exports/internal/env.js';
+import { get_config_aliases } from '../exports/vite/utils.js';
/**
* @typedef {'public' | 'private'} EnvType
*/
+let warned = false;
+
+/**
+ * @param {import('types').ValidatedKitConfig} config
+ * @returns {string | null}
+ */
+export function resolve_explicit_env_entry(config) {
+ const resolved = resolve_entry(path.join(config.files.src, 'env'));
+
+ if (resolved) {
+ if (config.experimental.explicitEnvironmentVariables) {
+ return resolved;
+ }
+
+ if (!warned) {
+ console.warn(
+ `${path.relative(process.cwd(), resolved)} requires the \`experimental.explicitEnvironmentVariables\` flag to be set`
+ );
+ warned = true;
+ }
+ } else if (config.experimental.explicitEnvironmentVariables) {
+ console.warn(
+ 'experimental.explicitEnvironmentVariables was set, but no src/env.ts or src/env.js file could be found'
+ );
+ }
+
+ return null;
+}
+
+/**
+ * @param {ValidatedKitConfig} kit
+ * @param {string | null} file
+ * @param {string} mode
+ * @returns {Promise> | null>}
+ */
+export async function load_explicit_env(kit, file, mode) {
+ if (!file) return null;
+
+ const server = await vite.createServer({
+ configFile: false,
+ logLevel: 'silent',
+ mode,
+ define: {
+ __SVELTEKIT_APP_VERSION__: JSON.stringify(kit.version.name) // needed by $app/env
+ },
+ resolve: {
+ alias: [
+ { find: '$app/env', replacement: `${runtime_directory}/app/env` },
+ ...get_config_aliases(kit)
+ ]
+ }
+ });
+
+ /** @type {Record>} */
+ let variables;
+
+ try {
+ ({ variables } = await server.ssrLoadModule(file));
+
+ if (!variables || typeof variables !== 'object') {
+ throw new Error(`${file} must export a variables object`);
+ }
+
+ // validate
+ for (const name of Object.keys(variables)) {
+ if (!valid_identifier.test(name) || reserved.has(name)) {
+ throw new Error(`Invalid environment variable name ${JSON.stringify(name)}`);
+ }
+ }
+ } catch (e) {
+ const error = /** @type {any} */ (e || {});
+
+ if (
+ error.code === 'ERR_MODULE_NOT_FOUND' &&
+ error.message?.includes(`Cannot find module '$app`)
+ ) {
+ throw new Error(
+ `Cannot import \`$app/*\` modules other than \`$app/env\` inside \`src/env\``,
+ { cause: e }
+ );
+ }
+
+ throw error;
+ } finally {
+ await server.close();
+ }
+
+ return variables;
+}
+
/**
* @param {string} id
* @param {Record} env
+ * @param {boolean} disabled
* @returns {string}
*/
-export function create_static_module(id, env) {
+export function create_static_module(id, env, disabled) {
/** @type {string[]} */
- const declarations = [];
+ const statements = [];
+
+ if (disabled) {
+ statements.push(
+ `throw new Error('Cannot import \`${id}\` when \`experimental.explicitEnvironmentVariables\` is enabled. Use \`${id.replace('$env/static', '$app/env')}\` instead.');`
+ );
+ }
for (const key in env) {
if (!valid_identifier.test(key) || reserved.has(key)) {
@@ -23,24 +130,128 @@ export function create_static_module(id, env) {
const comment = `/** @type {import('${id}').${key}} */`;
const declaration = `export const ${key} = ${JSON.stringify(env[key])};`;
- declarations.push(`${comment}\n${declaration}`);
+ statements.push(`${comment}\n${declaration}`);
}
- return GENERATED_COMMENT + declarations.join('\n\n');
+ return GENERATED_COMMENT + statements.join('\n\n');
}
/**
* @param {EnvType} type
* @param {Record | undefined} dev_values If in a development mode, values to pre-populate the module with.
+ * @param {boolean} disabled
*/
-export function create_dynamic_module(type, dev_values) {
+export function create_dynamic_module(type, dev_values, disabled) {
+ const prelude = disabled
+ ? `throw new Error('Cannot import \`$env/dynamic/${type}\` when \`experimental.explicitEnvironmentVariables\` is enabled. Use \`$app/env/${type}\` instead.');\n\n`
+ : '';
+
if (dev_values) {
const keys = Object.entries(dev_values).map(
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
);
- return `export const env = {\n${keys.join(',\n')}\n}`;
+ return `${prelude}export const env = {\n${keys.join(',\n')}\n}`;
}
- return `export { ${type}_env as env } from '${runtime_base}/shared-server.js';`;
+ return `${prelude}export { ${type}_env as env } from '${runtime_base}/shared-server.js';`;
+}
+
+/**
+ * Creates the `__sveltekit/env` module
+ * @param {Record> | null} variables
+ * @param {Record} 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 = () => {};`];
+
+ const declarations = [];
+ const setters = [];
+
+ /** @type {Record} */
+ const issues = {};
+
+ for (const [name, config] of Object.entries(variables ?? {})) {
+ if (config.static) {
+ const value = validate(variables ?? {}, env[name], name, issues);
+ declarations.push(`export const ${name} = ${devalue.uneval(value)};`);
+
+ if (config.public) {
+ declarations.push(`explicit_public_env.${name} = ${name};`);
+ }
+ } else {
+ declarations.push(`export var ${name};`);
+ setters.push(`${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};`);
+ }
+ }
+ }
+
+ handle_issues(issues);
+
+ const blocks = [
+ GENERATED_COMMENT,
+ imports.join('\n'),
+ `const issues = {};`,
+ 'export { variables }',
+ 'export const explicit_public_env = {};',
+ 'export const rendered_env = {};',
+ ...declarations,
+ `handle_issues(issues);`,
+ dedent`
+ export function set_env(env) {
+ const issues = {};
+ ${setters.join('\n')}
+ handle_issues(issues);
+ }`
+ ];
+
+ const module = blocks.join('\n\n');
+
+ return module;
+}
+
+/**
+ * Creates the `__sveltekit/env/browser` module
+ * @param {Record> | null} variables
+ * @param {Record} env
+ * @param {string} global
+ */
+export function create_sveltekit_env_browser(variables, env, global) {
+ if (!variables) {
+ return '';
+ }
+
+ /** @type {Record} */
+ const issues = {};
+
+ const exports = Object.entries(variables).map(([name, config]) => {
+ if (config.static) {
+ const value = validate(variables, env[name], name, issues);
+ return `export const ${name} = ${devalue.uneval(value)};\n`;
+ }
+
+ return `export const ${name} = env.${name};\n`;
+ });
+
+ handle_issues(issues);
+
+ return `const env = ${global}.env;\n\n${exports.join('')}`;
+}
+
+/** @param {string} description */
+function create_jsdoc(description) {
+ return `/**\n${description
+ .split('\n')
+ .map((line) => ` * ${line.replaceAll('*/', '*\\/')}`)
+ .join('\n')}\n */`;
}
/**
@@ -98,6 +309,29 @@ export function create_dynamic_types(id, env, { public_prefix, private_prefix })
`;
}
+/**
+ * @param {Record>} variables
+ * @param {string} relative
+ * @param {EnvType} type
+ */
+export function create_explicit_env_types(variables, relative, type) {
+ const declarations = Object.entries(variables)
+ .filter(([_, config]) => !!config.public === (type === 'public'))
+ .map(([name, config]) => {
+ const comment = config.description ? `${create_jsdoc(config.description)}\n` : '';
+ const type = config.schema
+ ? `import('@sveltejs/kit/internal/types').StandardSchemaV1.InferOutput`
+ : 'string';
+ return `${comment}export const ${name}: ${type};`;
+ });
+
+ return dedent`
+ declare module '$app/env/${type}' {
+ ${declarations.join('\n') || `// no ${type} environment variables were defined`}
+ }
+ `;
+}
+
export const reserved = new Set([
'do',
'if',
diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js
index 9255dd40f5f3..81789c7c34d2 100644
--- a/packages/kit/src/core/postbuild/analyse.js
+++ b/packages/kit/src/core/postbuild/analyse.js
@@ -50,7 +50,7 @@ async function analyse({
installPolyfills();
- // configure `import { building } from '$app/environment'` —
+ // configure `import { building } from '$app/environment'` and `$app/env` —
// essential we do this before analysing the code
internal.set_building();
@@ -60,6 +60,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);
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));
diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js
index 9251d6fac173..a811be3e2f3d 100644
--- a/packages/kit/src/core/postbuild/prerender.js
+++ b/packages/kit/src/core/postbuild/prerender.js
@@ -46,7 +46,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
/** @type {import('types').ServerModule} */
const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href);
- // configure `import { building } from '$app/environment'` —
+ // configure `import { building } from '$app/environment'` and `$app/env` —
// essential we do this before analysing the code
internal.set_building();
internal.set_prerendering();
@@ -497,6 +497,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);
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`));
diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js
index d9680e8ab65f..643f04341dad 100644
--- a/packages/kit/src/core/sync/sync.js
+++ b/packages/kit/src/core/sync/sync.js
@@ -11,6 +11,8 @@ import {
create_node_analyser,
get_page_options
} from '../../exports/vite/static_analysis/index.js';
+import { load_explicit_env } from '../env.js';
+import { write_env } from './write_env.js';
/**
* Initialize SvelteKit's generated files that only depend on the config and mode.
@@ -87,6 +89,20 @@ export function all_types(config, mode) {
write_non_ambient(config.kit, manifest_data);
}
+/**
+ * Generate modules and types for explicit env vars
+ * @param {import('types').ValidatedKitConfig} kit
+ * @param {string | null} entry
+ * @param {string} mode The Vite mode
+ */
+export async function env(kit, entry, mode) {
+ const env_config = await load_explicit_env(kit, entry, mode);
+
+ write_env(kit, entry, env_config);
+
+ return env_config;
+}
+
/**
* Regenerate __SERVER__/internal.js in response to src/{app.html,error.html,service-worker.js} changing
* @param {import('types').ValidatedConfig} config
diff --git a/packages/kit/src/core/sync/write_ambient.js b/packages/kit/src/core/sync/write_ambient.js
index 1f2188097ade..dd798178c86f 100644
--- a/packages/kit/src/core/sync/write_ambient.js
+++ b/packages/kit/src/core/sync/write_ambient.js
@@ -53,11 +53,17 @@ ${create_dynamic_types('public', env, prefixes)}
* @param {string} mode The Vite mode
*/
export function write_ambient(config, mode) {
- const env = get_env(config.env, mode);
- const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
+ /** @type {string} */
+ let content;
- write_if_changed(
- path.join(config.outDir, 'ambient.d.ts'),
- template(env, { public_prefix, private_prefix })
- );
+ if (config.experimental.explicitEnvironmentVariables) {
+ content = `${GENERATED_COMMENT}\n/// `;
+ } else {
+ const env = get_env(config.env, mode);
+ const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env;
+
+ content = template(env, { public_prefix, private_prefix });
+ }
+
+ write_if_changed(path.join(config.outDir, 'ambient.d.ts'), content);
}
diff --git a/packages/kit/src/core/sync/write_env.js b/packages/kit/src/core/sync/write_env.js
new file mode 100644
index 000000000000..07be47bba8e9
--- /dev/null
+++ b/packages/kit/src/core/sync/write_env.js
@@ -0,0 +1,32 @@
+/** @import { EnvVarConfig } from '@sveltejs/kit' */
+import path from 'node:path';
+import { create_explicit_env_types } from '../env.js';
+import { write_if_changed } from './utils.js';
+
+const DOCS = '// See https://svelte.dev/docs/kit/environment-variables for more information';
+
+/**
+ * Writes ambient declarations including types reference to @sveltejs/kit,
+ * and the existing environment variables in process.env to
+ * $env/static/private and $env/static/public
+ * @param {import('types').ValidatedKitConfig} kit
+ * @param {string | null} entry
+ * @param {Record> | null} env_config
+ */
+export function write_env(kit, entry, env_config) {
+ const content = [];
+ const out = path.join(kit.outDir, 'env.d.ts');
+
+ if (entry && env_config) {
+ const relative = path.relative(kit.outDir, entry);
+ content.push(
+ `// This file is generated from ${relative}.\n${DOCS}`,
+ create_explicit_env_types(env_config, relative, 'private'),
+ create_explicit_env_types(env_config, relative, 'public')
+ );
+ } else {
+ content.push(DOCS);
+ }
+
+ write_if_changed(out, content.join('\n\n'));
+}
diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js
index 445c17e45dbc..213e5b88ddd5 100644
--- a/packages/kit/src/core/sync/write_root.js
+++ b/packages/kit/src/core/sync/write_root.js
@@ -100,7 +100,7 @@ export function write_root(manifest_data, config, output) {
${isSvelte5Plus() ? '' : ''}
-