diff --git a/.changeset/calm-pugs-applaud.md b/.changeset/calm-pugs-applaud.md new file mode 100644 index 000000000000..556047ed1e63 --- /dev/null +++ b/.changeset/calm-pugs-applaud.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: prevent use of dynamic env vars during prerendering, serve env vars dynamically diff --git a/.changeset/pink-lobsters-protect.md b/.changeset/pink-lobsters-protect.md new file mode 100644 index 000000000000..2a92abb5b038 --- /dev/null +++ b/.changeset/pink-lobsters-protect.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-static': major +--- + +breaking: update SvelteKit peer dependency to version 2 diff --git a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md index 0f8babf08977..5bfeafa2f763 100644 --- a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md +++ b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md @@ -105,6 +105,14 @@ As such, SvelteKit 2 replaces `resolvePath` with a (slightly better named) funct `svelte-migrate` will do the method replacement for you, though if you later prepend the result with `base`, you need to remove that yourself. +## Dynamic environment variables cannot be used during prerendering + +The `$env/dynamic/public` and `$env/dynamic/private` modules provide access to _run time_ environment variables, as opposed to the _build time_ environment variables exposed by `$env/static/public` and `$env/static/private`. + +During prerendering in SvelteKit 1, they are one and the same. As such, prerendered pages that make use of 'dynamic' environment variables are really 'baking in' build time values, which is incorrect. Worse, `$env/dynamic/public` is populated in the browser with these stale values if the user happens to land on a prerendered page before navigating to dynamically-rendered pages. + +Because of this, dynamic environment variables can no longer be read during prerendering in SvelteKit 2 — you should use the `static` modules instead. If the user lands on a prerendered page, SvelteKit will request up-to-date values for `$env/dynamic/public` from the server (by default from a module called `_env.js` — this can be configured with `config.kit.env.publicModule`) instead of reading them from the server-rendered HTML. + ## `form` and `data` have been removed from `use:enhance` callbacks If you provide a callback to [`use:enhance`](/docs/form-actions#progressive-enhancement-use-enhance), it will be called with an object containing various useful properties. @@ -117,6 +125,16 @@ If a form contains an `` but does not have an `enctype="multi ## Updated dependency requirements -SvelteKit requires Node `18.13` or higher, Vite `^5.0`, vite-plugin-svelte `^3.0`, TypeScript `^5.0` and Svelte version 4 or higher. `svelte-migrate` will do the `package.json` bumps for you. +SvelteKit 2 requires Node `18.13` or higher, and the following minimum dependency versions: + +- `svelte@4` +- `vite@5` +- `typescript@5` +- `@sveltejs/adapter-static@3` (if you're using it) +- `@sveltejs/vite-plugin-svelte@3` (this is now required as a `peerDependency` of SvelteKit — previously it was directly depended upon) + +`svelte-migrate` will update your `package.json` for you. As part of the TypeScript upgrade, the generated `tsconfig.json` (the one your `tsconfig.json` extends from) now uses `"moduleResolution": "bundler"` (which is recommended by the TypeScript team, as it properly resolves types from packages with an `exports` map in package.json) and `verbatimModuleSyntax` (which replaces the existing `importsNotUsedAsValues ` and `preserveValueImports` flags — if you have those in your `tsconfig.json`, remove them. `svelte-migrate` will do this for you). + +SvelteKit 2 uses ES2022 features, which are supported in all modern browsers. diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index 1f50923f1f83..32d7e5319df7 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -62,6 +62,7 @@ See https://kit.svelte.dev/docs/page-options#prerender for more details` builder.rimraf(assets); builder.rimraf(pages); + builder.generateEnvModule(); builder.writeClient(assets); builder.writePrerendered(pages); diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json index 370fdf3e4286..878e652767f9 100644 --- a/packages/adapter-static/package.json +++ b/packages/adapter-static/package.json @@ -40,6 +40,6 @@ "vite": "^5.0.8" }, "peerDependencies": { - "@sveltejs/kit": "^1.5.0 || ^2.0.0" + "@sveltejs/kit": "^2.0.0" } } diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte b/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte index 1c06b45b43e4..a36f01afaa54 100644 --- a/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte +++ b/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte @@ -1,5 +1,11 @@ -

