diff --git a/.changeset/warm-boats-taste.md b/.changeset/warm-boats-taste.md new file mode 100644 index 000000000000..b381aecec82d --- /dev/null +++ b/.changeset/warm-boats-taste.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: emit env modules to disk + \ No newline at end of file diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js index d5aa39564a92..d3eae6b4fd01 100755 --- a/packages/kit/src/cli.js +++ b/packages/kit/src/cli.js @@ -2,9 +2,9 @@ import fs from 'node:fs'; import process from 'node:process'; import { parseArgs, styleText } from 'node:util'; import { extract_svelte_config, load_vite_config } from './core/config/index.js'; -import { resolve_explicit_env_entry } from './core/env.js'; import { coalesce_to_error } from './utils/error.js'; import { import_peer } from './utils/import.js'; +import { resolve_env_entry } from './core/env.js'; /** @param {unknown} e */ function handle_error(e) { @@ -105,8 +105,8 @@ if (command === 'sync') { const sync = await import('./core/sync/sync.js'); sync.all_types(sveltekit_config, vite_config.root); - const explicit_env_entry = resolve_explicit_env_entry(sveltekit_config); - await sync.env(vite, sveltekit_config, explicit_env_entry, vite_config.root, values.mode); + const entry = resolve_env_entry(sveltekit_config, vite_config.root); + await sync.env(sveltekit_config, entry, vite_config.root, values.mode); } catch (error) { handle_error(error); } finally { diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 372a1aeb525d..c70f50502bb7 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -3,13 +3,13 @@ /** @import { ValidatedConfig } from 'types' */ import path from 'node:path'; import * as devalue from 'devalue'; -import { GENERATED_COMMENT } from '../constants.js'; import { dedent } from './sync/utils.js'; import { 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'; import { get_runner } from '../runner.js'; +import { import_peer } from '../utils/import.js'; /** * @typedef {'public' | 'private'} EnvType @@ -17,22 +17,30 @@ import { get_runner } from '../runner.js'; /** * @param {ValidatedConfig} config + * @param {string} root * @returns {string | null} */ -export function resolve_explicit_env_entry(config) { - return resolve_entry(path.join(config.files.src, 'env')) ?? null; +export function resolve_env_entry(config, root) { + return resolve_entry(path.resolve(root, config.files.src, 'env')); } /** - * @param {typeof import('vite')} vite * @param {ValidatedConfig} kit * @param {string | null} file * @param {string} root * @param {string} mode - * @returns {Promise> | null>} + * @returns {Promise<{ variables: Record> | null, deps: Set }>} */ -export async function load_explicit_env(vite, kit, file, root, mode) { - if (!file) return null; +export async function load_explicit_env(kit, file, root, mode) { + /** @type {Set} */ + const deps = new Set(); + + if (!file) { + return { variables: null, deps }; + } + + /** @type {typeof import('vite')} */ + const vite = await import_peer('vite', root); const server = await vite.createServer({ configFile: false, @@ -49,7 +57,15 @@ export async function load_explicit_env(vite, kit, file, root, mode) { { find: '$app/env', replacement: `${runtime_directory}/app/env` }, ...get_config_aliases(kit, root) ] - } + }, + plugins: [ + { + name: 'dependency-scanner', + load(id) { + deps.add(id); + } + } + ] }); /** @type {Record>} */ @@ -92,7 +108,7 @@ export async function load_explicit_env(vite, kit, file, root, mode) { await server.close(); } - return variables; + return { variables, deps }; } /** @@ -100,9 +116,8 @@ export async function load_explicit_env(vite, kit, file, root, mode) { * @param {Record | undefined> | null} variables * @param {Record} env * @param {string | null} entry - * @param {boolean} is_dev */ -export function create_sveltekit_env(variables, env, entry, is_dev) { +export function create_sveltekit_env(variables, env, entry) { const imports = entry ? [ `import { variables } from ${JSON.stringify(entry)};`, @@ -139,7 +154,6 @@ export function create_sveltekit_env(variables, env, entry, is_dev) { handle_issues(issues); const blocks = [ - GENERATED_COMMENT, imports.join('\n'), `const issues = {};`, 'export { variables }', @@ -156,22 +170,29 @@ export function create_sveltekit_env(variables, env, entry, is_dev) { }` ]; + return blocks.join('\n\n'); +} + +/** + * @param {Record | undefined> | null} variables + * @param {Record} env + */ +export function create_sveltekit_env_dev(variables, env) { // In dev, initialise the env immediately. Tools like `vite-node` load modules // through the Vite config but don't run the SvelteKit dev server, which is what // normally calls `set_env`. Without this, dynamic env vars imported from // `$app/env/public` and `$app/env/private` would be `undefined` in such contexts. - if (is_dev) { - /** @type {Record} */ - const dev_env = {}; - for (const name of Object.keys(variables ?? {})) { - if (name in env) dev_env[name] = env[name]; - } - blocks.push(`set_env(${devalue.uneval(dev_env)});`); + /** @type {Record} */ + const dev_env = {}; + for (const name of Object.keys(variables ?? {})) { + if (name in env) dev_env[name] = env[name]; } - const module = blocks.join('\n\n'); - - return module; + return [ + `import { set_env } from './config.js';`, + `set_env(${devalue.uneval(dev_env)});`, + `export * from './config.js';` + ].join('\n\n'); } /** diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index 50d3e53a4101..2a089e2a0e76 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -102,16 +102,15 @@ export function all_types(config, root) { /** * Generate modules and types for explicit env vars - * @param {typeof import('vite')} vite * @param {import('types').ValidatedConfig} kit * @param {string | null} entry * @param {string} root The Vite root * @param {string} mode The Vite mode */ -export async function env(vite, kit, entry, root, mode) { - const env_config = await load_explicit_env(vite, kit, entry, root, mode); +export async function env(kit, entry, root, mode) { + const env_config = await load_explicit_env(kit, entry, root, mode); - write_env(entry, env_config, root); + write_env(entry, env_config.variables, root); return env_config; } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 8cd15c9e170d..47a067e51548 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -23,15 +23,7 @@ import MagicString from 'magic-string'; import { copy, read, resolve_entry } from '../../utils/filesystem.js'; import { posixify } from '../../utils/os.js'; import { to_fs } from '../../utils/vite.js'; -import { - create_sveltekit_env, - create_sveltekit_env_public, - resolve_explicit_env_entry, - create_sveltekit_env_service_worker, - create_sveltekit_env_service_worker_dev, - create_sveltekit_env_private, - create_exported_declarations -} from '../../core/env.js'; +import { create_exported_declarations } from '../../core/env.js'; import * as sync from '../../core/sync/sync.js'; import { load_and_validate_params } from '../../utils/params.js'; import { runtime_directory, logger } from '../../core/utils.js'; @@ -63,22 +55,14 @@ import { is_page_route } from '../../core/sync/create_manifest_data/index.js'; import { get_import_aliases, get_hash_import_keys } from '../../utils/imports.js'; -import { - app_env_private, - app_server, - sveltekit_env, - sveltekit_env_private, - sveltekit_env_service_worker, - sveltekit_manifest_data, - sveltekit_env_public_client, - sveltekit_env_public_server -} from './module_ids.js'; +import { app_env_private, app_server, sveltekit_manifest_data } from './module_ids.js'; import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; import { should_ignore, has_children } from './static_analysis/utils.js'; import { process_config, split_config, validate_config } from '../../core/config/index.js'; import { treeshake_prerendered_remotes } from './build/remote.js'; import { get_runner } from '../../runner.js'; +import { plugin_env_vars } from './plugins/env-vars.js'; /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { @@ -653,9 +637,6 @@ function kit({ svelte_config }) { } }; - /** @type {string | null} */ - let explicit_env_entry = null; - /** @type {Record> | null} */ let explicit_env_config = null; @@ -663,38 +644,6 @@ function kit({ svelte_config }) { const plugin_virtual_modules = { name: 'vite-plugin-sveltekit-virtual-modules', - async configResolved(config) { - explicit_env_entry = resolve_explicit_env_entry(kit); - explicit_env_config = await sync.env(vite, kit, explicit_env_entry, config.root, config.mode); - }, - - configureServer(server) { - server.watcher.on('all', async (_, file) => { - if (!file.includes('env')) { - return; - } - - const resolved = resolve_explicit_env_entry(kit); - - if (file === explicit_env_entry || file === resolved) { - explicit_env_entry = resolved; - explicit_env_config = await sync.env( - vite, - kit, - explicit_env_entry, - vite_config.root, - vite_config.mode - ); - - for (const id of [sveltekit_env, sveltekit_env_public_client]) { - invalidate_module(server, id); - } - - server.hot.send({ type: 'full-reload' }); - } - }); - }, - applyToEnvironment(environment) { return environment.name !== 'serviceWorker'; }, @@ -718,56 +667,12 @@ function kit({ svelte_config }) { load: { filter: { - id: [ - exactRegex(sveltekit_env), - exactRegex(sveltekit_env_private), - exactRegex(sveltekit_env_public_client), - exactRegex(sveltekit_env_public_server), - exactRegex(sveltekit_env_service_worker), - exactRegex(sveltekit_manifest_data) - ] + id: [exactRegex(sveltekit_manifest_data)] }, handler(id) { switch (id) { case sveltekit_manifest_data: return create_manifest_data_module(is_build, manifest_data); - - case sveltekit_env: - return create_sveltekit_env(explicit_env_config, env, explicit_env_entry, !is_build); - - case sveltekit_env_public_client: - return create_sveltekit_env_public( - explicit_env_config, - env, - `import { payload } from ${s(`${runtime_directory}/client/payload.js`)};\nconst env = payload.env;` - ); - - case sveltekit_env_public_server: - return create_sveltekit_env_public( - explicit_env_config, - env, - `import { rendered_env as env } from '__sveltekit/env';` - ); - - case sveltekit_env_private: - return create_sveltekit_env_private(explicit_env_config, env); - - case sveltekit_env_service_worker: - return is_build - ? create_sveltekit_env_service_worker( - explicit_env_config, - env, - kit.version.name, - kit_global, - kit.paths.base, - kit.appDir - ) - : create_sveltekit_env_service_worker_dev( - explicit_env_config, - env, - kit.version.name, - kit_global - ); } } } @@ -1198,7 +1103,7 @@ function kit({ svelte_config }) { resolveId: { filter: { - id: prefixRegex('__sveltekit/') + id: exactRegex('__sveltekit/manifest-data') }, handler(id) { return `\0virtual:${id}`; @@ -1207,12 +1112,7 @@ function kit({ svelte_config }) { load: { filter: { - id: [ - exactRegex('\0virtual:app/manifest'), - exactRegex(sveltekit_manifest_data), - exactRegex(sveltekit_env_service_worker), - exactRegex(sveltekit_env_public_client) - ] + id: [exactRegex('\0virtual:app/manifest'), exactRegex(sveltekit_manifest_data)] }, handler(id) { if (!manifest_data_code) { @@ -1246,32 +1146,6 @@ function kit({ svelte_config }) { if (id === sveltekit_manifest_data) { return manifest_data_code; } - - if (id === sveltekit_env_service_worker) { - return is_build - ? create_sveltekit_env_service_worker( - explicit_env_config, - env, - kit.version.name, - kit_global, - kit.paths.base, - kit.appDir - ) - : create_sveltekit_env_service_worker_dev( - explicit_env_config, - env, - kit.version.name, - kit_global - ); - } - - if (id === sveltekit_env_public_client) { - return create_sveltekit_env_public( - explicit_env_config, - env, - `const env = ${kit_global}.env;` - ); - } } }, @@ -1889,7 +1763,8 @@ function kit({ svelte_config }) { const uses_env_dynamic_public = has_explicit_dynamic_public_env && client_chunks.some( - (chunk) => chunk.type === 'chunk' && chunk.modules[sveltekit_env_public_client] + (chunk) => + chunk.type === 'chunk' && chunk.modules[`${out_dir}/generated/env/public/client.js`] ); if (kit.output.bundleStrategy === 'split') { @@ -2173,15 +2048,6 @@ function kit({ svelte_config }) { fs.mkdirSync(out, { recursive: true }); - explicit_env_entry = resolve_explicit_env_entry(kit); - explicit_env_config = await sync.env( - vite, - kit, - explicit_env_entry, - vite_config.root, - vite_config.mode - ); - await load_and_validate_params({ routes: manifest_data.routes, params_path: manifest_data.params, @@ -2244,6 +2110,9 @@ function kit({ svelte_config }) { plugin_setup, plugin_remote_guard, plugin_remote, + plugin_env_vars(svelte_config, (vars) => { + explicit_env_config = vars; + }), plugin_virtual_modules, process.env.TEST !== 'true' ? plugin_guard : undefined, plugin_service_worker, diff --git a/packages/kit/src/exports/vite/module_ids.js b/packages/kit/src/exports/vite/module_ids.js index 065df4df902e..a1a07875e4c9 100644 --- a/packages/kit/src/exports/vite/module_ids.js +++ b/packages/kit/src/exports/vite/module_ids.js @@ -1,11 +1,6 @@ import { fileURLToPath } from 'node:url'; import { posixify } from '../../utils/os.js'; -export const sveltekit_env = '\0virtual:__sveltekit/env'; -export const sveltekit_env_public_client = '\0virtual:__sveltekit/env/public/client'; -export const sveltekit_env_public_server = '\0virtual:__sveltekit/env/public/server'; -export const sveltekit_env_private = '\0virtual:__sveltekit/env/private'; -export const sveltekit_env_service_worker = '\0virtual:__sveltekit/env/service-worker'; export const sveltekit_server = '\0virtual:__sveltekit/server'; export const service_worker = '\0virtual:service-worker'; diff --git a/packages/kit/src/exports/vite/plugins/env-vars.js b/packages/kit/src/exports/vite/plugins/env-vars.js new file mode 100644 index 000000000000..45bbd6cfcb77 --- /dev/null +++ b/packages/kit/src/exports/vite/plugins/env-vars.js @@ -0,0 +1,215 @@ +/** @import { Plugin, ResolvedConfig } from 'vite' */ +/** @import { EnvVarConfig } from '@sveltejs/kit/env' */ +/** @import { ValidatedConfig } from 'types' */ +import path from 'node:path'; +import * as sync from '../../../core/sync/sync.js'; +import { + create_sveltekit_env, + create_sveltekit_env_dev, + create_sveltekit_env_private, + create_sveltekit_env_public, + create_sveltekit_env_service_worker, + create_sveltekit_env_service_worker_dev, + resolve_env_entry +} from '../../../core/env.js'; +import { import_peer } from '../../../utils/import.js'; +import { runtime_directory } from '../../../core/utils.js'; +import { s } from '../../../utils/misc.js'; +import { write_if_changed } from '../../../core/sync/utils.js'; +import { hash } from '../../../utils/hash.js'; +import { posixify } from '../../../utils/os.js'; +import { prefixRegex } from '@rolldown/pluginutils'; + +/** + * Generate (and, in dev, maintain) a `${outDir}/generated/env/config.js` module + * derived from `src/env.ts` + * + * @param {ValidatedConfig} config + * @param {(variables: Record> | null) => void} callback + * @returns {Plugin} + */ +export function plugin_env_vars(config, callback) { + // grab these values eagerly because they get mutated (TODO stop mutating them) + const dir = config.env.dir; + const out = config.outDir; + + const version_hash = hash(config.version.name); + + /** @type {string} */ + let out_dir; + + /** @type {Record} */ + let env; + + /** @type {ResolvedConfig} */ + let resolved_config; + + /** @type {string | null} */ + let resolved_entry = null; + + /** @type {Set} */ + let deps = new Set(); + + let is_build = false; + + /** @type {Promise | undefined} */ + let generated; + + async function generate() { + const synced = await sync.env( + config, + resolved_entry, + resolved_config.root, + resolved_config.mode + ); + + deps = synced.deps; + + const vars = synced.variables; + const dir = `${out_dir}/generated/env`; + + write_if_changed( + `${dir}/config.js`, + create_sveltekit_env( + vars, + env, + resolved_entry && posixify(path.relative(dir, resolved_entry)) + ) + ); + + write_if_changed(`${dir}/config-dev.js`, create_sveltekit_env_dev(vars, env)); + + write_if_changed( + `${dir}/public/client.js`, + create_sveltekit_env_public( + vars, + env, + `import { payload } from ${s(posixify(path.relative(`${dir}/public`, `${runtime_directory}/client/payload.js`)))};\nconst env = payload.env;` + ) + ); + + write_if_changed( + `${dir}/public/server.js`, + create_sveltekit_env_public( + vars, + env, + `import { rendered_env as env } from '__sveltekit/env';` + ) + ); + + write_if_changed( + `${dir}/public/service-worker.js`, + create_sveltekit_env_public( + vars, + env, + `const env = globalThis.__sveltekit_${version_hash}.env;` + ) + ); + + write_if_changed(`${dir}/private/server.js`, create_sveltekit_env_private(vars, env)); + + write_if_changed( + `${dir}/service-worker-prod.js`, + create_sveltekit_env_service_worker( + vars, + env, + config.version.name, + `globalThis.__sveltekit_${version_hash}`, + config.paths.base, + config.appDir + ) + ); + + write_if_changed( + `${dir}/service-worker-dev.js`, + create_sveltekit_env_service_worker_dev( + vars, + env, + config.version.name, + 'globalThis.__sveltekit_dev' + ) + ); + + callback(vars); + } + + return { + name: 'vite-plugin-sveltekit-env-vars', + + async configResolved(c) { + resolved_config = c; + + const vite = await import_peer('vite', c.root); + env = vite.loadEnv(c.mode, path.resolve(c.root, dir), ''); + + out_dir = posixify(path.resolve(c.root, out)); + + is_build = c.command === 'build'; + }, + + async buildStart() { + // runs once via the memo — per-process, whichever environment starts first + // (environment names vary by adapter), and never in the postbuild forks, + // which resolve the config without building + await (generated ??= (async () => { + resolved_entry = resolve_env_entry(config, resolved_config.root); + await generate(); + })()); + }, + + configureServer(server) { + // `handleHotUpdate` only fires for `change` events on files Vite already knows about, + // so it doesn't cover the env entry being created or deleted while the dev server is + // running. Watch for those events explicitly, re-resolve the entry, regenerate the + // modules and trigger a full reload (mirroring the previous behaviour). + const on_entry_add_unlink = async (/** @type {string} */ file) => { + const resolved = resolve_env_entry(config, resolved_config.root); + + if (file === resolved_entry || file === resolved) { + resolved_entry = resolved; + await generate(); + server.hot.send({ type: 'full-reload' }); + } + }; + + server.watcher.on('add', on_entry_add_unlink); + server.watcher.on('unlink', on_entry_add_unlink); + }, + + async handleHotUpdate(update) { + if (!deps.has(update.file)) return; + await generate(); + }, + + resolveId: { + filter: { + id: prefixRegex('__sveltekit/env') + }, + handler(id) { + const dir = `${out_dir}/generated/env`; + + if (id === '__sveltekit/env') { + return is_build ? `${dir}/config.js` : `${dir}/config-dev.js`; + } + + if (id === '__sveltekit/env/private') { + return `${dir}/private/server.js`; + } + + if (id === '__sveltekit/env/public/server') { + return `${dir}/public/server.js`; + } + + if (id === '__sveltekit/env/public/client') { + return this.environment.name === 'serviceWorker' + ? `${dir}/public/service-worker.js` + : `${dir}/public/client.js`; + } + + if (id === '__sveltekit/env/service-worker') { + return is_build ? `${dir}/service-worker-prod.js` : `${dir}/service-worker-dev.js`; + } + } + } + }; +} diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index a9ddf2279206..81a6960a2676 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -6,7 +6,7 @@ import { escape_html } from '../../utils/escape.js'; import { escape_for_regexp } from '../../utils/regex.js'; import { stackless } from '../../utils/error.js'; import { dedent } from '../../core/sync/utils.js'; -import { app_server, app_env_private, sveltekit_env_private } from './module_ids.js'; +import { app_server, app_env_private } from './module_ids.js'; /** * Transforms alias to a valid vite.resolve.alias array. @@ -119,7 +119,7 @@ export function normalize_id(id, aliases, cwd) { return '$app/server'; } - if (id === app_env_private || id === sveltekit_env_private) { + if (id === app_env_private) { return '$app/env/private'; }