Skip to content
Open
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/proud-socks-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: `$env/*` modules can now be imported from Playwright and other code running without Vite
2 changes: 1 addition & 1 deletion packages/kit/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const prog = sade('svelte-kit').version(pkg.version);

prog
.command('sync')
.describe('Synchronise generated type definitions')
.describe('Synchronise generated type definitions and $env modules')
.option('--mode', 'Specify a mode for loading environment variables', 'development')
.action(async ({ mode }) => {
const config_files = ['js', 'ts']
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import zlib from 'node:zlib';
import { copy, rimraf, mkdirp, posixify } from '../../utils/filesystem.js';
import { generate_manifest } from '../generate_manifest/index.js';
import { get_route_segments } from '../../utils/routing.js';
import { get_env } from '../../exports/vite/utils.js';
import { get_env } from '../../exports/vite/env.js';
import generate_fallback from '../postbuild/fallback.js';
import { write } from '../sync/utils.js';
import { list_files } from '../utils.js';
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function create_static_module(id, env) {
/**
* @param {EnvType} type
* @param {Record<string, string> | undefined} dev_values If in a development mode, values to pre-populate the module with.
* @returns {string}
*/
export function create_dynamic_module(type, dev_values) {
if (dev_values) {
Expand Down
17 changes: 12 additions & 5 deletions packages/kit/src/core/sync/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import { write_types, write_all_types } from './write_types/index.js';
import { write_ambient } from './write_ambient.js';
import { write_non_ambient } from './write_non_ambient.js';
import { write_server } from './write_server.js';
import { write_env } from './write_env.js';
import { get_env } from '../../exports/vite/env.js';

/**
* Initialize SvelteKit's generated files that only depend on the config and mode.
* @param {import('types').ValidatedConfig} config
* @param {string} mode
* @param {import('../../exports/vite/types.js').Env} env
*/
export function init(config, mode) {
export function init(config, mode, env) {
write_env(config.kit, mode, env);
write_tsconfig(config.kit);
write_ambient(config.kit, mode);
}
Expand Down Expand Up @@ -52,19 +56,22 @@ export function update(config, manifest_data, file) {
* Run sync.init and sync.create in series, returning the result from sync.create.
* @param {import('types').ValidatedConfig} config
* @param {string} mode The Vite mode
* @param {import('../../exports/vite/types.js').Env} env
*/
export function all(config, mode) {
init(config, mode);
export function all(config, mode, env) {
init(config, mode, env);
return create(config);
}

/**
* Run sync.init and then generate all type files.
* Run sync.init and then generate all type files and $env modules.
* @param {import('types').ValidatedConfig} config
* @param {string} mode The Vite mode
*/
export function all_types(config, mode) {
init(config, mode);
const env = get_env(config.kit.env, mode);

init(config, mode, env);
const manifest_data = create_manifest_data({ config });
write_all_types(config, manifest_data);
write_non_ambient(config.kit, manifest_data);
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/sync/write_ambient.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { get_env } from '../../exports/vite/utils.js';
import { get_env } from '../../exports/vite/env.js';
import { GENERATED_COMMENT } from '../../constants.js';
import { create_dynamic_types, create_static_types } from '../env.js';
import { write_if_changed } from './utils.js';
Expand Down
65 changes: 65 additions & 0 deletions packages/kit/src/core/sync/write_env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import path from 'node:path';
import { dedent, write_if_changed } from './utils.js';
import { create_static_module } from '../env.js';
import { runtime_directory } from '../utils.js';
import { s } from '../../utils/misc.js';

/**
* This version deviates from the one in env.js because we don't want to
* serialise the user's dynamic environment variables. Instead, it loads the
* environment variables directly. This is okay because it will only be used by
* modules importing $env/dynamic/* from outside the Vite pipeline. Those inside
* the Vite pipeline will load the virtual module which reuses the already loaded
* environment variables.
* @param {import('../env.js').EnvType} type
* @returns {string}
*/
function create_dynamic_module(type) {
return dedent`
import { env as full_env } from './internal.js';

export const env = full_env.${type};
`;
}

/**
* Writes env variable modules to the output directory
* @param {import('types').ValidatedKitConfig} config
* @param {string} mode
* @param {import('../../exports/vite/types.js').Env} env
*/
export function write_env(config, mode, env) {
const env_static_private = create_static_module('$env/static/private', env.private);
write_if_changed(
path.join(config.outDir, 'generated', 'env', 'static', 'private.js'),
env_static_private
);

const env_static_public = create_static_module('$env/static/public', env.public);
write_if_changed(
path.join(config.outDir, 'generated', 'env', 'static', 'public.js'),
env_static_public
);

const env_dynamic = dedent`
import { get_env } from '${runtime_directory}/../exports/vite/env.js';

export const env = get_env(${s(config.env)}, ${s(mode)});
`;
write_if_changed(
path.join(config.outDir, 'generated', 'env', 'dynamic', 'internal.js'),
env_dynamic
);

const env_dynamic_private = create_dynamic_module('private');
write_if_changed(
path.join(config.outDir, 'generated', 'env', 'dynamic', 'private.js'),
env_dynamic_private
);

const env_dynamic_public = create_dynamic_module('public');
write_if_changed(
path.join(config.outDir, 'generated', 'env', 'dynamic', 'public.js'),
env_dynamic_public
);
}
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_root.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dedent, isSvelte5Plus, write_if_changed } from './utils.js';

/**
* Creates the "App.svelte" root component used to mount the user's layouts and pages
* @param {import('types').ManifestData} manifest_data
* @param {string} output
*/
Expand Down
13 changes: 9 additions & 4 deletions packages/kit/src/core/sync/write_tsconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ export function get_tsconfig(kit) {

// Test folder is a special case - we advocate putting tests in a top-level test folder
// and it's not configurable (should we make it?)
const test_folder = project_relative('tests');
include.add(config_relative(`${test_folder}/**/*.js`));
include.add(config_relative(`${test_folder}/**/*.ts`));
include.add(config_relative(`${test_folder}/**/*.svelte`));
const tests_folder = project_relative('tests');
include.add(config_relative(`${tests_folder}/**/*.js`));
include.add(config_relative(`${tests_folder}/**/*.ts`));
include.add(config_relative(`${tests_folder}/**/*.svelte`));

const exclude = [config_relative('node_modules/**')];
// Add service worker to exclude list so that worker types references in it don't spill over into the rest of the app
Expand All @@ -100,6 +100,11 @@ export function get_tsconfig(kit) {
// generated options
paths: {
...get_tsconfig_paths(kit),
// This allows files outside the Vite pipeline to access $env
'$env/static/private': ['./generated/env/static/private.js'],
'$env/static/public': ['./generated/env/static/public.js'],
'$env/dynamic/private': ['./generated/env/dynamic/private.js'],
'$env/dynamic/public': ['./generated/env/dynamic/public.js'],
'$app/types': ['./types/index.d.ts']
},
rootDirs: [config_relative('.'), './types'],
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/core/sync/write_tsconfig.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ test('Creates tsconfig path aliases from kit.alias', () => {
// check in the implementation
expect(compilerOptions.paths).toEqual({
'$app/types': ['./types/index.d.ts'],
'$env/static/private': ['./generated/env/static/private.js'],
'$env/static/public': ['./generated/env/static/public.js'],
'$env/dynamic/private': ['./generated/env/dynamic/private.js'],
'$env/dynamic/public': ['./generated/env/dynamic/public.js'],
simpleKey: ['../simple/value'],
'simpleKey/*': ['../simple/value/*'],
key: ['../value'],
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/exports/vite/build/build_service_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import fs from 'node:fs';
import * as vite from 'vite';
import { dedent } from '../../../core/sync/utils.js';
import { s } from '../../../utils/misc.js';
import { get_config_aliases, strip_virtual_prefix, get_env, normalize_id } from '../utils.js';
import { get_config_aliases, strip_virtual_prefix, normalize_id } from '../utils.js';
import { get_env } from '../env.js';
import { create_static_module } from '../../../core/env.js';
import { env_static_public, service_worker } from '../module_ids.js';

Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/;
* @param {import('vite').ResolvedConfig} vite_config
* @param {import('types').ValidatedConfig} svelte_config
* @param {() => Array<{ hash: string, file: string }>} get_remotes
* @param {import('../types.js').Env} kit_env
* @return {Promise<Promise<() => void>>}
*/
export async function dev(vite, vite_config, svelte_config, get_remotes) {
export async function dev(vite, vite_config, svelte_config, get_remotes, kit_env) {
installPolyfills();

const async_local_storage = new AsyncLocalStorage();
Expand All @@ -55,7 +56,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) {
return fetch(info, init);
};

sync.init(svelte_config, vite_config.mode);
sync.init(svelte_config, vite_config.mode, kit_env);

/** @type {import('types').ManifestData} */
let manifest_data;
Expand Down
18 changes: 18 additions & 0 deletions packages/kit/src/exports/vite/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { loadEnv } from 'vite';
import { filter_env } from '../../utils/env.js';

/**
* Load environment variables from process.env and .env files
* @param {{ dir: string; publicPrefix: string; privatePrefix: string }} env_config
* @param {string} mode
* @returns {import('./types.js').Env}
*/
export function get_env(env_config, mode) {
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = env_config;
const env = loadEnv(mode, env_config.dir, '');

return {
public: filter_env(env, public_prefix, private_prefix),
private: filter_env(env, private_prefix, public_prefix)
};
}
23 changes: 9 additions & 14 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import process from 'node:process';
import colors from 'kleur';

import { copy, mkdirp, posixify, read, resolve_entry, rimraf } from '../../utils/filesystem.js';
import { create_static_module, create_dynamic_module } from '../../core/env.js';
import { create_dynamic_module } from '../../core/env.js';
import * as sync from '../../core/sync/sync.js';
import { create_assets } from '../../core/sync/create_manifest_data/index.js';
import { runtime_directory, logger } from '../../core/utils.js';
Expand All @@ -16,13 +16,7 @@ import { build_service_worker } from './build/build_service_worker.js';
import { assets_base, find_deps, resolve_symlinks } from './build/utils.js';
import { dev } from './dev/index.js';
import { preview } from './preview/index.js';
import {
error_for_missing_config,
get_config_aliases,
get_env,
normalize_id,
stackless
} from './utils.js';
import { error_for_missing_config, get_config_aliases, normalize_id, stackless } from './utils.js';
import { write_client_manifest } from '../../core/sync/write_client_manifest.js';
import prerender from '../../core/postbuild/prerender.js';
import analyse from '../../core/postbuild/analyse.js';
Expand All @@ -41,6 +35,7 @@ import {
import { import_peer } from '../../utils/import.js';
import { compact } from '../../utils/array.js';
import { should_ignore } from './static_analysis/utils.js';
import { get_env } from './env.js';

const cwd = posixify(process.cwd());

Expand Down Expand Up @@ -209,7 +204,7 @@ async function kit({ svelte_config }) {
/** @type {boolean} */
let is_build;

/** @type {{ public: Record<string, string>; private: Record<string, string> }} */
/** @type {import('./types.js').Env} */
let env;

/** @type {() => Promise<void>} */
Expand Down Expand Up @@ -370,7 +365,7 @@ async function kit({ svelte_config }) {
};

if (!secondary_build_started) {
manifest_data = sync.all(svelte_config, config_env.mode).manifest_data;
manifest_data = sync.all(svelte_config, config_env.mode, env).manifest_data;
// During the initial server build we don't know yet
new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = 'true';
new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = 'true';
Expand Down Expand Up @@ -474,10 +469,10 @@ async function kit({ svelte_config }) {

switch (id) {
case env_static_private:
return create_static_module('$env/static/private', env.private);
return read(`${kit.outDir}/generated/env/static/private.js`);

case env_static_public:
return create_static_module('$env/static/public', env.public);
return read(`${kit.outDir}/generated/env/static/public.js`);

case env_dynamic_private:
return create_dynamic_module(
Expand Down Expand Up @@ -590,7 +585,7 @@ async function kit({ svelte_config }) {

if (is_server_only) {
// in dev, this doesn't exist, so we need to create it
manifest_data ??= sync.all(svelte_config, vite_config_env.mode).manifest_data;
manifest_data ??= sync.all(svelte_config, vite_config_env.mode, env).manifest_data;

/** @type {Set<string>} */
const entrypoints = new Set();
Expand Down Expand Up @@ -975,7 +970,7 @@ async function kit({ svelte_config }) {
* @see https://vitejs.dev/guide/api-plugin.html#configureserver
*/
async configureServer(vite) {
return await dev(vite, vite_config, svelte_config, () => remotes);
return await dev(vite, vite_config, svelte_config, () => remotes, env);
},

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/kit/src/exports/vite/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export interface EnforcedConfig {
[key: string]: EnforcedConfig | true;
}

export interface Env {
public: Record<string, string>;
private: Record<string, string>;
}
17 changes: 0 additions & 17 deletions packages/kit/src/exports/vite/utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import path from 'node:path';
import { loadEnv } from 'vite';
import { posixify } from '../../utils/filesystem.js';
import { negotiate } from '../../utils/http.js';
import { filter_env } from '../../utils/env.js';
import { escape_html } from '../../utils/escape.js';
import { dedent } from '../../core/sync/utils.js';
import {
Expand Down Expand Up @@ -61,21 +59,6 @@ function escape_for_regexp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
}

/**
* Load environment variables from process.env and .env files
* @param {import('types').ValidatedKitConfig['env']} env_config
* @param {string} mode
*/
export function get_env(env_config, mode) {
const { publicPrefix: public_prefix, privatePrefix: private_prefix } = env_config;
const env = loadEnv(mode, env_config.dir, '');

return {
public: filter_env(env, public_prefix, private_prefix),
private: filter_env(env, private_prefix, public_prefix)
};
}

Comment on lines -64 to -78
Copy link
Member Author

@teemingc teemingc Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be moved to a separate module to avoid type errors when users import $env/dynamic/* and they type check their code. We're currently using it in the generated module to avoid serialising users' dynamic env vars

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
Expand Down
22 changes: 22 additions & 0 deletions packages/kit/test/apps/basics/test/cross-platform/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,25 @@ test.describe('Static files', () => {
expect(response.status()).toBe(404);
});
});

test.describe('$env access outside the Vite pipeline', () => {
test('$env/static/public', async () => {
const env = await import('$env/static/public');
expect(env.PUBLIC_DYNAMIC).toBe('accessible anywhere/evaluated at run time');
});

test('$env/static/private', async () => {
const env = await import('$env/static/private');
expect(env.PRIVATE_STATIC).toBe('accessible to server-side code/replaced at build time');
});

test('$env/dynamic/public', async () => {
const { env } = await import('$env/dynamic/public');
expect(env.PUBLIC_DYNAMIC).toBe('accessible anywhere/evaluated at run time');
});

test('$env/dynamic/private', async () => {
const { env } = await import('$env/dynamic/private');
expect(env.PRIVATE_DYNAMIC).toBe('accessible to server-side code/evaluated at run time');
});
});
Comment on lines +17 to +37
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also change these to Playwright component tests, which is what the majority of users are facing issues with.

Loading