diff --git a/.changeset/proud-socks-smash.md b/.changeset/proud-socks-smash.md new file mode 100644 index 000000000000..42b301bded91 --- /dev/null +++ b/.changeset/proud-socks-smash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: `$env/*` modules can now be imported from Playwright and other code running without Vite diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js index 9f56e2e17814..db2413e86b5a 100755 --- a/packages/kit/src/cli.js +++ b/packages/kit/src/cli.js @@ -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'] diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 20124e43e334..d956c3866fc5 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -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'; diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 455eb0f44b68..0638d25f8072 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -32,6 +32,7 @@ export function create_static_module(id, env) { /** * @param {EnvType} type * @param {Record | 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) { diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index c77bbca30f27..b6cffdc63dc2 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -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); } @@ -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); diff --git a/packages/kit/src/core/sync/write_ambient.js b/packages/kit/src/core/sync/write_ambient.js index 1f2188097ade..e02d3c21db6d 100644 --- a/packages/kit/src/core/sync/write_ambient.js +++ b/packages/kit/src/core/sync/write_ambient.js @@ -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'; 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..3cebbd354786 --- /dev/null +++ b/packages/kit/src/core/sync/write_env.js @@ -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 + ); +} diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js index 5866b3d8e783..c5a2be5ca39b 100644 --- a/packages/kit/src/core/sync/write_root.js +++ b/packages/kit/src/core/sync/write_root.js @@ -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 */ diff --git a/packages/kit/src/core/sync/write_tsconfig.js b/packages/kit/src/core/sync/write_tsconfig.js index 6bca8214bdb1..e5727257acbb 100644 --- a/packages/kit/src/core/sync/write_tsconfig.js +++ b/packages/kit/src/core/sync/write_tsconfig.js @@ -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 @@ -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'], diff --git a/packages/kit/src/core/sync/write_tsconfig.spec.js b/packages/kit/src/core/sync/write_tsconfig.spec.js index fbd780d74bdd..22d700615306 100644 --- a/packages/kit/src/core/sync/write_tsconfig.spec.js +++ b/packages/kit/src/core/sync/write_tsconfig.spec.js @@ -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'], diff --git a/packages/kit/src/exports/vite/build/build_service_worker.js b/packages/kit/src/exports/vite/build/build_service_worker.js index a90c62dd1a4a..6f5a5fa84f18 100644 --- a/packages/kit/src/exports/vite/build/build_service_worker.js +++ b/packages/kit/src/exports/vite/build/build_service_worker.js @@ -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'; diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 31c13c1ead8b..2dc5a373995c 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -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 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(); @@ -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; diff --git a/packages/kit/src/exports/vite/env.js b/packages/kit/src/exports/vite/env.js new file mode 100644 index 000000000000..6c359d06e478 --- /dev/null +++ b/packages/kit/src/exports/vite/env.js @@ -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) + }; +} diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 445001139726..7f3145897a80 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -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'; @@ -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'; @@ -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()); @@ -209,7 +204,7 @@ async function kit({ svelte_config }) { /** @type {boolean} */ let is_build; - /** @type {{ public: Record; private: Record }} */ + /** @type {import('./types.js').Env} */ let env; /** @type {() => Promise} */ @@ -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'; @@ -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( @@ -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} */ const entrypoints = new Set(); @@ -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); }, /** diff --git a/packages/kit/src/exports/vite/types.d.ts b/packages/kit/src/exports/vite/types.d.ts index 900aa43541ea..d72c90ce8cbe 100644 --- a/packages/kit/src/exports/vite/types.d.ts +++ b/packages/kit/src/exports/vite/types.d.ts @@ -1,3 +1,8 @@ export interface EnforcedConfig { [key: string]: EnforcedConfig | true; } + +export interface Env { + public: Record; + private: Record; +} diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index 77743bda77c9..0b4ec5249e93 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -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 { @@ -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) - }; -} - /** * @param {import('http').IncomingMessage} req * @param {import('http').ServerResponse} res diff --git a/packages/kit/test/apps/basics/test/cross-platform/server.test.js b/packages/kit/test/apps/basics/test/cross-platform/server.test.js index 678a07d575eb..e96d8118927c 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/server.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/server.test.js @@ -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'); + }); +});