From ceb7503aa56900082cf751bb3efaa34e0339211f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 16:57:57 -0500 Subject: [PATCH 01/28] create _env.js dynamic module --- packages/kit/src/exports/vite/index.js | 4 +- packages/kit/src/runtime/server/index.js | 39 +++++++++++++------ .../kit/src/runtime/server/page/render.js | 3 +- packages/kit/src/runtime/server/respond.js | 32 +++++++++++++++ 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index acd5b88f2b09..4b43a527045c 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -380,7 +380,7 @@ function kit({ svelte_config }) { case '\0virtual:$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 + '/_env.js')).env;`; } return create_dynamic_module( @@ -572,7 +572,7 @@ function kit({ svelte_config }) { preserveEntrySignatures: 'strict' }, ssrEmitAssets: true, - target: ssr ? 'node16.14' : undefined + target: ssr ? 'node18.13' : 'es2022' }, publicDir: kit.files.assets, worker: { diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 607f161b2742..448082fb1627 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -3,6 +3,18 @@ import { set_private_env, set_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'; + +const prerender_env = new Proxy( + {}, + { + get() { + throw new Error( + 'Cannot read values from $env/dynamic/public while prerendering. Use $env/static/public instead' + ); + } + } +); export class Server { /** @type {import('types').SSROptions} */ @@ -24,22 +36,25 @@ export class Server { * }} opts */ async init({ env }) { + const private_env = filter_private_env(env, { + public_prefix: this.#options.env_public_prefix, + private_prefix: this.#options.env_private_prefix + }); + + const public_env = building + ? prerender_env + : filter_public_env(env, { + public_prefix: this.#options.env_public_prefix, + private_prefix: this.#options.env_private_prefix + }); + // 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 - }) - ); + + set_private_env(private_env); + set_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 0edbeb333dab..b3d0f32a8191 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -292,10 +292,11 @@ export async function render_response({ const blocks = []; + // TODO only populate `env` if `$env/dynamic/public` is used in the app const properties = [ paths.assets && `assets: ${s(paths.assets)}`, `base: ${base_expression}`, - `env: ${s(public_env)}` + `env: ${state.prerendering ? null : s(public_env)}` ].filter(Boolean); if (chunks) { diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index f20fc2ff6373..dfb4eae1f739 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 { public_env } from '../shared-server.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ @@ -46,6 +47,15 @@ const page_methods = new Set(['GET', 'HEAD', 'POST']); const allowed_page_methods = new Set(['GET', 'HEAD', 'OPTIONS']); +/** @type {string} */ +let public_env_body; + +/** @type {string} */ +let public_env_etag; + +/** @type {Headers} */ +let public_env_headers; + /** * @param {Request} request * @param {import('types').SSROptions} options @@ -98,6 +108,28 @@ export async function respond(request, options, manifest, state) { decoded = decoded.slice(base.length) || '/'; } + if (decoded === '/_env.js') { + public_env_body ??= `export const env=${JSON.stringify(public_env)}`; + public_env_etag ??= `W/${Date.now()}`; + public_env_headers ??= new Headers({ + 'content-type': 'application/javascript; charset=utf-8', + etag: public_env_etag + }); + + let if_none_match_value = request.headers.get('if-none-match'); + + if (if_none_match_value === (public_env_etag ??= `W/${Date.now()}`)) { + return new Response(null, { + status: 304, + headers: public_env_headers + }); + } + + return new Response(public_env_body, { + headers: public_env_headers + }); + } + const is_data_request = has_data_suffix(decoded); /** @type {boolean[] | undefined} */ let invalidated_data_nodes; From bb540aa922a988a74bdd6b7540df9976748ba848 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 17:20:02 -0500 Subject: [PATCH 02/28] tidy up --- packages/kit/src/runtime/server/respond.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index dfb4eae1f739..9e0282ace18c 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -116,9 +116,7 @@ export async function respond(request, options, manifest, state) { etag: public_env_etag }); - let if_none_match_value = request.headers.get('if-none-match'); - - if (if_none_match_value === (public_env_etag ??= `W/${Date.now()}`)) { + if (request.headers.get('if-none-match') === public_env_etag) { return new Response(null, { status: 304, headers: public_env_headers From ced5f322702b628ac6ff7a6193f4dc11e3c47887 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 17:23:53 -0500 Subject: [PATCH 03/28] tidy up --- packages/kit/src/runtime/server/env_module.js | 29 +++++++++++++++++++ packages/kit/src/runtime/server/respond.js | 29 ++----------------- 2 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 packages/kit/src/runtime/server/env_module.js 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..28c280a6626f --- /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('', { status: 304, headers }); + } + + return new Response(body, { headers }); +} diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 9e0282ace18c..a40489a1f470 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -30,7 +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 { public_env } from '../shared-server.js'; +import { get_public_env } from './env_module.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ @@ -47,15 +47,6 @@ const page_methods = new Set(['GET', 'HEAD', 'POST']); const allowed_page_methods = new Set(['GET', 'HEAD', 'OPTIONS']); -/** @type {string} */ -let public_env_body; - -/** @type {string} */ -let public_env_etag; - -/** @type {Headers} */ -let public_env_headers; - /** * @param {Request} request * @param {import('types').SSROptions} options @@ -109,23 +100,7 @@ export async function respond(request, options, manifest, state) { } if (decoded === '/_env.js') { - public_env_body ??= `export const env=${JSON.stringify(public_env)}`; - public_env_etag ??= `W/${Date.now()}`; - public_env_headers ??= new Headers({ - 'content-type': 'application/javascript; charset=utf-8', - etag: public_env_etag - }); - - if (request.headers.get('if-none-match') === public_env_etag) { - return new Response(null, { - status: 304, - headers: public_env_headers - }); - } - - return new Response(public_env_body, { - headers: public_env_headers - }); + return get_public_env(request); } const is_data_request = has_data_suffix(decoded); From 18c0a9d0cc5d343848e6f694aeeb6465a9570eac Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 17:36:31 -0500 Subject: [PATCH 04/28] allow %sveltekit.env.PUBLIC_FOO% to bypass proxy --- packages/kit/src/core/postbuild/analyse.js | 7 ++++-- packages/kit/src/core/sync/write_server.js | 4 +-- packages/kit/src/runtime/server/index.js | 25 +++++++++---------- .../kit/src/runtime/server/page/render.js | 4 +-- packages/kit/src/runtime/shared-server.js | 21 ++++++++++++++-- packages/kit/src/types/internal.d.ts | 1 + 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 73bd7faa40b6..227d72790554 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/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index ae18efe2f4fe..8803c5e52304 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -28,7 +28,7 @@ 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_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, @@ -62,7 +62,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/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 448082fb1627..a51b6d33baec 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,5 +1,5 @@ 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'; @@ -36,25 +36,24 @@ export class Server { * }} opts */ async init({ env }) { + // 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 + const private_env = filter_private_env(env, { public_prefix: this.#options.env_public_prefix, private_prefix: this.#options.env_private_prefix }); - const public_env = building - ? prerender_env - : filter_public_env(env, { - public_prefix: this.#options.env_public_prefix, - private_prefix: this.#options.env_private_prefix - }); - - // 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 + const public_env = filter_public_env(env, { + public_prefix: this.#options.env_public_prefix, + private_prefix: this.#options.env_private_prefix + }); set_private_env(private_env); - set_public_env(public_env); + set_public_env(building ? prerender_env : 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 b3d0f32a8191..98b0e413ffa3 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'; @@ -432,7 +432,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/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..385abf2dc6ff 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; } From c921db885b27768acd256e62b3b125facf635c69 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 17:48:18 -0500 Subject: [PATCH 05/28] add config option --- packages/kit/src/core/config/index.spec.js | 1 + packages/kit/src/core/config/options.js | 1 + packages/kit/src/core/sync/write_server.js | 1 + packages/kit/src/exports/public.d.ts | 5 +++++ packages/kit/src/exports/vite/index.js | 2 +- packages/kit/src/runtime/server/env_module.js | 2 +- packages/kit/src/runtime/server/respond.js | 2 +- packages/kit/src/types/internal.d.ts | 1 + packages/kit/types/index.d.ts | 5 +++++ 9 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 673515c60802..cd2c1327cc41 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -72,6 +72,7 @@ const get_defaults = (prefix = '') => ({ embedded: false, env: { dir: process.cwd(), + publicModule: '_env.js', publicPrefix: 'PUBLIC_', privatePrefix: '' }, diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index dbbb19d97cce..4a8b5b787e9f 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -115,6 +115,7 @@ const options = object( env: object({ dir: string(process.cwd()), + publicModule: string('_env.js'), publicPrefix: string('PUBLIC_'), privatePrefix: string('') }), diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 8803c5e52304..86c533d4e4be 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -35,6 +35,7 @@ export const options = { csp: ${s(config.kit.csp)}, csrf_check_origin: ${s(config.kit.csrf.checkOrigin)}, embedded: ${config.kit.embedded}, + env_public_module: '${config.kit.env.publicModule}', env_public_prefix: '${config.kit.env.publicPrefix}', env_private_prefix: '${config.kit.env.privatePrefix}', hooks: null, // added lazily, via \`get_hooks\` diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index c96ba096b068..f38e03ea7a83 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -362,6 +362,11 @@ export interface KitConfig { * @default "." */ dir?: string; + /** + * The name of the module that contains public environment variables, relative to `paths.base`. Normally, these values are inlined into the HTML for maximum performance, but if the user lands on a prerendered page, values will instead be retrieved from this module — which is dynamically created by the server — if they are needed elsewhere in the app. + * @default "_env.js"; + */ + publicModule?: string; /** * A prefix that signals that an environment variable is safe to expose to client-side code. See [`$env/static/public`](/docs/modules#$env-static-public) and [`$env/dynamic/public`](/docs/modules#$env-dynamic-public). Note that Vite's [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix) must be set separately if you are using Vite's environment variable handling - though use of that feature should generally be unnecessary. * @default "PUBLIC_" diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 4b43a527045c..2f794ee7c5cf 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -380,7 +380,7 @@ function kit({ svelte_config }) { case '\0virtual:$env/dynamic/public': // populate `$env/dynamic/public` from `window` if (browser) { - return `export const env = ${global}.env ?? (await import(/* @vite-ignore */ ${global}.base + '/_env.js')).env;`; + return `export const env = ${global}.env ?? (await import(/* @vite-ignore */ ${global}.base + '/' + '${kit.env.publicModule}')).env;`; } return create_dynamic_module( diff --git a/packages/kit/src/runtime/server/env_module.js b/packages/kit/src/runtime/server/env_module.js index 28c280a6626f..7d37ac6a1481 100644 --- a/packages/kit/src/runtime/server/env_module.js +++ b/packages/kit/src/runtime/server/env_module.js @@ -22,7 +22,7 @@ export function get_public_env(request) { }); if (request.headers.get('if-none-match') === etag) { - return new Response('', { status: 304, headers }); + return new Response(undefined, { status: 304, headers }); } return new Response(body, { headers }); diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index a40489a1f470..9410d5f697ef 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -99,7 +99,7 @@ export async function respond(request, options, manifest, state) { decoded = decoded.slice(base.length) || '/'; } - if (decoded === '/_env.js') { + if (decoded === '/' + options.env_public_module) { return get_public_env(request); } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 385abf2dc6ff..d521c3b7a659 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -335,6 +335,7 @@ export interface SSROptions { csp: ValidatedConfig['kit']['csp']; csrf_check_origin: boolean; embedded: boolean; + env_public_module: string; env_public_prefix: string; env_private_prefix: string; hooks: ServerHooks; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 13f5143b12d5..b45c18dae7f7 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -344,6 +344,11 @@ declare module '@sveltejs/kit' { * @default "." */ dir?: string; + /** + * The name of the module that contains public environment variables, relative to `paths.base`. Normally, these values are inlined into the HTML for maximum performance, but if the user lands on a prerendered page, values will instead be retrieved from this module — which is dynamically created by the server — if they are needed elsewhere in the app. + * @default "_env.js"; + */ + publicModule?: string; /** * A prefix that signals that an environment variable is safe to expose to client-side code. See [`$env/static/public`](/docs/modules#$env-static-public) and [`$env/dynamic/public`](/docs/modules#$env-dynamic-public). Note that Vite's [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix) must be set separately if you are using Vite's environment variable handling - though use of that feature should generally be unnecessary. * @default "PUBLIC_" From f71120f1bd4aa54355b6e8e20e4f950b0195bb1f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 18:04:42 -0500 Subject: [PATCH 06/28] update test --- .../kit/test/prerendering/basics/src/routes/env/+page.svelte | 3 --- packages/kit/test/prerendering/basics/test/tests.spec.js | 1 - 2 files changed, 4 deletions(-) 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..b88cad6cc67e 100644 --- a/packages/kit/test/prerendering/basics/src/routes/env/+page.svelte +++ b/packages/kit/test/prerendering/basics/src/routes/env/+page.svelte @@ -1,6 +1,5 @@

PRIVATE_STATIC: {data.PRIVATE_STATIC}

-

PRIVATE_DYNAMIC: {data.PRIVATE_DYNAMIC}

PUBLIC_STATIC: {PUBLIC_STATIC}

diff --git a/packages/kit/test/prerendering/basics/test/tests.spec.js b/packages/kit/test/prerendering/basics/test/tests.spec.js index c92475d063dd..ecf337c35a76 100644 --- a/packages/kit/test/prerendering/basics/test/tests.spec.js +++ b/packages/kit/test/prerendering/basics/test/tests.spec.js @@ -205,10 +205,7 @@ 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); }); From ca45fa736fd5aaaea3888ad99d5acab44142b832 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 18:30:18 -0500 Subject: [PATCH 09/28] separate comments --- packages/kit/src/runtime/server/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 2ab97f97da4e..8996182daf7a 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -37,8 +37,8 @@ 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 env, in case it's used in initialisation const private_env = filter_private_env(env, { public_prefix: this.#options.env_public_prefix, private_prefix: this.#options.env_private_prefix From e36107fb352e9d7331cc54d82fb5c965432c9bbc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 18:34:22 -0500 Subject: [PATCH 10/28] drive-by: partially fix message --- packages/kit/src/core/postbuild/prerender.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index a064ae2c3c18..55abd8fc33d6 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -473,10 +473,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` ); } From 48b751275f964d85a4b8e090190058fd5d105126 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 18:46:54 -0500 Subject: [PATCH 11/28] tweak --- packages/kit/src/runtime/server/index.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 8996182daf7a..bf14575b3438 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -39,15 +39,13 @@ export class Server { // been done already. // set env, in case it's used in initialisation - const private_env = filter_private_env(env, { + const prefixes = { public_prefix: this.#options.env_public_prefix, private_prefix: this.#options.env_private_prefix - }); + }; - const public_env = filter_public_env(env, { - 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); From d2ff6afdfd19d8395a40a756fd1e0575207b2faa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 18:46:59 -0500 Subject: [PATCH 12/28] update test --- packages/kit/test/apps/basics/src/routes/+layout.server.js | 5 ----- 1 file changed, 5 deletions(-) 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') { From a132100fd605b7a22e77c2060ffe3c5dbf05dcb9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 19:35:29 -0500 Subject: [PATCH 13/28] add test --- packages/kit/test/apps/basics/package.json | 2 +- packages/kit/test/apps/basics/playwright.config.js | 12 +++++++++++- .../src/routes/prerendering/env/+layout.svelte | 4 ++++ .../src/routes/prerendering/env/dynamic/+page.svelte | 5 +++++ .../routes/prerendering/env/prerendered/+page.svelte | 1 + packages/kit/test/apps/basics/test/client.test.js | 9 +++++++++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/prerendering/env/+layout.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/prerendering/env/dynamic/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.svelte diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json index 96e00f1f05bd..b9bbea8d6959 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 })\" && 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..4ea30221df78 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 + ? 'PUBLIC_PRERENDERING=false pnpm dev' + : 'PUBLIC_PRERENDERING=true pnpm build && pnpm preview', + port: process.env.DEV ? 5173 : 4173 + } +}; 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.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', () => { From c129696a4458121fee1760db4067416c6ab35f38 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 19:47:11 -0500 Subject: [PATCH 14/28] migration notes --- .../docs/60-appendix/30-migrating-to-sveltekit-2.md | 8 ++++++++ 1 file changed, 8 insertions(+) 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 6cda9848e788..2f2d3bcb6605 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. + ## 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. From 0f15ede3d92c1b787872889add3e304445f4141b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 20:17:15 -0500 Subject: [PATCH 15/28] preload _env.js when appropriate --- packages/kit/src/core/postbuild/prerender.js | 1 + .../src/exports/vite/graph_analysis/index.js | 6 ++-- packages/kit/src/exports/vite/index.js | 28 +++++++++++++------ packages/kit/src/exports/vite/module_ids.js | 7 +++++ .../kit/src/runtime/server/page/render.js | 6 +++- packages/kit/src/types/internal.d.ts | 1 + 6 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 packages/kit/src/exports/vite/module_ids.js diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 55abd8fc33d6..814311510a70 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.env.publicModule); const immutable = `${config.appDir}/immutable`; if (existsSync(`${out}/server/${immutable}`)) { 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 2f794ee7c5cf..3886dda2d663 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,19 +374,19 @@ 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 ?? (await import(/* @vite-ignore */ ${global}.base + '/' + '${kit.env.publicModule}')).env;`; @@ -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` @@ -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/page/render.js b/packages/kit/src/runtime/server/page/render.js index 98b0e413ffa3..c2a28f16b266 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.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.env_public_module); + } + const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter( (path) => resolve_opts.preload({ type: 'js', path }) ); @@ -296,7 +300,7 @@ export async function render_response({ const properties = [ paths.assets && `assets: ${s(paths.assets)}`, `base: ${base_expression}`, - `env: ${state.prerendering ? null : s(public_env)}` + `env: ${!client.uses_env_dynamic_public || state.prerendering ? null : s(public_env)}` ].filter(Boolean); if (chunks) { diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index d521c3b7a659..317bc0f27844 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -60,6 +60,7 @@ export interface BuildData { imports: string[]; stylesheets: string[]; fonts: string[]; + uses_env_dynamic_public: boolean; } | null; server_manifest: import('vite').Manifest; } From 050e113883787a3b9c46a33f057c02524df28e9c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 20:19:42 -0500 Subject: [PATCH 16/28] fix adapter-static tests --- .../test/apps/prerendered/src/routes/public-env/+page.svelte | 4 ++-- .../test/apps/spa/src/routes/fallback/[...rest]/+page.svelte | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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..9d55084c4e5b 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,5 @@ -

The answer is {env.PUBLIC_ANSWER}

+

The answer is {PUBLIC_ANSWER}

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} From c4381919e30192ef6552e2c9222b69c43746284b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 20:31:36 -0500 Subject: [PATCH 17/28] expose generateEnvModule method for adapter-static --- packages/adapter-static/index.js | 1 + .../apps/prerendered/src/routes/public-env/+page.svelte | 6 ++++++ packages/adapter-static/test/apps/prerendered/test/test.js | 1 + packages/kit/src/core/adapt/builder.js | 7 +++++++ packages/kit/src/exports/public.d.ts | 5 +++++ packages/kit/types/index.d.ts | 6 ++++++ 6 files changed, 26 insertions(+) diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index 59791bac00ef..6f1dd2fc34e1 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/test/apps/prerendered/src/routes/public-env/+page.svelte b/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte index 9d55084c4e5b..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 {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/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index cc99e8da8555..e20c6bcb5f95 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/pages/${config.kit.env.publicModule}`; + 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/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index f38e03ea7a83..142ae65aaf03 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 diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b45c18dae7f7..8e187ae9dc42 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 @@ -1526,6 +1531,7 @@ declare module '@sveltejs/kit' { imports: string[]; stylesheets: string[]; fonts: string[]; + uses_env_dynamic_public: boolean; } | null; server_manifest: import('vite').Manifest; } From c336ee224896b83ad00c38f39dc442d41edcdc82 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 20:42:37 -0500 Subject: [PATCH 18/28] check --- packages/kit/src/exports/vite/dev/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 257fd69e2526..87abd2015c1a 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 () => { From 115bd19c6adbaed169c13c667e8f585b24c6dfdd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 20:43:23 -0500 Subject: [PATCH 19/28] windows --- packages/kit/test/apps/basics/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json index b9bbea8d6959..7dc877eca861 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 })\" && PUBLIC_PRERENDERING=false 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/" }, From 1197fcd956bd29f352b8c10778f9a7648b63c335 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Dec 2023 20:48:38 -0500 Subject: [PATCH 20/28] wow i really hate windows --- packages/kit/test/apps/basics/playwright.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/apps/basics/playwright.config.js b/packages/kit/test/apps/basics/playwright.config.js index 4ea30221df78..0f7c1458a513 100644 --- a/packages/kit/test/apps/basics/playwright.config.js +++ b/packages/kit/test/apps/basics/playwright.config.js @@ -4,8 +4,8 @@ export default { ...config, webServer: { command: process.env.DEV - ? 'PUBLIC_PRERENDERING=false pnpm dev' - : 'PUBLIC_PRERENDERING=true pnpm build && pnpm preview', + ? 'cross-env PUBLIC_PRERENDERING=false pnpm dev' + : 'cross-env PUBLIC_PRERENDERING=true pnpm build && pnpm preview', port: process.env.DEV ? 5173 : 4173 } }; From e5836a430651f539894cf699a252459670a43da2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 08:07:32 -0500 Subject: [PATCH 21/28] bump adapter-static peer dependency --- .changeset/pink-lobsters-protect.md | 5 +++++ packages/adapter-static/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/pink-lobsters-protect.md 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/packages/adapter-static/package.json b/packages/adapter-static/package.json index af7a7f8b585f..8d9e3452de98 100644 --- a/packages/adapter-static/package.json +++ b/packages/adapter-static/package.json @@ -40,6 +40,6 @@ "vite": "^5.0.4" }, "peerDependencies": { - "@sveltejs/kit": "^1.5.0 || ^2.0.0" + "@sveltejs/kit": "^2.0.0" } } From 6bc7b8e2decb7a018aa78ce13ad4c645eb6c2b7e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 08:08:07 -0500 Subject: [PATCH 22/28] remove obsolete comment --- packages/kit/src/runtime/server/page/render.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index c2a28f16b266..1855f8005d50 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -296,7 +296,6 @@ export async function render_response({ const blocks = []; - // TODO only populate `env` if `$env/dynamic/public` is used in the app const properties = [ paths.assets && `assets: ${s(paths.assets)}`, `base: ${base_expression}`, From 17fd0e0482fb75ba2a0cfae1462c748ee185f5f3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 08:09:22 -0500 Subject: [PATCH 23/28] changeset --- .changeset/calm-pugs-applaud.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/calm-pugs-applaud.md 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 From e87f14cf981da2f54777ba33c6c94f34ffb66f3e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 08:27:41 -0500 Subject: [PATCH 24/28] doh --- .../apps/basics/src/routes/prerendering/env/prerendered/+page.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/kit/test/apps/basics/src/routes/prerendering/env/prerendered/+page.js 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; From 27b7aade12c5a790180275243fe557cc8fb15e93 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 08:42:35 -0500 Subject: [PATCH 25/28] bump adapter-static as part of migration --- packages/migrate/migrations/sveltekit-2/migrate.js | 1 + 1 file changed, 1 insertion(+) 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 From 0337f6f06f6528ed316a41d1712345d611458ead Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 08:53:54 -0500 Subject: [PATCH 26/28] update migration docs --- .../docs/60-appendix/30-migrating-to-sveltekit-2.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 2f2d3bcb6605..eef9a6d6861c 100644 --- a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md +++ b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md @@ -115,6 +115,16 @@ Because of this, dynamic environment variables can no longer be read during prer ## 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. From 3a0be0435075a54ecad7af9470f82cf89cd800d1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 09:10:26 -0500 Subject: [PATCH 27/28] reuse appDir instead of polluting everything --- packages/kit/src/core/adapt/builder.js | 2 +- packages/kit/src/core/config/index.spec.js | 1 - packages/kit/src/core/config/options.js | 1 - packages/kit/src/core/postbuild/prerender.js | 2 +- packages/kit/src/core/sync/write_server.js | 2 +- packages/kit/src/exports/public.d.ts | 9 +++------ packages/kit/src/exports/vite/index.js | 2 +- packages/kit/src/runtime/server/page/render.js | 2 +- packages/kit/src/runtime/server/respond.js | 2 +- packages/kit/src/types/internal.d.ts | 2 +- 10 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index e20c6bcb5f95..55af5e3cdfc3 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -157,7 +157,7 @@ export function create_builder({ }, generateEnvModule() { - const dest = `${config.kit.outDir}/output/prerendered/pages/${config.kit.env.publicModule}`; + 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)}`); diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index cd2c1327cc41..673515c60802 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -72,7 +72,6 @@ const get_defaults = (prefix = '') => ({ embedded: false, env: { dir: process.cwd(), - publicModule: '_env.js', publicPrefix: 'PUBLIC_', privatePrefix: '' }, diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 4a8b5b787e9f..dbbb19d97cce 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -115,7 +115,6 @@ const options = object( env: object({ dir: string(process.cwd()), - publicModule: string('_env.js'), publicPrefix: string('PUBLIC_'), privatePrefix: string('') }), diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 814311510a70..f6e3e2315f37 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -150,7 +150,7 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) { } const files = new Set(walk(`${out}/client`).map(posixify)); - files.add(config.env.publicModule); + files.add(`${config.appDir}/env.js`); const immutable = `${config.appDir}/immutable`; if (existsSync(`${out}/server/${immutable}`)) { diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 86c533d4e4be..8971f82aa871 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -31,11 +31,11 @@ import { set_assets } from '__sveltekit/paths'; 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)}, embedded: ${config.kit.embedded}, - env_public_module: '${config.kit.env.publicModule}', env_public_prefix: '${config.kit.env.publicPrefix}', env_private_prefix: '${config.kit.env.privatePrefix}', hooks: null, // added lazily, via \`get_hooks\` diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 142ae65aaf03..434122dbc389 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -289,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; @@ -367,11 +369,6 @@ export interface KitConfig { * @default "." */ dir?: string; - /** - * The name of the module that contains public environment variables, relative to `paths.base`. Normally, these values are inlined into the HTML for maximum performance, but if the user lands on a prerendered page, values will instead be retrieved from this module — which is dynamically created by the server — if they are needed elsewhere in the app. - * @default "_env.js"; - */ - publicModule?: string; /** * A prefix that signals that an environment variable is safe to expose to client-side code. See [`$env/static/public`](/docs/modules#$env-static-public) and [`$env/dynamic/public`](/docs/modules#$env-dynamic-public). Note that Vite's [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix) must be set separately if you are using Vite's environment variable handling - though use of that feature should generally be unnecessary. * @default "PUBLIC_" diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 3886dda2d663..c4227c0bbd25 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -389,7 +389,7 @@ function kit({ svelte_config }) { case env_dynamic_public: // populate `$env/dynamic/public` from `window` if (browser) { - return `export const env = ${global}.env ?? (await import(/* @vite-ignore */ ${global}.base + '/' + '${kit.env.publicModule}')).env;`; + return `export const env = ${global}.env ?? (await import(/* @vite-ignore */ ${global}.base + '/' + '${kit.appDir}/env.js')).env;`; } return create_dynamic_module( diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 1855f8005d50..487d11c28b6b 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -277,7 +277,7 @@ export async function render_response({ if (page_config.csr) { if (client.uses_env_dynamic_public && state.prerendering) { - modulepreloads.add(options.env_public_module); + modulepreloads.add(`${options.app_dir}/env.js`); } const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter( diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 9410d5f697ef..076ceef7a089 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -99,7 +99,7 @@ export async function respond(request, options, manifest, state) { decoded = decoded.slice(base.length) || '/'; } - if (decoded === '/' + options.env_public_module) { + if (decoded === `/${options.app_dir}/env.js`) { return get_public_env(request); } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 317bc0f27844..2234eca3f4ef 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -332,11 +332,11 @@ 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; embedded: boolean; - env_public_module: string; env_public_prefix: string; env_private_prefix: string; hooks: ServerHooks; From d4f7ce228099cbf28185a751f543c40aba0b90af Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 09:23:35 -0500 Subject: [PATCH 28/28] regenerate types --- packages/kit/types/index.d.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index d1a861eaaf35..79e925d0f4cc 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -271,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; @@ -349,11 +351,6 @@ declare module '@sveltejs/kit' { * @default "." */ dir?: string; - /** - * The name of the module that contains public environment variables, relative to `paths.base`. Normally, these values are inlined into the HTML for maximum performance, but if the user lands on a prerendered page, values will instead be retrieved from this module — which is dynamically created by the server — if they are needed elsewhere in the app. - * @default "_env.js"; - */ - publicModule?: string; /** * A prefix that signals that an environment variable is safe to expose to client-side code. See [`$env/static/public`](/docs/modules#$env-static-public) and [`$env/dynamic/public`](/docs/modules#$env-dynamic-public). Note that Vite's [`envPrefix`](https://vitejs.dev/config/shared-options.html#envprefix) must be set separately if you are using Vite's environment variable handling - though use of that feature should generally be unnecessary. * @default "PUBLIC_"