The answer is {env.PUBLIC_ANSWER}

+

The answer is {PUBLIC_ANSWER}

+ +{#if browser} +

The dynamic answer is {env.PUBLIC_ANSWER}

+{/if} diff --git a/packages/adapter-static/test/apps/prerendered/test/test.js b/packages/adapter-static/test/apps/prerendered/test/test.js index 6a5ea0c2ee8b..2e187f8effd5 100644 --- a/packages/adapter-static/test/apps/prerendered/test/test.js +++ b/packages/adapter-static/test/apps/prerendered/test/test.js @@ -24,4 +24,5 @@ test('prerenders a referenced endpoint with implicit `prerender` setting', async test('exposes public env vars to the client', async ({ page }) => { await page.goto('/public-env'); expect(await page.textContent('h1')).toEqual('The answer is 42'); + expect(await page.textContent('h2')).toEqual('The dynamic answer is 42'); }); diff --git a/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte b/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte index 8950feed162a..55b4798e99c5 100644 --- a/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte +++ b/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte @@ -1,7 +1,7 @@

the fallback page was rendered

-{env.PUBLIC_VALUE} +{PUBLIC_VALUE} diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 11fd9cbc1fd6..2c55f5027485 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -156,6 +156,13 @@ export function create_builder({ write(dest, fallback); }, + generateEnvModule() { + 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)}`); + }, + generateManifest({ relativePath, routes: subset }) { return generate_manifest({ build_data, diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index bbbd3ca26644..9dcb0c432c82 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -43,8 +43,11 @@ async function analyse({ manifest_path, env }) { // set env, in case it's used in initialisation const { publicPrefix: public_prefix, privatePrefix: private_prefix } = config.env; - internal.set_private_env(filter_private_env(env, { public_prefix, private_prefix })); - internal.set_public_env(filter_public_env(env, { public_prefix, private_prefix })); + const private_env = filter_private_env(env, { public_prefix, private_prefix }); + const public_env = filter_public_env(env, { public_prefix, private_prefix }); + internal.set_private_env(private_env); + internal.set_public_env(public_env); + internal.set_safe_public_env(public_env); /** @type {import('types').ServerMetadata} */ const metadata = { diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index a064ae2c3c18..f6e3e2315f37 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -150,6 +150,7 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } const files = new Set(walk(`${out}/client`).map(posixify)); + files.add(`${config.appDir}/env.js`); const immutable = `${config.appDir}/immutable`; if (existsSync(`${out}/server/${immutable}`)) { @@ -473,10 +474,10 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } if (not_prerendered.length > 0) { + const list = not_prerendered.map((id) => ` - ${id}`).join('\n'); + throw new Error( - `The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${not_prerendered.map( - (id) => ` - ${id}` - )}\n\nSee https://kit.svelte.dev/docs/page-options#prerender-troubleshooting for info on how to solve this` + `The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${list}\n\nSee https://kit.svelte.dev/docs/page-options#prerender-troubleshooting for info on how to solve this` ); } diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index ae18efe2f4fe..8971f82aa871 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -28,9 +28,10 @@ const server_template = ({ import root from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; import { set_building } from '__sveltekit/environment'; import { set_assets } from '__sveltekit/paths'; -import { set_private_env, set_public_env } from '${runtime_directory}/shared-server.js'; +import { set_private_env, set_public_env, set_safe_public_env } from '${runtime_directory}/shared-server.js'; export const options = { + app_dir: ${s(config.kit.appDir)}, app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, csp: ${s(config.kit.csp)}, csrf_check_origin: ${s(config.kit.csrf.checkOrigin)}, @@ -62,7 +63,7 @@ export function get_hooks() { return ${hooks ? `import(${s(hooks)})` : '{}'}; } -export { set_assets, set_building, set_private_env, set_public_env }; +export { set_assets, set_building, set_private_env, set_public_env, set_safe_public_env }; `; // TODO need to re-run this whenever src/app.html or src/error.html are diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index f60171d86015..8d9f432220fa 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -101,6 +101,11 @@ export interface Builder { */ generateFallback(dest: string): Promise; + /** + * Generate a module exposing build-time environment variables as `$env/dynamic/public`. + */ + generateEnvModule(): void; + /** * Generate a server-side manifest to initialise the SvelteKit [server](https://kit.svelte.dev/docs/types#public-types-server) with. * @param opts a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated @@ -284,7 +289,9 @@ export interface KitConfig { */ alias?: Record; /** - * The directory relative to `paths.assets` where the built JS and CSS (and imported assets) are served from. (The filenames therein contain content-based hashes, meaning they can be cached indefinitely). Must not start or end with `/`. + * The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes. + * + * If `paths.assets` is specified, there will be two app directories — `${paths.assets}/${appDir}` and `${paths.base}/${appDir}`. * @default "_app" */ appDir?: string; diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 2a4293fff74b..82e662ec7a98 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -120,7 +120,8 @@ export async function dev(vite, vite_config, svelte_config) { app: `${to_fs(svelte_config.kit.outDir)}/generated/client/app.js`, imports: [], stylesheets: [], - fonts: [] + fonts: [], + uses_env_dynamic_public: true }, nodes: manifest_data.nodes.map((node, index) => { return async () => { diff --git a/packages/kit/src/exports/vite/graph_analysis/index.js b/packages/kit/src/exports/vite/graph_analysis/index.js index 6dc6860a0a3c..7ab7cd3cdfa2 100644 --- a/packages/kit/src/exports/vite/graph_analysis/index.js +++ b/packages/kit/src/exports/vite/graph_analysis/index.js @@ -1,11 +1,9 @@ import path from 'node:path'; import { posixify } from '../../../utils/filesystem.js'; import { strip_virtual_prefix } from '../utils.js'; +import { env_dynamic_private, env_static_private } from '../module_ids.js'; -const ILLEGAL_IMPORTS = new Set([ - '\0virtual:$env/dynamic/private', - '\0virtual:$env/static/private' -]); +const ILLEGAL_IMPORTS = new Set([env_dynamic_private, env_static_private]); const ILLEGAL_MODULE_NAME_PATTERN = /.*\.server\..+/; /** diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index acd5b88f2b09..c4227c0bbd25 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -27,6 +27,15 @@ import { s } from '../../utils/misc.js'; import { hash } from '../../runtime/hash.js'; import { dedent, isSvelte5Plus } from '../../core/sync/utils.js'; import sirv from 'sirv'; +import { + env_dynamic_private, + env_dynamic_public, + env_static_private, + env_static_public, + service_worker, + sveltekit_environment, + sveltekit_paths +} from './module_ids.js'; export { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; @@ -365,22 +374,22 @@ function kit({ svelte_config }) { } switch (id) { - case '\0virtual:$env/static/private': + case env_static_private: return create_static_module('$env/static/private', env.private); - case '\0virtual:$env/static/public': + case env_static_public: return create_static_module('$env/static/public', env.public); - case '\0virtual:$env/dynamic/private': + case env_dynamic_private: return create_dynamic_module( 'private', vite_config_env.command === 'serve' ? env.private : undefined ); - case '\0virtual:$env/dynamic/public': + case env_dynamic_public: // populate `$env/dynamic/public` from `window` if (browser) { - return `export const env = ${global}.env;`; + return `export const env = ${global}.env ?? (await import(/* @vite-ignore */ ${global}.base + '/' + '${kit.appDir}/env.js')).env;`; } return create_dynamic_module( @@ -388,12 +397,12 @@ function kit({ svelte_config }) { vite_config_env.command === 'serve' ? env.public : undefined ); - case '\0virtual:$service-worker': + case service_worker: return create_service_worker_module(svelte_config); // for internal use only. it's published as $app/paths externally // we use this alias so that we won't collide with user aliases - case '\0virtual:__sveltekit/paths': { + case sveltekit_paths: { const { assets, base } = svelte_config.kit.paths; // use the values defined in `global`, but fall back to hard-coded values @@ -431,7 +440,7 @@ function kit({ svelte_config }) { `; } - case '\0virtual:__sveltekit/environment': { + case sveltekit_environment: { const { version } = svelte_config.kit; return dedent` @@ -572,7 +581,7 @@ function kit({ svelte_config }) { preserveEntrySignatures: 'strict' }, ssrEmitAssets: true, - target: ssr ? 'node16.14' : undefined + target: ssr ? 'node18.13' : 'es2022' }, publicDir: kit.files.assets, worker: { @@ -765,7 +774,10 @@ function kit({ svelte_config }) { app: app.file, imports: [...start.imports, ...app.imports], stylesheets: [...start.stylesheets, ...app.stylesheets], - fonts: [...start.fonts, ...app.fonts] + fonts: [...start.fonts, ...app.fonts], + uses_env_dynamic_public: output.some( + (chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] + ) }; const css = output.filter( diff --git a/packages/kit/src/exports/vite/module_ids.js b/packages/kit/src/exports/vite/module_ids.js new file mode 100644 index 000000000000..a6097f5b6f44 --- /dev/null +++ b/packages/kit/src/exports/vite/module_ids.js @@ -0,0 +1,7 @@ +export const env_static_private = '\0virtual:$env/static/private'; +export const env_static_public = '\0virtual:$env/static/public'; +export const env_dynamic_private = '\0virtual:$env/dynamic/private'; +export const env_dynamic_public = '\0virtual:$env/dynamic/public'; +export const service_worker = '\0virtual:$service-worker'; +export const sveltekit_paths = '\0virtual:__sveltekit/paths'; +export const sveltekit_environment = '\0virtual:__sveltekit/environment'; diff --git a/packages/kit/src/runtime/server/env_module.js b/packages/kit/src/runtime/server/env_module.js new file mode 100644 index 000000000000..7d37ac6a1481 --- /dev/null +++ b/packages/kit/src/runtime/server/env_module.js @@ -0,0 +1,29 @@ +import { public_env } from '../shared-server.js'; + +/** @type {string} */ +let body; + +/** @type {string} */ +let etag; + +/** @type {Headers} */ +let headers; + +/** + * @param {Request} request + * @returns {Response} + */ +export function get_public_env(request) { + body ??= `export const env=${JSON.stringify(public_env)}`; + etag ??= `W/${Date.now()}`; + headers ??= new Headers({ + 'content-type': 'application/javascript; charset=utf-8', + etag + }); + + if (request.headers.get('if-none-match') === etag) { + return new Response(undefined, { status: 304, headers }); + } + + return new Response(body, { headers }); +} diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 607f161b2742..bf14575b3438 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,8 +1,18 @@ import { respond } from './respond.js'; -import { set_private_env, set_public_env } from '../shared-server.js'; +import { set_private_env, set_public_env, set_safe_public_env } from '../shared-server.js'; import { options, get_hooks } from '__SERVER__/internal.js'; import { DEV } from 'esm-env'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; +import { building } from '../app/environment.js'; + +/** @type {ProxyHandler<{ type: 'public' | 'private' }>} */ +const prerender_env_handler = { + get({ type }, prop) { + throw new Error( + `Cannot read values from $env/dynamic/${type} while prerendering (attempted to read env.${prop.toString()}). Use $env/static/${type} instead` + ); + } +}; export class Server { /** @type {import('types').SSROptions} */ @@ -27,19 +37,19 @@ export class Server { // Take care: Some adapters may have to call `Server.init` per-request to set env vars, // so anything that shouldn't be rerun should be wrapped in an `if` block to make sure it hasn't // been done already. + // set env, in case it's used in initialisation - set_private_env( - filter_private_env(env, { - public_prefix: this.#options.env_public_prefix, - private_prefix: this.#options.env_private_prefix - }) - ); - set_public_env( - filter_public_env(env, { - public_prefix: this.#options.env_public_prefix, - private_prefix: this.#options.env_private_prefix - }) - ); + const prefixes = { + public_prefix: this.#options.env_public_prefix, + private_prefix: this.#options.env_private_prefix + }; + + const private_env = filter_private_env(env, prefixes); + const public_env = filter_public_env(env, prefixes); + + set_private_env(building ? new Proxy({ type: 'private' }, prerender_env_handler) : private_env); + set_public_env(building ? new Proxy({ type: 'public' }, prerender_env_handler) : public_env); + set_safe_public_env(public_env); if (!this.#options.hooks) { try { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index c06c5cb8d47d..3b673648135f 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -8,7 +8,7 @@ import { s } from '../../../utils/misc.js'; import { Csp } from './csp.js'; import { uneval_action_response } from './actions.js'; import { clarify_devalue_error, stringify_uses, handle_error_and_jsonify } from '../utils.js'; -import { public_env } from '../../shared-server.js'; +import { public_env, safe_public_env } from '../../shared-server.js'; import { text } from '../../../exports/index.js'; import { create_async_iterator } from '../../../utils/streaming.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; @@ -276,6 +276,10 @@ export async function render_response({ } if (page_config.csr) { + if (client.uses_env_dynamic_public && state.prerendering) { + modulepreloads.add(`${options.app_dir}/env.js`); + } + const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter( (path) => resolve_opts.preload({ type: 'js', path }) ); @@ -295,7 +299,7 @@ export async function render_response({ const properties = [ paths.assets && `assets: ${s(paths.assets)}`, `base: ${base_expression}`, - `env: ${s(public_env)}` + `env: ${!client.uses_env_dynamic_public || state.prerendering ? null : s(public_env)}` ].filter(Boolean); if (chunks) { @@ -431,7 +435,7 @@ export async function render_response({ body, assets, nonce: /** @type {string} */ (csp.nonce), - env: public_env + env: safe_public_env }); // TODO flush chunks as early as we can diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 7637e5437fcc..f58a0d8fa103 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -30,6 +30,7 @@ import { get_option } from '../../utils/options.js'; import { json, text } from '../../exports/index.js'; import { action_json_redirect, is_action_json_request } from './page/actions.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; +import { get_public_env } from './env_module.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ @@ -98,6 +99,10 @@ export async function respond(request, options, manifest, state) { decoded = decoded.slice(base.length) || '/'; } + if (decoded === `/${options.app_dir}/env.js`) { + return get_public_env(request); + } + const is_data_request = has_data_suffix(decoded); /** @type {boolean[] | undefined} */ let invalidated_data_nodes; diff --git a/packages/kit/src/runtime/shared-server.js b/packages/kit/src/runtime/shared-server.js index 778e31aad481..7bd6bde4c234 100644 --- a/packages/kit/src/runtime/shared-server.js +++ b/packages/kit/src/runtime/shared-server.js @@ -1,9 +1,21 @@ -/** @type {Record} */ +/** + * `$env/dynamic/private` + * @type {Record} + */ export let private_env = {}; -/** @type {Record} */ +/** + * `$env/dynamic/public`. When prerendering, this will be a proxy that forbids reads + * @type {Record} + */ export let public_env = {}; +/** + * The same as `public_env`, but without the proxy. Use for `%sveltekit.env.PUBLIC_FOO%` + * @type {Record} + */ +export let safe_public_env = {}; + /** @param {any} error */ export let fix_stack_trace = (error) => error?.stack; @@ -17,6 +29,11 @@ export function set_public_env(environment) { public_env = environment; } +/** @type {(environment: Record) => void} */ +export function set_safe_public_env(environment) { + safe_public_env = environment; +} + /** @param {(error: Error) => string} value */ export function set_fix_stack_trace(value) { fix_stack_trace = value; diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 62c63f9ec291..2234eca3f4ef 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -31,6 +31,7 @@ export interface ServerInternalModule { set_assets(path: string): void; set_private_env(environment: Record): void; set_public_env(environment: Record): void; + set_safe_public_env(environment: Record): void; set_version(version: string): void; set_fix_stack_trace(fix_stack_trace: (error: unknown) => string): void; } @@ -59,6 +60,7 @@ export interface BuildData { imports: string[]; stylesheets: string[]; fonts: string[]; + uses_env_dynamic_public: boolean; } | null; server_manifest: import('vite').Manifest; } @@ -330,6 +332,7 @@ export interface SSRNode { export type SSRNodeLoader = () => Promise; export interface SSROptions { + app_dir: string; app_template_contains_nonce: boolean; csp: ValidatedConfig['kit']['csp']; csrf_check_origin: boolean; diff --git a/packages/kit/src/types/synthetic/$env+dynamic+private.md b/packages/kit/src/types/synthetic/$env+dynamic+private.md index 99fabd42485d..eed73d827bd3 100644 --- a/packages/kit/src/types/synthetic/$env+dynamic+private.md +++ b/packages/kit/src/types/synthetic/$env+dynamic+private.md @@ -2,6 +2,8 @@ This module provides access to runtime environment variables, as defined by the This module cannot be imported into client-side code. +Dynamic environment variables cannot be used during prerendering. + ```ts import { env } from '$env/dynamic/private'; console.log(env.DEPLOYMENT_SPECIFIC_VARIABLE); diff --git a/packages/kit/src/types/synthetic/$env+dynamic+public.md b/packages/kit/src/types/synthetic/$env+dynamic+public.md index b70626ba6417..ad1cf54d2fb6 100644 --- a/packages/kit/src/types/synthetic/$env+dynamic+public.md +++ b/packages/kit/src/types/synthetic/$env+dynamic+public.md @@ -2,6 +2,8 @@ Similar to [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dyn Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead. +Dynamic environment variables cannot be used during prerendering. + ```ts import { env } from '$env/dynamic/public'; console.log(env.PUBLIC_DEPLOYMENT_SPECIFIC_VARIABLE); diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json index 1c705def699a..4866372a0361 100644 --- a/packages/kit/test/apps/basics/package.json +++ b/packages/kit/test/apps/basics/package.json @@ -9,7 +9,7 @@ "check": "svelte-kit sync && tsc && svelte-check", "test": "node test/setup.js && pnpm test:dev && pnpm test:build", "test:dev": "node -e \"fs.rmSync('test/errors.json', { force: true })\" && cross-env DEV=true playwright test", - "test:build": "node -e \"fs.rmSync('test/errors.json', { force: true })\" && playwright test", + "test:build": "node -e \"fs.rmSync('test/errors.json', { force: true })\" && cross-env PUBLIC_PRERENDERING=false playwright test", "test:cross-platform:dev": "node test/setup.js && node -e \"fs.rmSync('test/errors.json', { force: true })\" && cross-env DEV=true playwright test test/cross-platform/", "test:cross-platform:build": "node test/setup.js && node -e \"fs.rmSync('test/errors.json', { force: true })\" && playwright test test/cross-platform/" }, diff --git a/packages/kit/test/apps/basics/playwright.config.js b/packages/kit/test/apps/basics/playwright.config.js index 33d36b651014..0f7c1458a513 100644 --- a/packages/kit/test/apps/basics/playwright.config.js +++ b/packages/kit/test/apps/basics/playwright.config.js @@ -1 +1,11 @@ -export { config as default } from '../../utils.js'; +import { config } from '../../utils.js'; + +export default { + ...config, + webServer: { + command: process.env.DEV + ? 'cross-env PUBLIC_PRERENDERING=false pnpm dev' + : 'cross-env PUBLIC_PRERENDERING=true pnpm build && pnpm preview', + port: process.env.DEV ? 5173 : 4173 + } +}; diff --git a/packages/kit/test/apps/basics/src/routes/+layout.server.js b/packages/kit/test/apps/basics/src/routes/+layout.server.js index 6df957a1aa99..50f99accae43 100644 --- a/packages/kit/test/apps/basics/src/routes/+layout.server.js +++ b/packages/kit/test/apps/basics/src/routes/+layout.server.js @@ -1,5 +1,4 @@ import { error, redirect } from '@sveltejs/kit'; -import { env } from '$env/dynamic/private'; import { SOME_JSON } from '$env/static/private'; // https://github.com/sveltejs/kit/issues/8646 @@ -7,10 +6,6 @@ if (JSON.parse(SOME_JSON).answer !== 42) { throw new Error('failed to parse env var'); } -if (JSON.parse(env.SOME_JSON).answer !== 42) { - throw new Error('failed to parse env var'); -} - /** @type {import('./$types').LayoutServerLoad} */ export async function load({ cookies, locals, fetch }) { if (locals.url?.pathname === '/non-existent-route') { diff --git a/packages/kit/test/apps/basics/src/routes/prerendering/env/+layout.svelte b/packages/kit/test/apps/basics/src/routes/prerendering/env/+layout.svelte new file mode 100644 index 000000000000..9fd745e3ba79 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/prerendering/env/+layout.svelte @@ -0,0 +1,4 @@ +prerendered +dynamic + + diff --git a/packages/kit/test/apps/basics/src/routes/prerendering/env/dynamic/+page.svelte b/packages/kit/test/apps/basics/src/routes/prerendering/env/dynamic/+page.svelte new file mode 100644 index 000000000000..c0db5dfb5fcb --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/prerendering/env/dynamic/+page.svelte @@ -0,0 +1,5 @@ + + +

prerendering: {env.PUBLIC_PRERENDERING}

diff --git a/packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.js b/packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.js new file mode 100644 index 000000000000..189f71e2e1b3 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.js @@ -0,0 +1 @@ +export const prerender = true; diff --git a/packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.svelte b/packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.svelte new file mode 100644 index 000000000000..34c62bbc7ead --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.svelte @@ -0,0 +1 @@ +

prerendered

diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index b140ca45ddc6..06a6561fd0b1 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -736,6 +736,15 @@ test.describe('env', () => { 'accessible anywhere/evaluated at run time' ); }); + + test('uses correct dynamic env when navigating from prerendered page', async ({ + page, + clicknav + }) => { + await page.goto('/prerendering/env/prerendered'); + await clicknav('[href="/prerendering/env/dynamic"]'); + expect(await page.locator('h2')).toHaveText('prerendering: false'); + }); }); test.describe('Snapshots', () => { diff --git a/packages/kit/test/prerendering/basics/src/routes/env/+page.server.js b/packages/kit/test/prerendering/basics/src/routes/env/+page.server.js index 617a8542fd21..d0f085387247 100644 --- a/packages/kit/test/prerendering/basics/src/routes/env/+page.server.js +++ b/packages/kit/test/prerendering/basics/src/routes/env/+page.server.js @@ -1,9 +1,7 @@ import { PRIVATE_STATIC } from '$env/static/private'; -import { env } from '$env/dynamic/private'; export function load() { return { - PRIVATE_STATIC, - PRIVATE_DYNAMIC: env.PRIVATE_DYNAMIC + PRIVATE_STATIC }; } diff --git a/packages/kit/test/prerendering/basics/src/routes/env/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/env/+page.svelte index 4ee2f424da67..bb13de1f8f55 100644 --- a/packages/kit/test/prerendering/basics/src/routes/env/+page.svelte +++ b/packages/kit/test/prerendering/basics/src/routes/env/+page.svelte @@ -1,13 +1,9 @@

PRIVATE_STATIC: {data.PRIVATE_STATIC}

-

PRIVATE_DYNAMIC: {data.PRIVATE_DYNAMIC}

-

PUBLIC_STATIC: {PUBLIC_STATIC}

-

PUBLIC_DYNAMIC: {env.PUBLIC_DYNAMIC}

diff --git a/packages/kit/test/prerendering/basics/test/tests.spec.js b/packages/kit/test/prerendering/basics/test/tests.spec.js index 5b5763d19e1d..ecf337c35a76 100644 --- a/packages/kit/test/prerendering/basics/test/tests.spec.js +++ b/packages/kit/test/prerendering/basics/test/tests.spec.js @@ -205,12 +205,8 @@ test('$env - includes environment variables', () => { content, /.*PRIVATE_STATIC: accessible to server-side code\/replaced at build time.*/gs ); - assert.match( - content, - /.*PRIVATE_DYNAMIC: accessible to server-side code\/evaluated at run time.*/gs - ); + assert.match(content, /.*PUBLIC_STATIC: accessible anywhere\/replaced at build time.*/gs); - assert.match(content, /.*PUBLIC_DYNAMIC: accessible anywhere\/evaluated at run time.*/gs); }); test('prerenders a page in a (group)', () => { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3210343dd793..79e925d0f4cc 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -83,6 +83,11 @@ declare module '@sveltejs/kit' { */ generateFallback(dest: string): Promise; + /** + * Generate a module exposing build-time environment variables as `$env/dynamic/public`. + */ + generateEnvModule(): void; + /** * Generate a server-side manifest to initialise the SvelteKit [server](https://kit.svelte.dev/docs/types#public-types-server) with. * @param opts a relative path to the base directory of the app and optionally in which format (esm or cjs) the manifest should be generated @@ -266,7 +271,9 @@ declare module '@sveltejs/kit' { */ alias?: Record; /** - * The directory relative to `paths.assets` where the built JS and CSS (and imported assets) are served from. (The filenames therein contain content-based hashes, meaning they can be cached indefinitely). Must not start or end with `/`. + * The directory where SvelteKit keeps its stuff, including static assets (such as JS and CSS) and internally-used routes. + * + * If `paths.assets` is specified, there will be two app directories — `${paths.assets}/${appDir}` and `${paths.base}/${appDir}`. * @default "_app" */ appDir?: string; @@ -1501,6 +1508,7 @@ declare module '@sveltejs/kit' { imports: string[]; stylesheets: string[]; fonts: string[]; + uses_env_dynamic_public: boolean; } | null; server_manifest: import('vite').Manifest; } diff --git a/packages/migrate/migrations/sveltekit-2/migrate.js b/packages/migrate/migrations/sveltekit-2/migrate.js index e956baa40601..0b5098da396b 100644 --- a/packages/migrate/migrations/sveltekit-2/migrate.js +++ b/packages/migrate/migrations/sveltekit-2/migrate.js @@ -17,6 +17,7 @@ export function update_pkg_json_content(content) { return update_pkg(content, [ // All other bumps are done as part of the Svelte 4 migration ['@sveltejs/kit', '^2.0.0'], + ['@sveltejs/adapter-static', '^3.0.0'], ['vite', '^5.0.0'], ['vitest', '^1.0.0'], ['typescript', '^5.0.0'], // should already be done by Svelte 4 migration, but who knows