From 779d85752578c13b2dcb900f86b841729e899d1d Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Fri, 10 Jul 2026 19:04:55 +0800 Subject: [PATCH 01/11] Revert "firnat" This reverts commit 9f27bd256e5c4797688e88df936df9e29683e245. --- packages/kit/src/core/postbuild/analyse.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index c6410df47ea9..ebe786f369fb 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -115,7 +115,12 @@ async function analyse({ const prerender = page?.prerender ?? endpoint?.prerender; if (prerender !== true) { - for (const _ of list_features(route, manifest_data, server_manifest, tracked_features)) { + for (const _ of list_features( + route, + manifest_data, + server_manifest, + tracked_features + )) { // TODO: check feature while inside the environment } } From a41a0b224ce9a22a880278a4141993679e901efb Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Fri, 10 Jul 2026 19:04:57 +0800 Subject: [PATCH 02/11] Revert "split" This reverts commit f9204a6dbd8f5c14f708d21257b4ade325527583. --- packages/kit/src/exports/vite/types.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/kit/src/exports/vite/types.d.ts b/packages/kit/src/exports/vite/types.d.ts index 08461dd45e86..5e076af58d2f 100644 --- a/packages/kit/src/exports/vite/types.d.ts +++ b/packages/kit/src/exports/vite/types.d.ts @@ -19,9 +19,17 @@ declare module 'vite/types/customEvent.d.ts' { endpoints_page_options: Array; }; 'sveltekit:ssr-load-module-error': Error; + 'sveltekit:prerender-assets': string; } } +export interface SerialisedResponse { + status: number; + statusText: string; + headers: Record; + body: ArrayBuffer; +} + export interface EnforcedConfig { [key: string]: EnforcedConfig | true; } From 82a5fecc637bc380accb4ac799fa338969106465 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Fri, 10 Jul 2026 19:05:00 +0800 Subject: [PATCH 03/11] Revert "Delete short-chefs-stare.md" This reverts commit 52db6b17856dd9c19d3ba3a478d3daa560dde32e. --- .changeset/short-chefs-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/short-chefs-stare.md diff --git a/.changeset/short-chefs-stare.md b/.changeset/short-chefs-stare.md new file mode 100644 index 000000000000..32bf84b5b390 --- /dev/null +++ b/.changeset/short-chefs-stare.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +feat: use the Vite Environment API in development, preview, build analysis and prerendering From 0d8c90896147d8b4d28307bbd7996d44709f1f65 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Fri, 10 Jul 2026 19:05:01 +0800 Subject: [PATCH 04/11] Revert "separate analysis and prerender changes" This reverts commit 400790ff6bb131150385a6b48ebf8333b9840256. --- packages/kit/src/core/adapt/builder.js | 9 +- packages/kit/src/core/adapt/index.js | 3 + packages/kit/src/core/postbuild/ambient.d.ts | 7 + packages/kit/src/core/postbuild/analyse.js | 305 ++----------- .../kit/src/core/postbuild/analysis_entry.js | 321 +++++++++++++ packages/kit/src/core/postbuild/fallback.js | 78 ++-- packages/kit/src/core/postbuild/prerender.js | 327 ++++++++------ .../kit/src/core/postbuild/prerender_entry.js | 134 ++++++ .../kit/src/exports/vite/build/vite_server.js | 424 ++++++++++++++++++ packages/kit/src/exports/vite/index.js | 16 +- packages/kit/src/runtime/server/ambient.d.ts | 4 + packages/kit/src/runtime/server/fetch.js | 2 +- packages/kit/src/runtime/server/index.js | 37 +- .../kit/src/runtime/server/page/load_data.js | 5 +- packages/kit/src/runtime/server/read.js | 39 ++ packages/kit/src/types/internal.d.ts | 5 +- .../kit/test/build-errors/prerender.spec.js | 3 +- 17 files changed, 1225 insertions(+), 494 deletions(-) create mode 100644 packages/kit/src/core/postbuild/ambient.d.ts create mode 100644 packages/kit/src/core/postbuild/analysis_entry.js create mode 100644 packages/kit/src/core/postbuild/prerender_entry.js create mode 100644 packages/kit/src/exports/vite/build/vite_server.js create mode 100644 packages/kit/src/runtime/server/read.js diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js index 57d10b1e15c2..8b29d81b1df6 100644 --- a/packages/kit/src/core/adapt/builder.js +++ b/packages/kit/src/core/adapt/builder.js @@ -48,6 +48,7 @@ const extensions = [ * log: Logger; * vite_config: ResolvedConfig; * remotes: RemoteChunk[]; + * out: string; * explicit_env_config: Record> | null; * }} opts * @returns {Builder} @@ -62,6 +63,7 @@ export function create_builder({ log, vite_config, remotes, + out, explicit_env_config }) { /** @type {Map} */ @@ -131,14 +133,11 @@ export function create_builder({ async generateFallback(dest) { const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`; - const env = loadEnv(vite_config.mode, config.kit.env.dir, ''); const fallback = await generate_fallback({ manifest_path, - env, - out_dir: config.kit.outDir, - origin: config.kit.paths.origin || 'http://sveltekit-prerender', - assets: config.kit.files.assets + root: vite_config.root, + out }); if (existsSync(dest)) { diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js index b8d8f0b17ebe..766c09628570 100644 --- a/packages/kit/src/core/adapt/index.js +++ b/packages/kit/src/core/adapt/index.js @@ -10,6 +10,7 @@ import { create_builder } from './builder.js'; * @param {import('types').Logger} log * @param {import('types').RemoteChunk[]} remotes * @param {import('vite').ResolvedConfig} vite_config + * @param {string} out * @param {Record> | null} explicit_env_config */ export async function adapt( @@ -21,6 +22,7 @@ export async function adapt( log, remotes, vite_config, + out, explicit_env_config ) { const { name, adapt } = config.kit.adapter; @@ -37,6 +39,7 @@ export async function adapt( log, remotes, vite_config, + out, explicit_env_config }); diff --git a/packages/kit/src/core/postbuild/ambient.d.ts b/packages/kit/src/core/postbuild/ambient.d.ts new file mode 100644 index 000000000000..d8464c7df2a7 --- /dev/null +++ b/packages/kit/src/core/postbuild/ambient.d.ts @@ -0,0 +1,7 @@ +declare module '__SERVER__/index.js' { + export { Server } from '@sveltejs/kit'; +} + +declare module '__SERVER__/env.js' { + export * from '__sveltekit/env'; +} diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index ebe786f369fb..134a139b21df 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -1,274 +1,67 @@ /** @import { RemoteChunk } from 'types' */ -import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { validate_server_exports } from '../../utils/exports.js'; +/** @import { ManifestData, ServerMetadata } from 'types' */ +/** @import { Manifest } from 'vite' */ +import * as devalue from 'devalue'; import { extract_svelte_config, load_vite_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; -import { ENDPOINT_METHODS } from '../../constants.js'; -import { has_server_load, resolve_route } from '../../utils/routing.js'; -import { createReadableStream } from '@sveltejs/kit/node'; -import { PageNodes } from '../../utils/page_nodes.js'; +import { create_build_server } from '../../exports/vite/build/vite_server.js'; +import { get_port } from '../utils.js'; export default forked(import.meta.url, analyse); +const analysis_entry = import.meta.resolve('./analysis_entry.js'); + /** - * @param {{ - * hash: boolean; - * manifest_path: string; - * manifest_data: import('types').ManifestData; - * server_manifest: import('vite').Manifest; - * tracked_features: Record; - * env: Record; - * remotes: RemoteChunk[]; - * vite_config_file: string | undefined; - * }} opts + * @param {object} opts Arguments must be serialisable via the structured clone algorithm + * @param {string} opts.manifest_path + * @param {ManifestData} opts.manifest_data + * @param {Manifest} opts.server_manifest + * @param {Record} opts.tracked_features + * @param {string} opts.out + * @param {RemoteChunk[]} opts.remotes + * @param {string} opts.root + * @param {string | undefined} opts.vite_config_file + * @returns {Promise<{ metadata: ServerMetadata }>} */ async function analyse({ - hash, manifest_path, manifest_data, server_manifest, tracked_features, - env, - remotes, + out, + root, vite_config_file }) { - /** @type {import('@sveltejs/kit').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; - const vite_config = await load_vite_config(vite_config_file); - const config = extract_svelte_config(vite_config).kit; - const server_root = join(config.outDir, 'output'); - - /** @type {import('types').ServerInternalModule} */ - const internal = await import(pathToFileURL(`${server_root}/server/internal.js`).href); - - // configure `import { building } from '$app/env'` — - // essential we do this before analysing the code - internal.set_building(); - - // set `read` and `manifest`, in case they're used in initialisation - internal.set_manifest(manifest); - internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`)); - - // `set_env` lives in a separate module that imports the user's `src/env` config. We import it - // *after* `set_building()` so that `building`-dependent expressions resolve correctly - /** @type {import('__sveltekit/env')} */ - const { set_env } = await import(pathToFileURL(`${server_root}/server/env.js`).href); - set_env(env); - - /** @type {import('types').ServerMetadata} */ - const metadata = { - nodes: [], - routes: new Map(), - remotes: new Map() - }; - - const nodes = await Promise.all(manifest._.nodes.map((loader) => loader())); - - // analyse nodes - for (const node of nodes) { - if (hash && node.universal) { - const options = Object.keys(node.universal).filter((o) => o !== 'load'); - if (options.length > 0) { - throw new Error( - `Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options - .filter((o) => o !== 'load') - .map((o) => `'${o}'`) - .join(', ')})` - ); - } - } - - metadata.nodes[node.index] = { - has_server_load: has_server_load(node), - has_universal_load: node.universal?.load !== undefined - }; - } - - // analyse routes - for (const route of manifest._.routes) { - const page = - route.page && - analyse_page( - route.page.layouts.map((n) => (n === undefined ? n : nodes[n])), - nodes[route.page.leaf] - ); - - const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint()); - - if (page?.prerender && endpoint?.prerender) { - throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`); - } - - if (page?.config && endpoint?.config) { - for (const key in { ...page.config, ...endpoint.config }) { - if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) { - throw new Error( - `Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any` - ); - } - } - } - - const route_config = page?.config ?? endpoint?.config ?? {}; - const prerender = page?.prerender ?? endpoint?.prerender; - - if (prerender !== true) { - for (const _ of list_features( - route, - manifest_data, - server_manifest, - tracked_features - )) { - // TODO: check feature while inside the environment - } - } - - const page_methods = page?.methods ?? []; - const api_methods = endpoint?.methods ?? []; - const entries = page?.entries ?? endpoint?.entries; - - metadata.routes.set(route.id, { - config: route_config, - methods: Array.from(new Set([...page_methods, ...api_methods])), - page: { - methods: page_methods - }, - api: { - methods: api_methods - }, - prerender, - entries: - entries && (await entries()).map((entry_object) => resolve_route(route.id, entry_object)) - }); - } - - // analyse remotes - for (const remote of remotes) { - const loader = manifest._.remotes[remote.hash]; - const { default: functions } = await loader(); - - const exports = new Map(); - - for (const name in functions) { - const internals = /** @type {import('types').RemoteInternals} */ (functions[name].__); - const type = internals.type; - - exports.set(name, { - type, - dynamic: type !== 'prerender' || internals.dynamic - }); - } - - metadata.remotes.set(remote.hash, exports); - } - - return { metadata }; -} - -/** - * @param {import('types').SSRRoute} route - * @param {import('types').SSREndpoint} mod - */ -function analyse_endpoint(route, mod) { - validate_server_exports(mod, route.id); - - if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { - throw new Error( - `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` - ); - } - - /** @type {Array} */ - const methods = []; - - for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) { - if (mod[method]) methods.push(method); - } - - if (mod.fallback) { - methods.push('*'); - } - - return { - config: mod.config, - entries: mod.entries, - methods, - prerender: mod.prerender ?? false - }; -} - -/** - * @param {Array} layouts - * @param {import('types').SSRNode} leaf - */ -function analyse_page(layouts, leaf) { - /** @type {Array<'GET' | 'POST'>} */ - const methods = ['GET']; - if (leaf.server?.actions) methods.push('POST'); - - const nodes = new PageNodes([...layouts, leaf]); - nodes.validate(); - - return { - config: nodes.get_config(), - entries: leaf.universal?.entries ?? leaf.server?.entries, - methods, - prerender: nodes.prerender() - }; -} - -/** - * @param {import('types').SSRRoute} route - * @param {import('types').ManifestData} manifest_data - * @param {import('vite').Manifest} server_manifest - * @param {Record} tracked_features - */ -function list_features(route, manifest_data, server_manifest, tracked_features) { - const features = new Set(); - - const route_data = /** @type {import('types').RouteData} */ ( - manifest_data.routes.find((r) => r.id === route.id) - ); - - const visited = new Set(); - /** @param {string} id */ - function visit(id) { - if (visited.has(id)) return; - visited.add(id); - - const chunk = server_manifest[id]; - if (!chunk) return; - - if (chunk.file in tracked_features) { - for (const feature of tracked_features[chunk.file]) { - features.add(feature); - } - } - - if (chunk.imports) { - for (const id of chunk.imports) { - visit(id); - } - } - } - - let page_node = route_data?.leaf; - while (page_node) { - if (page_node.server) visit(page_node.server); - page_node = page_node.parent ?? null; - } - - if (route_data.endpoint) { - visit(route_data.endpoint.file); - } - - if (manifest_data.hooks.server) { - // TODO if hooks.server.js imports `read`, it will be in the entry chunk - // we don't currently account for that case - visit(manifest_data.hooks.server); - } - return Array.from(features); + const config = extract_svelte_config(vite_config); + + const server = await create_build_server({ + name: 'analyse', + svelte_config: config, + out, + root, + manifest_path, + server_path: analysis_entry + }); + + await server.listen(); + + const port = get_port(server); + const response = await fetch(new URL(`http://localhost:${port}`), { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: devalue.stringify({ + server_manifest, + tracked_features, + manifest_data, + hash: config.kit.router.type === 'hash' + }) + }); + + await server.close(); + + return { metadata: devalue.parse(await response.text()) }; } diff --git a/packages/kit/src/core/postbuild/analysis_entry.js b/packages/kit/src/core/postbuild/analysis_entry.js new file mode 100644 index 000000000000..c966f8d39aa5 --- /dev/null +++ b/packages/kit/src/core/postbuild/analysis_entry.js @@ -0,0 +1,321 @@ +/** @import { Server as KitServer, SSRManifest } from '@sveltejs/kit' */ +/** @import { ManifestData, ServerMetadata } from 'types' */ +/** @import { Manifest } from 'vite' */ +import { get } from '__sveltekit/ipc'; +import { + options, + set_manifest, + set_read_implementation, + set_building +} from '__SERVER__/internal.js'; +import * as devalue from 'devalue'; +import { ENDPOINT_METHODS } from '../../constants.js'; +import { has_server_load, resolve_route } from '../../utils/routing.js'; +import { validate_server_exports } from '../../utils/exports.js'; +import { PageNodes } from '../../utils/page_nodes.js'; +import { check_feature } from '../../utils/features.js'; +import { load_and_validate_params } from '../../utils/params.js'; +import { create_synchronous_read } from '../../runtime/server/read.js'; +import { loud_ssr_load_module } from '../../exports/vite/dev/utils.js'; +import { to_fs } from '../../utils/vite.js'; + +set_building(); + +// `set_env` lives in a separate module that imports the user's `src/env` config. We import it +// *after* `set_building()` so that `building`-dependent expressions resolve correctly +const { set_env } = await import('__SERVER__/env.js'); + +/** @implements {KitServer} */ +export class Server { + /** @type {import('@sveltejs/kit').SSRManifest} */ + manifest; + + /** @type {import('types').SSROptions} */ + options; + + // set env, `read`, and `manifest`, in case they're used when we analyse the user's code + + /** @param {SSRManifest} manifest */ + constructor(manifest) { + this.manifest = manifest; + /** @type {import('types').SSROptions} */ + this.options = options; + + set_manifest(manifest); + } + + /** @type {KitServer['init']} */ + init({ env }) { + set_env(env); + + set_read_implementation( + create_synchronous_read(async (file) => { + const response = await get(`/read?${new URLSearchParams({ file })}`); + if (!response.ok) { + throw new Error( + `read(...) failed: could not fetch ${file} (${response.status} ${response.statusText})` + ); + } + return response.body; + }) + ); + + return Promise.resolve(); + } + + /** @type {KitServer['respond']} */ + async respond(request) { + /** @type {{ hash: boolean; server_manifest: Manifest; tracked_features: Record; manifest_data: ManifestData; }} */ + const { server_manifest, tracked_features, manifest_data, hash } = devalue.parse( + await request.text() + ); + + const metadata = await analyse({ + server_manifest, + tracked_features, + manifest: this.manifest, + manifest_data, + hash + }); + + return new Response(devalue.stringify(metadata)); + } +} + +/** + * + * @param {object} opts + * @param {Manifest} opts.server_manifest + * @param {Record} opts.tracked_features + * @param {SSRManifest} opts.manifest + * @param {ManifestData} opts.manifest_data + * @param {boolean} opts.hash + * @returns {Promise} + */ +async function analyse({ server_manifest, tracked_features, manifest, manifest_data, hash }) { + await load_and_validate_params({ + routes: manifest_data.routes, + params_path: 'entries/params.js', + root: to_fs(`${__SVELTEKIT_OUT_DIR__}/server`), + load: (file) => loud_ssr_load_module(file) + }); + + /** @type {import('types').ServerMetadata} */ + const metadata = { + nodes: [], + routes: new Map(), + remotes: new Map(), + remotes_with_prerender: new Set() + }; + + const nodes = await Promise.all(manifest._.nodes.map((loader) => loader())); + + // analyse nodes + for (const node of nodes) { + if (hash && node.universal) { + const options = Object.keys(node.universal).filter((o) => o !== 'load'); + if (options.length > 0) { + throw new Error( + `Page options are ignored when \`router.type === 'hash'\` (${node.universal_id} has ${options + .filter((o) => o !== 'load') + .map((o) => `'${o}'`) + .join(', ')})` + ); + } + } + + metadata.nodes[node.index] = { + has_server_load: has_server_load(node), + has_universal_load: node.universal?.load !== undefined + }; + } + + // analyse routes + for (const route of manifest._.routes) { + const page = + route.page && + analyse_page( + route.page.layouts.map((n) => (n === undefined ? n : nodes[n])), + nodes[route.page.leaf] + ); + + const endpoint = route.endpoint && analyse_endpoint(route, await route.endpoint()); + + if (page?.prerender && endpoint?.prerender) { + throw new Error(`Cannot prerender a route with both +page and +server files (${route.id})`); + } + + if (page?.config && endpoint?.config) { + for (const key in { ...page.config, ...endpoint.config }) { + if (JSON.stringify(page.config[key]) !== JSON.stringify(endpoint.config[key])) { + throw new Error( + `Mismatched route config for ${route.id} — the +page and +server files must export the same config, if any` + ); + } + } + } + + const route_config = page?.config ?? endpoint?.config ?? {}; + const prerender = page?.prerender ?? endpoint?.prerender; + + if (prerender !== true) { + for (const feature of list_features( + route, + manifest_data, + server_manifest, + tracked_features + )) { + await check_feature(route.id, route_config, feature); + } + } + + const page_methods = page?.methods ?? []; + const api_methods = endpoint?.methods ?? []; + const entries = page?.entries ?? endpoint?.entries; + + metadata.routes.set(route.id, { + config: route_config, + methods: Array.from(new Set([...page_methods, ...api_methods])), + page: { + methods: page_methods + }, + api: { + methods: api_methods + }, + prerender, + entries: + entries && (await entries()).map((entry_object) => resolve_route(route.id, entry_object)) + }); + } + + // analyse remotes + for (const [hash, loader] of Object.entries(manifest._.remotes)) { + const { default: functions } = await loader(); + + const exports = new Map(); + + for (const name in functions) { + const internals = /** @type {import('types').RemoteInternals} */ (functions[name].__); + const type = internals.type; + + exports.set(name, { + type, + dynamic: type !== 'prerender' || internals.dynamic + }); + + if (type === 'prerender') { + metadata.remotes_with_prerender.add(hash); + } + } + + metadata.remotes.set(hash, exports); + } + + return metadata; +} + +/** + * @param {import('types').SSRRoute} route + * @param {import('types').SSREndpoint} mod + */ +function analyse_endpoint(route, mod) { + validate_server_exports(mod, route.id); + + if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { + throw new Error( + `Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})` + ); + } + + /** @type {Array} */ + const methods = []; + + for (const method of /** @type {import('types').HttpMethod[]} */ (ENDPOINT_METHODS)) { + if (mod[method]) methods.push(method); + } + + if (mod.fallback) { + methods.push('*'); + } + + return { + config: mod.config, + entries: mod.entries, + methods, + prerender: mod.prerender ?? false + }; +} + +/** + * @param {Array} layouts + * @param {import('types').SSRNode} leaf + */ +function analyse_page(layouts, leaf) { + /** @type {Array<'GET' | 'POST'>} */ + const methods = ['GET']; + if (leaf.server?.actions) methods.push('POST'); + + const nodes = new PageNodes([...layouts, leaf]); + nodes.validate(); + + return { + config: nodes.get_config(), + entries: leaf.universal?.entries ?? leaf.server?.entries, + methods, + prerender: nodes.prerender() + }; +} + +/** + * @param {import('types').SSRRoute} route + * @param {import('types').ManifestData} manifest_data + * @param {import('vite').Manifest} server_manifest + * @param {Record} tracked_features + */ +function list_features(route, manifest_data, server_manifest, tracked_features) { + const features = new Set(); + + const route_data = /** @type {import('types').RouteData} */ ( + manifest_data.routes.find((r) => r.id === route.id) + ); + + const visited = new Set(); + /** @param {string} id */ + function visit(id) { + if (visited.has(id)) return; + visited.add(id); + + const chunk = server_manifest[id]; + if (!chunk) return; + + if (chunk.file in tracked_features) { + for (const feature of tracked_features[chunk.file]) { + features.add(feature); + } + } + + if (chunk.imports) { + for (const id of chunk.imports) { + visit(id); + } + } + } + + let page_node = route_data?.leaf; + while (page_node) { + if (page_node.server) visit(page_node.server); + page_node = page_node.parent ?? null; + } + + if (route_data.endpoint) { + visit(route_data.endpoint.file); + } + + if (manifest_data.hooks.server) { + // TODO if hooks.server.js imports `read`, it will be in the entry chunk + // we don't currently account for that case + visit(manifest_data.hooks.server); + } + + return Array.from(features); +} diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js index cc9e7a8f7681..9f2272987058 100644 --- a/packages/kit/src/core/postbuild/fallback.js +++ b/packages/kit/src/core/postbuild/fallback.js @@ -1,47 +1,61 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; +/** @import { Plugin } from 'vite' */ +import { escape_for_regexp } from '../../utils/escape.js'; +import { create_build_server } from '../../exports/vite/build/vite_server.js'; import { forked } from '../../utils/fork.js'; +import { get_port } from '../utils.js'; +import { extract_svelte_config, load_vite_config } from '../config/index.js'; export default forked(import.meta.url, generate_fallback); +const prerender_entry = import.meta.resolve('./prerender_entry.js'); + /** - * @param {{ - * manifest_path: string; - * env: Record - * out_dir: string; - * origin: string; - * assets: string; - * }} opts + * @param {object} opts Arguments must be serialisable via the structured clone algorithm + * @param {string} opts.manifest_path + * @param {string} opts.out + * @param {string} opts.root + * @returns {Promise} */ -async function generate_fallback({ manifest_path, env, out_dir, origin, assets }) { - const server_root = join(out_dir, 'output'); +async function generate_fallback({ manifest_path, out, root }) { + const vite_config = await load_vite_config(); // TODO configFile + const svelte_config = extract_svelte_config(vite_config); + + const prerender_origin = svelte_config.kit.paths.origin || 'http://sveltekit-prerender'; - /** @type {import('types').ServerInternalModule} */ - const { set_building } = await import(pathToFileURL(`${server_root}/server/internal.js`).href); + /** @type {Plugin} */ + const plugin_generate_fallback = { + name: 'vite-plugin-sveltekit-compile:generate-fallback', + configureServer(vite) { + return () => { + vite.middlewares.use((req, _, next) => { + req.url = req.url?.replace( + new RegExp(escape_for_regexp(`^http://localhost:${port}`)), + prerender_origin + ); + req.headers.host = new URL(prerender_origin).host; - /** @type {import('types').ServerModule} */ - const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href); + next(); + }); + }; + } + }; - /** @type {import('@sveltejs/kit').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; + const server = await create_build_server({ + name: 'generate-fallback', + svelte_config, + out, + root, + manifest_path, + server_path: prerender_entry, + vite_plugins: [plugin_generate_fallback] + }); - set_building(); + await server.listen(); - const server = new Server(manifest); - await server.init({ env }); + const port = get_port(server); + const response = await fetch(`http://localhost:${port}/[fallback]`); - const response = await server.respond(new Request(origin + '/[fallback]'), { - getClientAddress: () => { - throw new Error('Cannot read clientAddress during prerendering'); - }, - prerendering: { - fallback: true, - dependencies: new Map(), - remote_responses: new Map() - }, - read: (file) => readFileSync(join(assets, file)) - }); + await server.close(); if (response.ok) { return await response.text(); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index f4bbda81fc25..84b838f7e121 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -1,21 +1,23 @@ +/** @import { Logger, PrerenderDependency, Prerendered, PrerenderMap, ServerMetadata } from 'types' */ +/** @import { Plugin } from 'vite' */ +/** @import { SerialisedResponse } from '../../exports/vite/types.js' */ import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { pathToFileURL } from 'node:url'; import { mkdirp, walk } from '../../utils/filesystem.js'; -import { posixify } from '../../utils/os.js'; import { noop } from '../../utils/functions.js'; import { decode_uri, is_root_relative, resolve } from '../../utils/url.js'; -import { escape_html } from '../../utils/escape.js'; -import { logger } from '../utils.js'; -import { extract_svelte_config, load_vite_config } from '../config/index.js'; +import { escape_for_regexp, escape_html } from '../../utils/escape.js'; +import { get_port, logger } from '../utils.js'; import { get_route_segments } from '../../utils/routing.js'; import { queue } from './queue.js'; import { crawl } from './crawl.js'; import { forked } from '../../utils/fork.js'; import * as devalue from 'devalue'; -import { createReadableStream } from '@sveltejs/kit/node'; import generate_fallback from './fallback.js'; -import { stringify_remote_arg } from '../../runtime/shared.js'; +import { posixify } from '../../utils/os.js'; +import { create_app_dir_matcher } from '../../exports/vite/dev/index.js'; +import { create_build_server } from '../../exports/vite/build/vite_server.js'; +import { extract_svelte_config, load_vite_config } from '../config/index.js'; export default forked(import.meta.url, prerender); @@ -25,42 +27,22 @@ export default forked(import.meta.url, prerender); // "If decodedFragment is an ASCII case-insensitive match for the string 'top', then return the top of the document." const SPECIAL_HASHLINKS = new Set(['', 'top']); +const prerender_entry = import.meta.resolve('./prerender_entry.js'); + /** - * @param {{ - * hash: boolean; - * out: string; - * manifest_path: string; - * metadata: import('types').ServerMetadata; - * verbose: boolean; - * env: Record; - * vite_config_file: string | undefined; - * }} opts + * @param {object} opts Arguments must be serialisable via the structured clone algorithm + * @param {string} opts.out + * @param {string} opts.manifest_path + * @param {ServerMetadata} opts.metadata + * @param {boolean} opts.verbose + * @param {string} opts.root + * @param {string | undefined} opts.vite_config_file */ -async function prerender({ hash, out, manifest_path, metadata, verbose, env, vite_config_file }) { - /** @type {import('@sveltejs/kit').SSRManifest} */ - const manifest = (await import(pathToFileURL(manifest_path).href)).manifest; - - /** @type {import('types').ServerInternalModule} */ - const internal = await import(pathToFileURL(`${out}/server/internal.js`).href); - - // configure `import { building } from `$app/env` — - // essential we do this before analysing the code - internal.set_building(); - internal.set_prerendering(); - - // `set_env` and `Server` live in modules that import the user's `src/env` config. We import them - // *after* `set_building()` so that `building`-dependent expressions resolve correctly - /** @type {import('__sveltekit/env')} */ - const { set_env } = await import(pathToFileURL(`${out}/server/env.js`).href); - set_env(env); - - /** @type {import('types').ServerModule} */ - const { Server } = await import(pathToFileURL(`${out}/server/index.js`).href); - +async function prerender({ out, manifest_path, metadata, verbose, root, vite_config_file }) { /** * @template {{message: string}} T * @template {Omit} K - * @param {import('types').Logger} log + * @param {Logger} log * @param {'fail' | 'warn' | 'ignore' | ((details: T) => void)} input * @param {(details: K) => string} format * @returns {(details: K) => void} @@ -86,7 +68,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const OK = 2; const REDIRECT = 3; - /** @type {import('types').Prerendered} */ + /** @type {Prerendered} */ const prerendered = { pages: new Map(), assets: new Map(), @@ -94,7 +76,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit paths: [] }; - /** @type {import('types').PrerenderMap} */ + /** @type {PrerenderMap} */ const prerender_map = new Map(); for (const [id, { prerender }] of metadata.routes) { @@ -103,26 +85,21 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit } } - /** @type {Set} */ - const prerendered_routes = new Set(); - const vite_config = await load_vite_config(vite_config_file); - const config = extract_svelte_config(vite_config).kit; + const svelte_config = extract_svelte_config(vite_config); - const prerender_origin = config.paths.origin || 'http://sveltekit-prerender'; + const prerender_origin = svelte_config.kit.paths.origin || 'http://sveltekit-prerender'; - if (hash) { + if (svelte_config.kit.router.type === 'hash') { const fallback = await generate_fallback({ manifest_path, - env, - out_dir: config.outDir, - origin: prerender_origin, - assets: config.files.assets + out, + root }); const file = output_filename('/', true); - const dest = `${config.outDir}/output/prerendered/pages/${file}`; + const dest = `${out}/prerendered/pages/${file}`; mkdirp(dirname(dest)); writeFileSync(dest, fallback); @@ -132,7 +109,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit return { prerendered, prerender_map }; } - /** @type {import('types').Logger} */ + /** @type {Logger} */ const log = logger({ verbose }); /** @type {Map} */ @@ -140,10 +117,10 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const handle_http_error = normalise_error_handler( log, - config.prerender.handleHttpError, + svelte_config.kit.prerender.handleHttpError, ({ status, path, referrer, referenceType }) => { const message = - status === 404 && !path.startsWith(config.paths.base) + status === 404 && !path.startsWith(svelte_config.kit.paths.base) ? `${path} does not begin with \`base\`. You can fix this by using \`resolve('${path}')\` from \`$app/paths\`. The base path is configurable from \`paths.base\` - see https://svelte.dev/docs/kit/configuration#paths for more info` : path; @@ -153,7 +130,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const handle_missing_id = normalise_error_handler( log, - config.prerender.handleMissingId, + svelte_config.kit.prerender.handleMissingId, ({ path, id, referrers }) => { return ( `The following pages contain links to ${path}#${id}, but no element with id="${id}" exists on ${path} - see the \`handleMissingId\` option in https://svelte.dev/docs/kit/configuration#prerender for more info:` + @@ -164,7 +141,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const handle_entry_generator_mismatch = normalise_error_handler( log, - config.prerender.handleEntryGeneratorMismatch, + svelte_config.kit.prerender.handleEntryGeneratorMismatch, ({ generatedFromId, entry, matchedId }) => { return `The entries export from ${generatedFromId} generated entry ${entry}, which was matched by ${matchedId} - see the \`handleEntryGeneratorMismatch\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.`; } @@ -172,7 +149,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const handle_not_prerendered_route = normalise_error_handler( log, - config.prerender.handleUnseenRoutes, + svelte_config.kit.prerender.handleUnseenRoutes, ({ routes }) => { const list = routes.map((id) => ` - ${id}`).join('\n'); return `The following routes were marked as prerenderable, but were not prerendered because they were not found while crawling your app:\n${list}\n\nSee the \`handleUnseenRoutes\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.`; @@ -181,20 +158,20 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const handle_invalid_url = normalise_error_handler( log, - config.prerender.handleInvalidUrl, + svelte_config.kit.prerender.handleInvalidUrl, ({ href, referrer }) => { return `Invalid URL ${href}${referrer ? ` (linked from ${referrer})` : ''}`; } ); - const q = queue(config.prerender.concurrency); + const q = queue(svelte_config.kit.prerender.concurrency); /** * @param {string} path * @param {boolean} is_html */ function output_filename(path, is_html) { - const file = path.slice(config.paths.base.length + 1) || 'index.html'; + const file = path.slice(svelte_config.kit.paths.base.length + 1) || 'index.html'; if (is_html && !file.endsWith('.html')) { return file + (file.endsWith('/') ? 'index.html' : '.html'); @@ -204,23 +181,20 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit } const files = new Set(walk(`${out}/client`).map(posixify)); - files.add(`${config.appDir}/env.js`); + files.add(`${svelte_config.kit.appDir}/env.js`); - const immutable = `${config.appDir}/immutable`; + const immutable = `${svelte_config.kit.appDir}/immutable`; if (existsSync(`${out}/server/${immutable}`)) { for (const file of walk(`${out}/server/${immutable}`)) { - files.add(posixify(`${config.appDir}/immutable/${file}`)); + files.add(posixify(`${svelte_config.kit.appDir}/immutable/${file}`)); } } - const remote_prefix = `${config.paths.base}/${config.appDir}/remote/`; + const remote_prefix = `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}/remote/`; const seen = new Set(); const written = new Set(); - /** @type {Map>} */ - const remote_responses = new Map(); - /** @type {Map>} */ const expected_hashlinks = new Map(); @@ -237,7 +211,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit if (seen.has(decoded)) return; seen.add(decoded); - const file = decoded.slice(config.paths.base.length + 1); + const file = decoded.slice(svelte_config.kit.paths.base.length + 1); if (files.has(file)) return; return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id)); @@ -250,36 +224,35 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit * @param {string} [generated_from_id] */ async function visit(decoded, encoded, referrer, generated_from_id) { - if (!decoded.startsWith(config.paths.base)) { + if (!decoded.startsWith(svelte_config.kit.paths.base)) { handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' }); return; } - /** @type {Map} */ - const dependencies = new Map(); - - const response = await server.respond(new Request(prerender_origin + encoded), { - getClientAddress() { - throw new Error('Cannot read clientAddress during prerendering'); - }, - prerendering: { - dependencies, - remote_responses - }, - read: (file) => { - // stuff we just wrote - const filepath = saved.get(file); - if (filepath) return readFileSync(filepath); - - // Static assets emitted during build - if (file.startsWith(config.appDir)) { - return readFileSync(`${out}/server/${file}`); - } - - // stuff in `static` - return readFileSync(join(config.files.assets, file)); + /** @type {PromiseWithResolvers>} */ + const prerender_dependencies = Promise.withResolvers(); + + const event = `sveltekit:prerender-dependencies:${encoded}`; + /** @param {{ dependencies: Record }} data */ + const handle_dependencies = (data) => { + /** @type {Map} */ + const deserialised = new Map(); + for (const [path, dependency] of Object.entries(data.dependencies)) { + deserialised.set(path, { + response: new Response(dependency.response.body, { + headers: dependency.response.headers, + status: dependency.response.status, + statusText: dependency.response.statusText + }), + body: dependency.body + }); } - }); + prerender_dependencies.resolve(deserialised); + server.environments.ssr.hot.off(event, handle_dependencies); + }; + server.environments.ssr.hot.on(event, handle_dependencies); + + const response = await fetch(`http://localhost:${port}${encoded}`, { redirect: 'manual' }); const encoded_id = response.headers.get('x-sveltekit-routeid'); const decoded_id = encoded_id && decode_uri(encoded_id); @@ -300,7 +273,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const category = decoded.startsWith(remote_prefix) ? 'data' : 'pages'; save(category, response, body, decoded, encoded, referrer, 'linked'); - for (const [dependency_path, result] of dependencies) { + for (const [dependency_path, result] of await prerender_dependencies.promise) { // this seems circuitous, but using new URL allows us to not care // whether dependency_path is encoded or not const encoded_dependency_path = new URL(dependency_path, 'http://localhost').pathname; @@ -339,7 +312,11 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const headers = Object.fromEntries(response.headers); // if it's a 200 HTML response, crawl it. Skip error responses, as we don't save those - if (response.ok && config.prerender.crawl && headers['content-type'] === 'text/html') { + if ( + response.ok && + svelte_config.kit.prerender.crawl && + headers['content-type'] === 'text/html' + ) { const { ids, hrefs, invalid } = crawl(body.toString(), decoded); for (const href of invalid) { @@ -349,7 +326,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit actual_hashlinks.set(decoded, ids); /** @param {string} href */ - const removePrerenderOrigin = (href) => { + const remove_prerender_origin = (href) => { if (href.startsWith(prerender_origin)) { if (href === prerender_origin) return '/'; if (href.at(prerender_origin.length) !== '/') return href; @@ -358,7 +335,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit return href; }; - for (const href of hrefs.map(removePrerenderOrigin)) { + for (const href of hrefs.map(remove_prerender_origin)) { if (!is_root_relative(href)) continue; const { pathname, search, hash } = new URL(href, 'http://localhost'); @@ -382,6 +359,9 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit } } + /** @type {Set} */ + const prerendered_routes = new Set(); + /** * @param {'pages' | 'dependencies' | 'data'} category * @param {Response} response @@ -398,14 +378,18 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit const type = headers['content-type']; const is_html = response_type === REDIRECT || type === 'text/html'; - if (!is_html && response.status === 200 && decoded.slice(config.paths.base.length + 1) === '') { + if ( + !is_html && + response.status === 200 && + decoded.slice(svelte_config.kit.paths.base.length + 1) === '' + ) { throw new Error( - `Cannot prerender a root +server.js that returns a non-HTML response - static hosts always serve an HTML file for \`${config.paths.base || '/'}\`` + `Cannot prerender a root +server.js that returns a non-HTML response - static hosts always serve an HTML file for \`${svelte_config.kit.paths.base || '/'}\`` ); } const file = output_filename(decoded, is_html); - const dest = `${config.outDir}/output/prerendered/${category}/${file}`; + const dest = `${out}/prerendered/${category}/${file}`; if (written.has(file)) return; @@ -492,7 +476,7 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit handle_http_error({ status: response.status, path: decoded, referrer, referenceType }); } - manifest.assets.add(file); + server.environments.ssr.hot.send('sveltekit:prerender-assets', file); saved.set(file, dest); } @@ -504,49 +488,92 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit } } - let should_prerender = false; + const should_prerender = + prerender_map.values().some((value) => !!value) || !!metadata.remotes_with_prerender.size; - for (const value of prerender_map.values()) { - if (value) { - should_prerender = true; - break; - } + if (!should_prerender) { + return { prerendered, prerender_map }; } - // the user's remote function modules may reference `read` or the `manifest` at the top-level - // so we need to set them before evaluating those modules to avoid potential runtime errors - internal.set_manifest(manifest); - internal.set_read_implementation((file) => createReadableStream(`${out}/server/${file}`)); + log.info('Prerendering'); - /** @type {Array} */ - const prerender_functions = []; + const prerender_read_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/prerender-read' + ); - for (const loader of Object.values(manifest._.remotes)) { - const module = await loader(); + /** @type {Plugin} */ + const plugin_prerender = { + name: 'vite-plugin-sveltekit-compile:prerender', + configureServer(vite) { + return () => { + vite.middlewares.use((req, res, next) => { + req.url = req.url?.replace( + new RegExp(escape_for_regexp(`^http://localhost:${port}`)), + prerender_origin + ); + req.headers.host = new URL(prerender_origin).host; + + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const url = new URL(base + req.url); + const decoded = decodeURI(url.pathname); + + if (decoded.match(prerender_read_pathname)) { + const file = url.searchParams.get('file'); + + if (!file) { + res.writeHead(400); + res.end('Missing file query argument'); + return; + } + + /** @type {Buffer} */ + let data; + + // stuff we just wrote + const filepath = saved.get(file); + if (filepath) { + data = readFileSync(filepath); + } else if (file.startsWith(svelte_config.kit.appDir)) { + // Static assets emitted during build + data = readFileSync(`${out}/server/${file}`); + } else { + // stuff in `static` + data = readFileSync(join(svelte_config.kit.files.assets, file)); + } + + res.setHeader('content-type', 'application/octet-stream'); + res.end(data); + return; + } - for (const fn of Object.values(module.default)) { - if (fn?.__?.type === 'prerender') { - prerender_functions.push(fn.__); - should_prerender = true; - } + next(); + }); + }; } - } - - if (!should_prerender) { - return { prerendered, prerender_map }; - } + }; - // only run the server after the `should_prerender` check so that we - // don't run the user's init hook unnecessarily - const server = new Server(manifest); - await server.init({ - env, - read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`) + const server = await create_build_server({ + name: 'prerender', + svelte_config, + out, + root, + manifest_path, + server_path: prerender_entry, + vite_plugins: [plugin_prerender] }); - log.info('Prerendering'); + // only start the app server after checking if prerendering is needed so + // that we don't run the user's `init` hook unnecessarily + await server.listen(); - for (const entry of config.prerender.entries) { + const port = get_port(server); + + for (const entry of svelte_config.kit.prerender.entries) { if (entry === '*') { for (const [id, prerender] of prerender_map) { if (prerender) { @@ -556,36 +583,40 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit if (processed_id.includes('[')) continue; const path = `/${get_route_segments(processed_id).join('/')}`; - void enqueue(null, config.paths.base + path); + void enqueue(null, svelte_config.kit.paths.base + path); } } } else { - void enqueue(null, config.paths.base + entry); + void enqueue(null, svelte_config.kit.paths.base + entry); } } for (const { id, entries } of route_level_entries) { for (const entry of entries) { - void enqueue(null, config.paths.base + entry, undefined, id); + void enqueue(null, svelte_config.kit.paths.base + entry, undefined, id); } } - const transport = (await internal.get_hooks()).transport ?? {}; - for (const internals of prerender_functions) { - if (internals.has_arg) { - for (const arg of (await internals.inputs?.()) ?? []) { - void enqueue( - null, - remote_prefix + internals.id + '/' + stringify_remote_arg(arg, transport) - ); - } - } else { - void enqueue(null, remote_prefix + internals.id); - } + const url = new URL( + `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}/prerender-functions`, + `http://localhost:${port}` + ); + for (const name of metadata.remotes_with_prerender) { + url.searchParams.append('name', name); + } + + const response = await fetch(url); + /** @type {string[]} */ + const functions_to_prerender = await response.json(); + + for (const decoded of functions_to_prerender) { + void enqueue(null, decoded); } await q.done(); + await server.close(); + // handle invalid fragment links for (const [key, referrers] of expected_hashlinks) { const index = key.indexOf('#'); diff --git a/packages/kit/src/core/postbuild/prerender_entry.js b/packages/kit/src/core/postbuild/prerender_entry.js new file mode 100644 index 000000000000..ec9a58e93f35 --- /dev/null +++ b/packages/kit/src/core/postbuild/prerender_entry.js @@ -0,0 +1,134 @@ +/** @import { SSRManifest } from '@sveltejs/kit' */ +/** @import { InternalServer, RemotePrerenderInternals } from 'types' */ +/** @import { SerialisedResponse } from '../../exports/vite/types.js' */ +import { get } from '__sveltekit/ipc'; +import { get_hooks, set_building, set_prerendering } from '__SERVER__/internal.js'; +import { stringify_remote_arg } from '../../runtime/shared.js'; + +set_building(); +set_prerendering(); + +// `set_env` and `Server` live in modules that import the user's `src/env` config. We import them +// *after* `set_building()` so that `building`-dependent expressions resolve correctly +const { set_env } = await import('__SERVER__/env.js'); +const { Server: KitServer } = await import('__SERVER__/index.js'); + +export class Server extends KitServer { + #manifest; + + /** @param {SSRManifest} manifest */ + constructor(manifest) { + super(manifest); + + this.#manifest = manifest; + + import.meta.hot?.on('sveltekit:prerender-assets', (data) => { + manifest.assets.add(data); + }); + } + + /** @type {InternalServer['init']} */ + init(options) { + set_env(options.env); + + options.read = async (file) => { + const response = await get(`/read?${new URLSearchParams({ file })}`); + if (!response.ok) { + throw new Error( + `read(...) failed: could not fetch ${file} (${response.status} ${response.statusText})` + ); + } + return response.body; + }; + + return super.init(options); + } + + /** @type {InternalServer['respond']} */ + async respond(request, options) { + options.getClientAddress = () => { + throw new Error('Cannot read clientAddress during prerendering'); + }; + + options.prerendering = { + fallback: request.url.endsWith('/[fallback]'), + dependencies: new Map(), + remote_responses: import.meta.hot?.data.remote_responses + }; + + options.read = async (file) => { + const response = await get(`/prerender-read?${new URLSearchParams({ file })}`); + return Buffer.from(await response.arrayBuffer()); + }; + + const url = new URL(request.url); + + if (url.pathname === `/${this.#manifest.appPath}/prerender-functions`) { + const names = url.searchParams.getAll('name'); + const pathnames = await get_prerender_function_paths(this.#manifest, names); + return Response.json(pathnames); + } + + const response = await super.respond(request, options); + + /** @type {Map} */ + const dependencies = new Map(); + for (const [pathname, dependency] of options.prerendering.dependencies) { + dependencies.set(pathname, { + response: { + status: dependency.response.status, + statusText: dependency.response.statusText, + headers: Object.fromEntries(dependency.response.headers), + body: await dependency.response.arrayBuffer() + }, + body: dependency.body + }); + } + import.meta.hot?.send(`sveltekit:prerender-dependencies:${url.pathname}`, { + dependencies: Object.fromEntries(dependencies) + }); + + return response; + } +} + +/** + * @param {SSRManifest} manifest + * @param {string[]} names + * @returns {Promise} + */ +async function get_prerender_function_paths(manifest, names) { + /** @type {RemotePrerenderInternals[]} */ + const prerender_functions = []; + + for (const name of names) { + const module = await manifest._.remotes[name](); + + for (const fn of Object.values(module.default)) { + if (fn?.__?.type === 'prerender') { + prerender_functions.push(fn.__); + } + } + } + + const remote_prefix = `/${manifest.appPath}/remote/`; + + /** @type {string[]} */ + const pathnames = []; + + const transport = (await get_hooks()).transport ?? {}; + for (const internals of prerender_functions) { + if (internals.has_arg) { + for (const arg of (await internals.inputs?.()) ?? []) { + pathnames.push(remote_prefix + internals.id + '/' + stringify_remote_arg(arg, transport)); + } + } else { + pathnames.push(remote_prefix + internals.id); + } + } + return pathnames; +} + +if (import.meta.hot) { + import.meta.hot.data.remote_responses ??= new Map(); +} diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js new file mode 100644 index 000000000000..00407815928c --- /dev/null +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -0,0 +1,424 @@ +/** @import { Adapter } from '@sveltejs/kit' */ +/** @import { ValidatedConfig } from 'types' */ +/** @import { Connect, Plugin, PluginOption, ViteDevServer } from 'vite' */ +/** @import { ModuleRunner } from 'vite/module-runner' */ +import fs from 'node:fs'; +import path from 'node:path'; +import { loadEnv } from 'vite'; +import { exactRegex } from 'rolldown/filter'; +import sirv from 'sirv'; +import { getRequest, setResponse } from '@sveltejs/kit/node'; +import { sveltekit_ipc } from '../module_ids.js'; +import { dedent } from '../../../core/sync/utils.js'; +import { + check_feature, + create_app_dir_matcher, + has_correct_case, + invalidate_module, + remove_static_middlewares +} from '../dev/index.js'; +import { s } from '../../../utils/misc.js'; +import { SVELTE_KIT_ASSETS } from '../../../constants.js'; +import { import_peer } from '../../../utils/import.js'; +import { get_port } from '../../../core/utils.js'; + +/** + * Spins up a Vite dev server along with the build output so that we can run + * analysis and prerendering in the environment itself. This helps us avoid + * runtime errors when the user imports non-Node runtime APIs such as `cloudflare:workers`. + * We achieve this by using Vite's `resolveId` hook to intercept module resolution + * and provide a `Server` class that runs our custom instructions. + * @param {object} opts + * @param {string} opts.name a unique name to avoid Vite `optimizeDeps` cache collision + * @param {ValidatedConfig} opts.svelte_config + * @param {string} opts.root + * @param {string} opts.out + * @param {string} opts.manifest_path + * @param {string} opts.server_path path to the module with our custom Server export + * @param {Plugin[]} [opts.vite_plugins] additional plugins to customise the Vite behaviour + * @returns {Promise} + */ +export async function create_build_server({ + name, + svelte_config, + root, + out, + manifest_path, + server_path, + vite_plugins +}) { + /** @type {typeof import('vite')} */ + const vite = await import_peer('vite', root); + + // TODO: is it important to set defaultMode, defaultNodeEnv, and isPreview? + const vite_config = await vite.resolveConfig({}, 'build'); + /** @type {Adapter | undefined} */ + const adapter = vite_config.plugins.find( + (plugin) => plugin.name === 'vite-plugin-sveltekit-adapter' + )?.api?.adapter; + + /** @type {number | undefined} */ + let port; + + const app_path = `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}`; + + /** + * Allows us to perform Node operations from a non-Node environment by sending + * a request to the Vite dev server. We can then configure a middleware to + * intercept, operate, and respond. + * + * We can't achieve the same with import.meta.hot and Promise.withResolver() + * because Cloudflare's workerd doesn't like it when we await a promise resolved + * from import.meta.hot + * @type {Plugin} + */ + const plugin_ipc = { + name: 'vite-plugin-sveltekit-compile:ipc', + configureServer(server) { + return () => { + server.middlewares.use((_req, _res, next) => { + // ensure the server port is up-to-date + const current_port = get_port(server); + if (current_port && current_port !== port) { + port = current_port; + server.environments.ssr.hot.send('sveltekit:port', port); + invalidate_module(server, sveltekit_ipc); + } + + next(); + }); + }; + }, + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + resolveId: { + filter: { + id: exactRegex('__sveltekit/ipc') + }, + handler() { + return '\0virtual:__sveltekit/ipc'; + } + }, + load: { + filter: { + id: exactRegex(sveltekit_ipc) + }, + handler() { + return dedent` + // helps us avoid global fetch warnings we emit when the user uses it incorrectly + const native_fetch = globalThis.fetch; + + export function get(pathname) { + return native_fetch(\`http://localhost:\${port}${app_path}\${pathname}\`); + } + + let port${port ? ` = ${port}` : ''}; + import.meta.hot?.on('sveltekit:port', (update) => { port = update }); + `; + } + } + }; + + /** @type {Record} */ + let env; + + /** @type {Connect.ServerStackItem | undefined} */ + let serve_static_middleware; + + /** @type {Plugin} */ + const plugin_server = { + name: 'vite-plugin-sveltekit-compile:build-entry', + config(_, vite_config_env) { + env = loadEnv(vite_config_env.mode, svelte_config.kit.env.dir, ''); + + const server_dir = `${out}/server`; + + return { + appType: 'custom', + cacheDir: `node_modules/.vite-${name}`, + environments: { + ssr: { + build: { + outDir: server_dir + } + } + }, + publicDir: `${out}/client`, + define: { + __SVELTEKIT_OUT_DIR__: s(out) + }, + resolve: { + alias: [ + { + find: '__SERVER__', + replacement: server_dir + } + ] + }, + server: { + watch: { + ignored: out + } + } + }; + }, + configureServer(vite) { + const assets = svelte_config.kit.paths.assets + ? SVELTE_KIT_ASSETS + : svelte_config.kit.paths.base; + + const asset_server = sirv(`${out}/client`, { + dev: true, + etag: true, + maxAge: 0, + extensions: [], + setHeaders: (res) => { + res.setHeader('access-control-allow-origin', '*'); + } + }); + + vite.middlewares.use((req, res, next) => { + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const decoded = decodeURI(new URL(base + req.url).pathname); + + if (decoded.startsWith(assets)) { + const pathname = decoded.slice(assets.length); + const file = svelte_config.kit.files.assets + pathname; + + if (fs.existsSync(file) && !fs.statSync(file).isDirectory()) { + if (has_correct_case(file, svelte_config.kit.files.assets)) { + req.url = encodeURI(pathname); // don't need query/hash + asset_server(req, res); + return; + } + } + } + + next(); + }); + + const check_feature_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/check-feature' + ); + + const read_pathname = create_app_dir_matcher( + svelte_config.kit.paths.base, + svelte_config.kit.appDir, + '/read' + ); + + return () => { + serve_static_middleware = vite.middlewares.stack.find( + (middleware) => + /** @type {Function} */ (middleware.handle).name === 'viteServeStaticMiddleware' + ); + + // Vite will give a 403 on URLs like /test, /static, and /package.json preventing us from + // serving routes with those names. See https://github.com/vitejs/vite/issues/7363 + remove_static_middlewares(vite.middlewares); + + vite.middlewares.use((req, res, next) => { + // Vite's base middleware strips out the base path. Restore it + req.url = req.originalUrl; + + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const url = new URL(base + req.url); + const decoded = decodeURI(url.pathname); + + if (decoded.match(check_feature_pathname)) { + const route_id = url.searchParams.get('route_id'); + const config = url.searchParams.get('config'); + const feature = url.searchParams.get('feature'); + + if (!route_id || !config || !feature) { + res.writeHead(400); + res.end('Must have route_id, config, and feature query arguments'); + return; + } + + const result = check_feature(route_id, JSON.parse(config), feature, adapter); + + res.writeHead(200); + res.end(result?.message); + return; + } + + if (decoded.match(read_pathname)) { + const file = url.searchParams.get('file'); + if (!file) { + res.writeHead(400); + res.end('Missing file query argument'); + return; + } + + const readable_stream = fs.createReadStream( + `${svelte_config.kit.outDir}/output/server/${file}` + ); + + res.writeHead(200); + readable_stream.pipe(res); + return; + } + + next(); + }); + }; + }, + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + resolveId: { + filter: { + id: [ + exactRegex('sveltekit:server-manifest'), + exactRegex('sveltekit:server'), + exactRegex('sveltekit:env'), + exactRegex('__sveltekit/env') + ] + }, + handler(id) { + if (id === 'sveltekit:server-manifest') { + return manifest_path; + } + + // substitute the Server class with our script instead + if (id === 'sveltekit:server') { + return server_path; + } + + if (id === 'sveltekit:env') { + return id; + } + } + }, + load: { + filter: { + id: exactRegex('sveltekit:env') + }, + handler() { + return `export const env = ${s(env)};`; + } + } + }; + + /** @type {ModuleRunner} */ + let runner; + + /** @type {string | undefined} */ + let remote_address; + + /** @type {Plugin} */ + const plugin_node_environment = { + name: 'vite-plugin-sveltekit-compile:node-environment', + config: { + order: 'pre', + handler() { + return { + environments: { + ssr: { + dev: { + createEnvironment(name, config) { + return vite.createFetchableDevEnvironment(name, config, { + hot: true, + transport: vite.createServerHotChannel(), + async handleRequest(request) { + try { + /** @type {import('../dev/ssr_entry.js')} */ + const { respond } = await runner.import('__sveltekit/dev-server-entry.js'); + return await respond(request, remote_address); + } catch (error) { + // Vite doesn't log errors so we do it ourselves + console.error(error); + throw error; + } + } + }); + } + } + } + } + }; + } + }, + async configureServer(server) { + if (runner) await runner.close(); + runner = vite.createServerModuleRunner(server.environments.ssr); + + return () => { + server.middlewares.use(async (req, res) => { + remote_address = req.socket.remoteAddress; + + // Vite's base middleware strips out the base path. Restore it + req.url = req.originalUrl; + + const base = `${server.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + // TODO: allow RunnableDevEnvironment too + if (!vite.isFetchableDevEnvironment(server.environments.ssr)) { + throw new Error( + 'The Vite configured dev SSR environment must be a FetchableDevEnvironment' + ); + } + + const request = getRequest({ + base, + request: req + }); + const response = await server.environments.ssr.dispatchFetch(request); + + if (response.status === 404) { + // @ts-expect-error + serve_static_middleware?.handle(req, res, () => { + void setResponse(res, response); + }); + } else { + void setResponse(res, response); + } + }); + }; + }, + applyToEnvironment(environment) { + return environment.config.consumer === 'server'; + }, + resolveId: { + filter: { + id: exactRegex('__sveltekit/dev-server-entry.js') + }, + handler() { + return path.join(import.meta.dirname, '../dev/ssr_entry.js'); + } + } + }; + + /** @type {PluginOption} */ + const plugins = [ + adapter?.vite?.plugins, + vite_plugins, + plugin_ipc, + plugin_server, + plugin_node_environment + ].filter(Boolean); + + return await vite.createServer({ + configFile: false, + command: 'serve', + plugins, + // we need to avoid using the default port of 5173 because some test configs + // may wait for that port to be available and start tests immediately because + // of our dev server instead of their app + server: { + port: 8483 + } + }); +} diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 2a9da5c87602..ed242698ce1f 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -25,7 +25,6 @@ import { } from '../../core/env.js'; import * as sync from '../../core/sync/sync.js'; import { create_assets } from '../../core/sync/create_manifest_data/index.js'; -import { load_and_validate_params } from '../../utils/params.js'; import { logger, get_mime_lookup, @@ -1891,13 +1890,6 @@ function kit({ svelte_config }) { } mkdirp(out); - await load_and_validate_params({ - routes: manifest_data.routes, - params_path: manifest_data.params, - root, - load: (file) => import(file) - }); - const { output: server_chunks } = /** @type {Rolldown.RolldownOutput} */ ( await builder.build(builder.environments.ssr) ); @@ -1950,13 +1942,13 @@ function kit({ svelte_config }) { log.info('Analysing routes'); const { metadata } = await analyse({ - hash: kit.router.type === 'hash', manifest_path, manifest_data, server_manifest: vite_server_manifest, tracked_features, - env, + out, remotes, + root, vite_config_file: vite_config.configFile }); @@ -2161,12 +2153,11 @@ function kit({ svelte_config }) { // ...and prerender const prerender_results = await prerender({ - hash: kit.router.type === 'hash', out, manifest_path, metadata, verbose, - env, + root, vite_config_file: vite_config.configFile }); prerendered = prerender_results.prerendered; @@ -2236,6 +2227,7 @@ function kit({ svelte_config }) { log, remotes, vite_config, + out, explicit_env_config ); } else { diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index 2f38261123dc..b85c9cc7fdd7 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -1,4 +1,8 @@ declare module '__SERVER__/internal.js' { export const options: import('types').SSROptions; export const get_hooks: () => Promise>; + export const set_manifest: (manifest: import('@sveltejs/kit').SSRManifest) => void; + export const set_read_implementation: (read: (file: string) => ReadableStream) => void; + export const set_building: () => void; + export const set_prerendering: () => void; } diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index baf93f1b507f..b010dd7589b7 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -99,7 +99,7 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade ? manifest.mimeTypes[filename.slice(filename.lastIndexOf('.'))] : 'text/html'; - return new Response(state.read(file), { + return new Response(await state.read(file), { headers: type ? { 'content-type': type } : {} }); } else if (read_implementation && file in manifest._.server_assets) { diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 7c926adb6a45..60f2c1b028bf 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -6,6 +6,7 @@ import { format_server_error } from './utils.js'; import { set_read_implementation, set_manifest } from '__sveltekit/server'; import { set_env } from '__sveltekit/env'; import { set_app } from './app.js'; +import { create_synchronous_read } from './read.js'; /** @type {Promise} */ let init_promise; @@ -60,41 +61,7 @@ export class Server { set_env(env); if (read) { - // Wrap the read function to handle MaybePromise - // and ensure the public API stays synchronous - /** @param {string} file */ - const wrapped_read = (file) => { - const result = read(file); - if (result instanceof ReadableStream) { - return result; - } else { - return new ReadableStream({ - async start(controller) { - try { - const stream = await Promise.resolve(result); - if (!stream) { - controller.close(); - return; - } - - const reader = stream.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - controller.enqueue(value); - } - - controller.close(); - } catch (error) { - controller.error(error); - } - } - }); - } - }; - - set_read_implementation(wrapped_read); + set_read_implementation(create_synchronous_read(read)); } // During dev and for some adapters this function might be called in quick succession, diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index a61be5fb8685..888ea2d4e381 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -291,7 +291,10 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) if (same_origin) { if (state.prerendering) { - dependency = { response, body: null }; + // the prerender code needs to read and serialise the response body + // from the environment to the Vite process so we clone the response + // to ensure its body remains unused until then + dependency = { response: response.clone(), body: null }; state.prerendering.dependencies.set(url.pathname, dependency); } } else if (url.protocol === 'https:' || url.protocol === 'http:') { diff --git a/packages/kit/src/runtime/server/read.js b/packages/kit/src/runtime/server/read.js new file mode 100644 index 000000000000..31801e0a80f6 --- /dev/null +++ b/packages/kit/src/runtime/server/read.js @@ -0,0 +1,39 @@ +/** + * Wrap the `read` function to handle `MaybePromise` + * and ensure the public API stays synchronous + * @param {NonNullable} read + * @returns {(path: string) => ReadableStream} + */ +export function create_synchronous_read(read) { + return (file) => { + const result = read(file); + if (result instanceof ReadableStream) { + return result; + } else { + return new ReadableStream({ + async start(controller) { + try { + const stream = await Promise.resolve(result); + if (!stream) { + controller.close(); + return; + } + + const reader = stream.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + controller.enqueue(value); + } + + controller.close(); + } catch (error) { + // TODO: we should throw here even if the user doesn't try to read the response body + controller.error(error); + } + } + }); + } + }; +} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index f7ee1c5ef62e..88dbe23b21b0 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -183,7 +183,7 @@ export class InternalServer extends Server { options: RequestOptions & { prerendering?: PrerenderOptions; /** @internal for saving dependencies during prerendering and generating fallback pages */ - read: (file: string) => Buffer; + read: (file: string) => MaybePromise>; /** @internal used during development to check feature availability depending on the current route */ before_handle?: ( event: RequestEvent, @@ -419,6 +419,7 @@ export interface ServerMetadata { routes: Map; /** For each hashed remote file, a map of export name -> { type, dynamic }, where `dynamic` is `false` for non-dynamic prerender functions */ remotes: Map>; + remotes_with_prerender: Set; } // TODO get rid of this in favor us using just import('svelte').Component @@ -575,7 +576,7 @@ export interface SSRState { */ prerender_default?: PrerenderOption; /** @internal reads from the filesystem when user code tries to fetch a static asset */ - read?: (file: string) => Buffer; + read?: (file: string) => MaybePromise>; /** * Used to set up `__SVELTEKIT_TRACK__` which checks if a used feature is supported. * E.g. if `read` from `$app/server` is used, it checks whether the route's config is compatible. diff --git a/packages/kit/test/build-errors/prerender.spec.js b/packages/kit/test/build-errors/prerender.spec.js index ac3dea304565..1243415dc1fd 100644 --- a/packages/kit/test/build-errors/prerender.spec.js +++ b/packages/kit/test/build-errors/prerender.spec.js @@ -1,7 +1,6 @@ import { assert, test } from 'vitest'; import { execSync } from 'node:child_process'; import path from 'node:path'; -import { EOL } from 'node:os'; const timeout = 60_000; @@ -25,7 +24,7 @@ test('entry generators should match their own route', { timeout }, () => { stdio: 'pipe', timeout }), - `Error: The entries export from /[slug]/[notSpecific] generated entry /whatever/specific, which was matched by /[slug]/specific - see the \`handleEntryGeneratorMismatch\` option in https://svelte.dev/docs/kit/configuration#prerender for more info.${EOL}To suppress or handle this error, implement \`handleEntryGeneratorMismatch\` in https://svelte.dev/docs/kit/configuration#prerender` + /Error: The entries export from \/\[slug\]\/\[notSpecific\] generated entry \/whatever\/specific, which was matched by \/\[slug\]\/specific - see the `handleEntryGeneratorMismatch` option in https:\/\/svelte\.dev\/docs\/kit\/configuration#prerender for more info\.\r?\nTo suppress or handle this error, implement `handleEntryGeneratorMismatch` in https:\/\/svelte\.dev\/docs\/kit\/configuration#prerender/ ); }); From 937f7ac52a167ff7ad458f1643b1bdd55c2c57f1 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Fri, 10 Jul 2026 22:12:43 +0800 Subject: [PATCH 05/11] fix hanging response --- .../kit/src/exports/vite/build/vite_server.js | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js index 00407815928c..9fcdfafe1276 100644 --- a/packages/kit/src/exports/vite/build/vite_server.js +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -224,52 +224,55 @@ export async function create_build_server({ remove_static_middlewares(vite.middlewares); vite.middlewares.use((req, res, next) => { - // Vite's base middleware strips out the base path. Restore it - req.url = req.originalUrl; - - const base = `${vite.config.server.https ? 'https' : 'http'}://${ - req.headers[':authority'] || req.headers.host - }`; - - const url = new URL(base + req.url); - const decoded = decodeURI(url.pathname); + try { + const base = `${vite.config.server.https ? 'https' : 'http'}://${ + req.headers[':authority'] || req.headers.host + }`; + + const url = new URL(base + req.url); + const decoded = decodeURI(url.pathname); + + if (decoded.match(check_feature_pathname)) { + const route_id = url.searchParams.get('route_id'); + const config = url.searchParams.get('config'); + const feature = url.searchParams.get('feature'); + + if (!route_id || !config || !feature) { + res.writeHead(400); + res.end('Must have route_id, config, and feature query arguments'); + return; + } - if (decoded.match(check_feature_pathname)) { - const route_id = url.searchParams.get('route_id'); - const config = url.searchParams.get('config'); - const feature = url.searchParams.get('feature'); + const result = check_feature(route_id, JSON.parse(config), feature, adapter); - if (!route_id || !config || !feature) { - res.writeHead(400); - res.end('Must have route_id, config, and feature query arguments'); + res.writeHead(200); + res.end(result?.message); return; } - const result = check_feature(route_id, JSON.parse(config), feature, adapter); + if (decoded.match(read_pathname)) { + const file = url.searchParams.get('file'); + if (!file) { + res.writeHead(400); + res.end('Missing file query argument'); + return; + } - res.writeHead(200); - res.end(result?.message); - return; - } + const readable_stream = fs.createReadStream( + `${svelte_config.kit.outDir}/output/server/${file}` + ); - if (decoded.match(read_pathname)) { - const file = url.searchParams.get('file'); - if (!file) { - res.writeHead(400); - res.end('Missing file query argument'); + res.writeHead(200); + readable_stream.pipe(res); return; } - const readable_stream = fs.createReadStream( - `${svelte_config.kit.outDir}/output/server/${file}` - ); - - res.writeHead(200); - readable_stream.pipe(res); - return; + next(); + } catch (e) { + const error = coalesce_to_error(e); + res.statusCode = 500; + res.end(error.message); } - - next(); }); }; }, From 7504dc8e4d338c90db2581e7d4a3dabb3ff91116 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Fri, 10 Jul 2026 22:29:36 +0800 Subject: [PATCH 06/11] missing import --- packages/kit/src/exports/vite/build/vite_server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js index 9fcdfafe1276..32f9675904bf 100644 --- a/packages/kit/src/exports/vite/build/vite_server.js +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -9,7 +9,6 @@ import { exactRegex } from 'rolldown/filter'; import sirv from 'sirv'; import { getRequest, setResponse } from '@sveltejs/kit/node'; import { sveltekit_ipc } from '../module_ids.js'; -import { dedent } from '../../../core/sync/utils.js'; import { check_feature, create_app_dir_matcher, @@ -17,10 +16,12 @@ import { invalidate_module, remove_static_middlewares } from '../dev/index.js'; -import { s } from '../../../utils/misc.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; -import { import_peer } from '../../../utils/import.js'; import { get_port } from '../../../core/utils.js'; +import { dedent } from '../../../core/sync/utils.js'; +import { s } from '../../../utils/misc.js'; +import { import_peer } from '../../../utils/import.js'; +import { coalesce_to_error } from '../../../utils/error.js'; /** * Spins up a Vite dev server along with the build output so that we can run From 67f6b9b43f2026c61208d5f9ae6e781227f78619 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Wed, 15 Jul 2026 04:57:36 +0800 Subject: [PATCH 07/11] silence --- packages/kit/src/exports/vite/build/vite_server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js index 32f9675904bf..b5b814cea237 100644 --- a/packages/kit/src/exports/vite/build/vite_server.js +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -137,6 +137,7 @@ export async function create_build_server({ return { appType: 'custom', + logLevel: 'silent', cacheDir: `node_modules/.vite-${name}`, environments: { ssr: { From f79d438a29f43961ac8cf7242b386d41ef980167 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Wed, 15 Jul 2026 05:11:33 +0800 Subject: [PATCH 08/11] stringify --- packages/kit/src/exports/vite/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 795ded8df263..e3f6e1b1671d 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -767,7 +767,7 @@ function kit({ svelte_config }) { return native_fetch(\`\${address}${app_path}\${pathname}\`); } - let address${address ? ` = ${address}` : ''}; + let address${address ? ` = '${address}'` : ''}; import.meta.hot?.on('sveltekit:server-address', (update) => { address = update }); `; } From 822c4be7aca5b78f40eae3f5a8fa12fdc133a389 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Wed, 15 Jul 2026 05:19:54 +0800 Subject: [PATCH 09/11] fix --- packages/kit/src/core/postbuild/analyse.js | 5 ++--- packages/kit/src/core/postbuild/fallback.js | 8 +++---- packages/kit/src/core/postbuild/prerender.js | 10 ++++----- .../kit/src/exports/vite/build/vite_server.js | 22 +++++++++---------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index 134a139b21df..c0b4678beb79 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -5,7 +5,7 @@ import * as devalue from 'devalue'; import { extract_svelte_config, load_vite_config } from '../config/index.js'; import { forked } from '../../utils/fork.js'; import { create_build_server } from '../../exports/vite/build/vite_server.js'; -import { get_port } from '../utils.js'; +import { get_address } from '../utils.js'; export default forked(import.meta.url, analyse); @@ -47,8 +47,7 @@ async function analyse({ await server.listen(); - const port = get_port(server); - const response = await fetch(new URL(`http://localhost:${port}`), { + const response = await fetch(get_address(server), { method: 'POST', headers: { 'content-type': 'application/json' diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js index 9f2272987058..ddf8ce7ee884 100644 --- a/packages/kit/src/core/postbuild/fallback.js +++ b/packages/kit/src/core/postbuild/fallback.js @@ -2,7 +2,7 @@ import { escape_for_regexp } from '../../utils/escape.js'; import { create_build_server } from '../../exports/vite/build/vite_server.js'; import { forked } from '../../utils/fork.js'; -import { get_port } from '../utils.js'; +import { get_address } from '../utils.js'; import { extract_svelte_config, load_vite_config } from '../config/index.js'; export default forked(import.meta.url, generate_fallback); @@ -29,7 +29,7 @@ async function generate_fallback({ manifest_path, out, root }) { return () => { vite.middlewares.use((req, _, next) => { req.url = req.url?.replace( - new RegExp(escape_for_regexp(`^http://localhost:${port}`)), + new RegExp(escape_for_regexp(`^${address}`)), prerender_origin ); req.headers.host = new URL(prerender_origin).host; @@ -52,8 +52,8 @@ async function generate_fallback({ manifest_path, out, root }) { await server.listen(); - const port = get_port(server); - const response = await fetch(`http://localhost:${port}/[fallback]`); + const address = get_address(server); + const response = await fetch(new URL('/[fallback]', address)); await server.close(); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 84b838f7e121..41c9cd0f0638 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -7,7 +7,7 @@ import { mkdirp, walk } from '../../utils/filesystem.js'; import { noop } from '../../utils/functions.js'; import { decode_uri, is_root_relative, resolve } from '../../utils/url.js'; import { escape_for_regexp, escape_html } from '../../utils/escape.js'; -import { get_port, logger } from '../utils.js'; +import { get_address, logger } from '../utils.js'; import { get_route_segments } from '../../utils/routing.js'; import { queue } from './queue.js'; import { crawl } from './crawl.js'; @@ -252,7 +252,7 @@ async function prerender({ out, manifest_path, metadata, verbose, root, vite_con }; server.environments.ssr.hot.on(event, handle_dependencies); - const response = await fetch(`http://localhost:${port}${encoded}`, { redirect: 'manual' }); + const response = await fetch(address + encoded, { redirect: 'manual' }); const encoded_id = response.headers.get('x-sveltekit-routeid'); const decoded_id = encoded_id && decode_uri(encoded_id); @@ -510,7 +510,7 @@ async function prerender({ out, manifest_path, metadata, verbose, root, vite_con return () => { vite.middlewares.use((req, res, next) => { req.url = req.url?.replace( - new RegExp(escape_for_regexp(`^http://localhost:${port}`)), + new RegExp(escape_for_regexp(`^${address}`)), prerender_origin ); req.headers.host = new URL(prerender_origin).host; @@ -571,7 +571,7 @@ async function prerender({ out, manifest_path, metadata, verbose, root, vite_con // that we don't run the user's `init` hook unnecessarily await server.listen(); - const port = get_port(server); + const address = get_address(server); for (const entry of svelte_config.kit.prerender.entries) { if (entry === '*') { @@ -599,7 +599,7 @@ async function prerender({ out, manifest_path, metadata, verbose, root, vite_con const url = new URL( `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}/prerender-functions`, - `http://localhost:${port}` + address ); for (const name of metadata.remotes_with_prerender) { url.searchParams.append('name', name); diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js index b5b814cea237..3f63be89130d 100644 --- a/packages/kit/src/exports/vite/build/vite_server.js +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -17,7 +17,7 @@ import { remove_static_middlewares } from '../dev/index.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; -import { get_port } from '../../../core/utils.js'; +import { get_address } from '../../../core/utils.js'; import { dedent } from '../../../core/sync/utils.js'; import { s } from '../../../utils/misc.js'; import { import_peer } from '../../../utils/import.js'; @@ -58,8 +58,8 @@ export async function create_build_server({ (plugin) => plugin.name === 'vite-plugin-sveltekit-adapter' )?.api?.adapter; - /** @type {number | undefined} */ - let port; + /** @type {string | undefined} */ + let current_address; const app_path = `${svelte_config.kit.paths.base}/${svelte_config.kit.appDir}`; @@ -78,11 +78,11 @@ export async function create_build_server({ configureServer(server) { return () => { server.middlewares.use((_req, _res, next) => { - // ensure the server port is up-to-date - const current_port = get_port(server); - if (current_port && current_port !== port) { - port = current_port; - server.environments.ssr.hot.send('sveltekit:port', port); + // ensure the server address is up-to-date + const address = get_address(server); + if (address && address !== current_address) { + current_address = address; + server.environments.ssr.hot.send('sveltekit:server-address', current_address); invalidate_module(server, sveltekit_ipc); } @@ -111,11 +111,11 @@ export async function create_build_server({ const native_fetch = globalThis.fetch; export function get(pathname) { - return native_fetch(\`http://localhost:\${port}${app_path}\${pathname}\`); + return native_fetch(\`\${address}${app_path}\${pathname}\`); } - let port${port ? ` = ${port}` : ''}; - import.meta.hot?.on('sveltekit:port', (update) => { port = update }); + let address${current_address ? ` = '${current_address}'` : ''}; + import.meta.hot?.on('sveltekit:server-address', (update) => { address = update }); `; } } From eff595c7bd4ac83f814f62f9cc9f7d5d3719f12e Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Wed, 15 Jul 2026 05:24:09 +0800 Subject: [PATCH 10/11] trailing slash --- packages/kit/src/core/postbuild/prerender.js | 2 +- packages/kit/src/exports/vite/build/vite_server.js | 2 +- packages/kit/src/exports/vite/index.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index 41c9cd0f0638..f0239e4da075 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -252,7 +252,7 @@ async function prerender({ out, manifest_path, metadata, verbose, root, vite_con }; server.environments.ssr.hot.on(event, handle_dependencies); - const response = await fetch(address + encoded, { redirect: 'manual' }); + const response = await fetch(address + encoded.slice(1), { redirect: 'manual' }); const encoded_id = response.headers.get('x-sveltekit-routeid'); const decoded_id = encoded_id && decode_uri(encoded_id); diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js index 3f63be89130d..212c734bdc0e 100644 --- a/packages/kit/src/exports/vite/build/vite_server.js +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -111,7 +111,7 @@ export async function create_build_server({ const native_fetch = globalThis.fetch; export function get(pathname) { - return native_fetch(\`\${address}${app_path}\${pathname}\`); + return native_fetch(\`\${address}${app_path.slice(1)}\${pathname}\`); } let address${current_address ? ` = '${current_address}'` : ''}; diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index e3f6e1b1671d..0cb0ec4feaa1 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -764,7 +764,7 @@ function kit({ svelte_config }) { const native_fetch = globalThis.fetch; export function get(pathname) { - return native_fetch(\`\${address}${app_path}\${pathname}\`); + return native_fetch(\`\${address}${app_path.slice(1)}\${pathname}\`); } let address${address ? ` = '${address}'` : ''}; From c5e9b9ebfaabc9951861b68cbc99f37bb703d4cc Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Wed, 15 Jul 2026 18:40:27 +0800 Subject: [PATCH 11/11] use url constructor --- packages/kit/src/exports/vite/build/vite_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/build/vite_server.js b/packages/kit/src/exports/vite/build/vite_server.js index 212c734bdc0e..d15af94ffcf2 100644 --- a/packages/kit/src/exports/vite/build/vite_server.js +++ b/packages/kit/src/exports/vite/build/vite_server.js @@ -111,7 +111,7 @@ export async function create_build_server({ const native_fetch = globalThis.fetch; export function get(pathname) { - return native_fetch(\`\${address}${app_path.slice(1)}\${pathname}\`); + return native_fetch(new URL(\`${app_path}\${pathname}\`, address)); } let address${current_address ? ` = '${current_address}'` : ''};