UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

443 lines (393 loc) • 12.8 kB
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */ /** @import { EnvVarConfig } from '@sveltejs/kit/env' */ /** @import { ValidatedConfig } from 'types' */ import path from 'node:path'; import * as devalue from 'devalue'; import { dedent } from './sync/utils.js'; import { get_global_name, runtime_directory } from './utils.js'; import * as e from '../messages/build-errors.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'; import { posixify } from '../utils/os.js'; /** * @typedef {'public' | 'private'} EnvType */ /** * @param {ValidatedConfig} config * @param {string} root * @returns {string | null} */ export function resolve_env_entry(config, root) { const entry = resolve_entry(path.resolve(root, config.files.src, 'env'), config.moduleExtensions); // posix, like the paths Vite hands to `hotUpdate` return entry && posixify(entry); } /** * @param {ValidatedConfig} kit * @param {string | null} file * @param {string} root * @param {string} mode * @returns {Promise<{ variables: Record<string, EnvVarConfig<any>> | null, deps: Set<string> }>} */ export async function load_explicit_env(kit, file, root, mode) { /** @type {Set<string>} */ const deps = new Set(); /** @type {Map<EnvType, string>} */ const env_importers = new Map(); if (!file) { return { variables: null, deps }; } /** @type {typeof import('vite')} */ const vite = await import_peer('vite', root); const server = await vite.createServer({ configFile: false, logLevel: 'silent', mode, define: { // these are needed by $app/env __SVELTEKIT_APP_VERSION__: JSON.stringify(kit.version.name), __SVELTEKIT_DEV__: mode === 'development', __SVELTEKIT_PAYLOAD__: 'undefined' // coming in through static import in env/internal.js but will end up unused }, resolve: { alias: [ { find: '$app/env', replacement: `${runtime_directory}/app/env` }, ...get_config_aliases(kit, root) ] }, plugins: [ { name: 'dependency-scanner', enforce: 'pre', resolveId(id, importer) { const prefixes = ['$app/env/', `${runtime_directory}/app/env/`]; const prefix = prefixes.find((prefix) => id.startsWith(prefix)); const type = prefix && id.slice(prefix.length); if (importer && (type === 'private' || type === 'public')) { env_importers.set(type, importer); } }, load(id) { deps.add(id); } } ] }); /** @type {Record<string, EnvVarConfig<any>>} */ let variables; const runner = get_runner(vite, server); /** @type {typeof import('../runtime/app/env/server.js')} */ ( await runner.import(`${runtime_directory}/app/env/server.js`) ).set_building(); try { ({ variables } = await runner.import(file)); if (!variables || typeof variables !== 'object') { e.env_variables_missing({ file }); } // validate for (const name of Object.keys(variables)) { if (!valid_identifier.test(name) || reserved.has(name)) { e.env_invalid_variable_name({ name: JSON.stringify(name) }); } } } catch (err) { const error = /** @type {any} */ (err || {}); if (error.code === 'ERR_MODULE_NOT_FOUND') { const match = error.message?.match( /<sveltekit:generated>\/env\/(private|public)\/server\.js/ ); if (match) { const type = /** @type {EnvType} */ (match[1]); const importer = env_importers.get(type); // the stack trace would only point into Vite's module runner e.env_circular_import( { type, importer: importer && posixify(path.relative(root, importer)) }, { stackless: true } ); } if (error.message?.includes(`Cannot find module '$app`)) { e.env_app_import(undefined, { cause: err }); } } throw error; } finally { await server.close(); } return { variables, deps }; } /** * Creates the `<sveltekit:generated>/env/*` modules, keyed by path relative to `dir`. Every module * derives from one pass over `variables`, so an inlined value is validated once per build. * @param {ValidatedConfig} config * @param {Record<string, EnvVarConfig<any>> | null} variables * @param {Record<string, string>} env * @param {string} dir * @param {string | null} entry * @param {boolean} is_dev * @returns {Record<string, string>} */ export function create_env_modules(config, variables, env, dir, entry, is_dev) { /** @type {Record<string, StandardSchemaV1.Issue[]>} */ const issues = {}; /** @type {Record<string, string>} */ const dev_env = {}; /** @type {string[]} */ const declarations = []; /** @type {string[]} */ const setters = []; /** @type {string[]} */ const public_exports = []; /** @type {string[]} */ const private_exports = []; /** @type {string[]} */ const sw_properties = []; let sw_dynamic = false; for (const [name, { public: is_public, static: is_static }] of Object.entries(variables ?? {})) { if (is_dev && name in env) dev_env[name] = env[name]; const exports = is_public ? public_exports : private_exports; if (is_static) { const value = devalue.uneval(validate(variables ?? {}, env[name], name, issues)); exports.push(`export const ${name} = ${value};\n`); if (is_public) { declarations.push(`explicit_public_env.${name} = ${value};`); sw_properties.push(`${name}: ${value}`); } } else { exports.push(`export const ${name} = env.${name};\n`); setters.push( `const ${name} = validate(variables, env.${name}, ${JSON.stringify(name)}, issues);` ); if (is_public) { setters.push(`explicit_public_env.${name} = ${name};`); setters.push(`rendered_env.${name} = ${name};`); sw_dynamic = true; // in dev there is no prerendered env module, so the service worker inlines the value if (is_dev) { const value = devalue.uneval(validate(variables ?? {}, env[name], name, issues)); sw_properties.push(`${name}: ${value}`); } } else { setters.push(`dynamic_private_env.${name} = ${name};`); } } } handle_issues(issues); const config_blocks = [ entry ? [ `import { variables } from ${JSON.stringify(entry)};`, `import { validate, handle_issues } from '@sveltejs/kit/internal/env';` ].join('\n') : [`const variables = {};`, `const handle_issues = () => {};`].join('\n'), `const issues = {};`, 'export { variables }', 'export const dynamic_private_env = {};', '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); }` ]; if (is_dev) { // 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. config_blocks.push(`set_env(${devalue.uneval(dev_env)});`); } /** * @param {string} prelude * @param {string[]} exports */ const module = (prelude, exports) => (variables ? `${prelude}\n\n${exports.join('')}` : ''); const global = `globalThis.${get_global_name(config.version.name, is_dev)}`; const version = JSON.stringify(config.version.name); // a production build with dynamic public env vars loads them at runtime via an import of // the prerendered `env.js`; otherwise the values are inlined const service_worker = !is_dev && sw_dynamic ? dedent` import { env } from '${config.paths.base}/${config.appDir}/env.js'; ${global} = { base: location.pathname.split('/').slice(0, -1).join('/'), env, version: ${version} }; ` : dedent` ${global} = { base: location.pathname.split('/').slice(0, -1).join('/'), env: { ${sw_properties.join(',\n\t\t') || '// empty'} }, version: ${version} }; `; return { 'config.js': config_blocks.join('\n\n'), 'public/server.js': module( `import { rendered_env as env } from '../config.js';`, public_exports ), 'private/server.js': module( `import { dynamic_private_env as env } from '../config.js';`, private_exports ), 'public/client.js': module( is_dev ? `const { env } = ${global};` : `import { payload } from ${JSON.stringify(posixify(path.relative(`${dir}/public`, `${runtime_directory}/client/payload.js`)))};\nconst env = payload.env;`, public_exports ), 'service-worker.js': service_worker }; } /** @param {string} description */ function create_jsdoc(description) { return `/**\n${description .split('\n') .map((line) => ` * ${line.replaceAll('*/', '*\\/')}`) .join('\n')}\n */`; } /** * @param {Record<string, EnvVarConfig<any>>} 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<typeof import('${relative}').variables.${name}.schema>` : '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', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof' ]); export const valid_identifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; /** * Generates `export const` declarations (and, for reserved-word names that need * aliasing, `const` + re-export specifiers) for a set of named exports. * * For regular names, emits a single efficient `export const name = expr;` statement. * For reserved-word names (e.g. `delete`, `class`), emits `const alias = expr;` plus * a re-export specifier (`alias as name`), since reserved words can't be `const` * binding names but CAN appear in export specifiers. * * You can do evil things like `export { c as class }`. In order to import/re-export * these, you need to alias the binding, then un-alias it when re-exporting: * * const _0 = ...; // safe binding name * export { _0 as class }; // valid — `class` is allowed in export specifiers * * Aliases are chosen to avoid collisions with any of the supplied names. The * namespace binding (used to hold the imported module) is likewise chosen to * avoid collisions. * * @param {Iterable<string>} names — the export names * @param {(name: string, namespace: string) => string} build_expression — * called for each name to produce the right-hand side of the declaration; * receives the chosen namespace binding so it can reference the imported module * @param {string} namespace_prefix — the preferred binding name for the namespace * (suffixed with a number if it collides with any export name) * @returns {{ namespace: string, declarations: string[], reexports: string[] }} */ export function create_exported_declarations(names, build_expression, namespace_prefix) { /** @type {Set<string>} */ const set = new Set(names); let namespace = namespace_prefix; let namespace_index = 0; while (set.has(namespace)) { namespace = `${namespace_prefix}${namespace_index++}`; } let alias_index = 0; /** @type {Map<string, string>} */ const aliases = new Map(); for (const name of set) { if (!reserved.has(name)) continue; let alias = `_${alias_index++}`; while (set.has(alias)) { alias = `_${alias_index++}`; } aliases.set(name, alias); } /** @type {string[]} */ const declarations = []; /** @type {string[]} */ const reexports = []; for (const name of set) { const alias = aliases.get(name); const expr = build_expression(name, namespace); if (alias) { declarations.push(`const ${alias} = ${expr};`); reexports.push(`${alias} as ${name}`); } else { declarations.push(`export const ${name} = ${expr};`); } } return { namespace, declarations, reexports }; }