From e2b1e45053b65407da4facd9f5fa193ee5a12488 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 4 Aug 2026 02:39:10 +0800 Subject: [PATCH 01/15] fix and changeset --- .changeset/twenty-actors-relax.md | 5 +++ packages/kit/src/exports/vite/index.js | 58 ++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .changeset/twenty-actors-relax.md diff --git a/.changeset/twenty-actors-relax.md b/.changeset/twenty-actors-relax.md new file mode 100644 index 000000000000..a5b85aaf3ad0 --- /dev/null +++ b/.changeset/twenty-actors-relax.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly build when the `--watch` flag is enabled diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0b0374c4068a..d2b5bc90e608 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1224,6 +1224,10 @@ function kit({ svelte_config }) { } }; + /** @type {Array} */ + let server_chunks_from_watched_build; + /** @type {Array} */ + let client_chunks_from_watched_build; /** @type {() => Promise | undefined} */ let finalise; @@ -1366,6 +1370,12 @@ function kit({ svelte_config }) { } handler(warning); + }, + watch: { + exclude: [ + // Ignore all siblings of config.outDir/generated + `${out_dir}/generated/**` + ] } }, emptyOutDir: false, @@ -1542,7 +1552,15 @@ function kit({ svelte_config }) { } }, - generateBundle() { + generateBundle(_options, bundle) { + if (this.meta.watchMode) { + if (this.environment.name === 'ssr') { + server_chunks_from_watched_build = Object.values(bundle); + } else if (this.environment.name === 'client') { + client_chunks_from_watched_build = Object.values(bundle); + } + } + if (this.environment.config.consumer !== 'client') return; this.emitFile({ @@ -1565,9 +1583,8 @@ function kit({ svelte_config }) { root }); - const { output: server_chunks } = /** @type {Rolldown.RolldownOutput} */ ( - await builder.build(builder.environments.ssr) - ); + const server_build = await builder.build(builder.environments.ssr); + const server_chunks = await normalise_build(server_build, server_chunks_from_watched_build); // Replace manifest placeholders in SSR output. `assets` and `routes` // are known from `manifest_data`. `immutable` and `prerendered` are not @@ -1668,9 +1685,8 @@ function kit({ svelte_config }) { s(has_universal_load); } - const { output: client_chunks } = /** @type {Rolldown.RolldownOutput} */ ( - await builder.build(builder.environments.client) - ); + const client_build = await builder.build(builder.environments.client); + const client_chunks = await normalise_build(client_build, client_chunks_from_watched_build); // We use `build.ssrEmitAssets` so that asset URLs created from // imports in server-only modules correspond to files in the build, @@ -2287,3 +2303,31 @@ const replace_manifest_placeholder_strings = (dir, values) => { } } }; + +/** + * @param {Rolldown.RolldownOutput | Rolldown.RolldownOutput[] | Rolldown.RolldownWatcher} build The return value of builder.build + * @param {Array} bundle_from_watched_build The bundle returned from the generateBundle hook during a build with watch mode + * @returns {Promise} The output chunks from a build + */ +async function normalise_build(build, bundle_from_watched_build) { + if ('output' in build) { + return build.output; + } + + if (Array.isArray(build)) { + return build[0].output; + } + + /** @type {PromiseWithResolvers} */ + const bundle = Promise.withResolvers(); + + build.on('event', (event) => { + if (event.code === 'BUNDLE_END' || event.code === 'ERROR') { + bundle.resolve(); + } + }); + + await bundle.promise; + + return /** @type {Rolldown.RolldownOutput['output']} */ (bundle_from_watched_build); +} From 849b73d34eb9cb2fa0e154230be03e606b9ea6f8 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 4 Aug 2026 03:20:41 +0800 Subject: [PATCH 02/15] fix --- packages/kit/src/exports/vite/index.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index d2b5bc90e608..3f1d7912ffcc 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1224,9 +1224,9 @@ function kit({ svelte_config }) { } }; - /** @type {Array} */ + /** @type {Rolldown.RolldownOutput['output']} */ let server_chunks_from_watched_build; - /** @type {Array} */ + /** @type {Rolldown.RolldownOutput['output']} */ let client_chunks_from_watched_build; /** @type {() => Promise | undefined} */ let finalise; @@ -1555,9 +1555,13 @@ function kit({ svelte_config }) { generateBundle(_options, bundle) { if (this.meta.watchMode) { if (this.environment.name === 'ssr') { - server_chunks_from_watched_build = Object.values(bundle); + server_chunks_from_watched_build = /** @type {Rolldown.RolldownOutput['output']} */ ( + Object.values(bundle) + ); } else if (this.environment.name === 'client') { - client_chunks_from_watched_build = Object.values(bundle); + client_chunks_from_watched_build = /** @type {Rolldown.RolldownOutput['output']} */ ( + Object.values(bundle) + ); } } @@ -1584,7 +1588,8 @@ function kit({ svelte_config }) { }); const server_build = await builder.build(builder.environments.ssr); - const server_chunks = await normalise_build(server_build, server_chunks_from_watched_build); + const server_chunks = + (await normalise_build(server_build)) ?? server_chunks_from_watched_build; // Replace manifest placeholders in SSR output. `assets` and `routes` // are known from `manifest_data`. `immutable` and `prerendered` are not @@ -1686,7 +1691,8 @@ function kit({ svelte_config }) { } const client_build = await builder.build(builder.environments.client); - const client_chunks = await normalise_build(client_build, client_chunks_from_watched_build); + const client_chunks = + (await normalise_build(client_build)) ?? client_chunks_from_watched_build; // We use `build.ssrEmitAssets` so that asset URLs created from // imports in server-only modules correspond to files in the build, @@ -2306,10 +2312,9 @@ const replace_manifest_placeholder_strings = (dir, values) => { /** * @param {Rolldown.RolldownOutput | Rolldown.RolldownOutput[] | Rolldown.RolldownWatcher} build The return value of builder.build - * @param {Array} bundle_from_watched_build The bundle returned from the generateBundle hook during a build with watch mode - * @returns {Promise} The output chunks from a build + * @returns {Promise} The output chunks from the build or `null` if watch mode is enabled */ -async function normalise_build(build, bundle_from_watched_build) { +async function normalise_build(build) { if ('output' in build) { return build.output; } @@ -2329,5 +2334,5 @@ async function normalise_build(build, bundle_from_watched_build) { await bundle.promise; - return /** @type {Rolldown.RolldownOutput['output']} */ (bundle_from_watched_build); + return null; } From 136b9abd7eb0d829ea59af67ae5075f414b931df Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Sat, 8 Aug 2026 01:25:25 +0800 Subject: [PATCH 03/15] something like this? --- packages/kit/src/exports/vite/index.js | 876 ++++++++++++---------- playgrounds/basic/src/routes/+page.svelte | 2 +- 2 files changed, 479 insertions(+), 399 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 987bc2ad45be..cda609c8448b 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -69,6 +69,7 @@ import { should_ignore, has_children } from './static_analysis/utils.js'; import { process_config, split_config, validate_config } from '../../core/config/index.js'; import { treeshake_prerendered_remotes } from './build/remote.js'; import { get_runner } from '../../runner.js'; +import { adapt } from '../../core/adapt/index.js'; /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { @@ -274,8 +275,8 @@ function kit({ svelte_config }) { /** @type {ManifestData} */ let manifest_data; - /** @type {ServerMetadata | undefined} only set at build time once analysis is finished */ - let build_metadata = undefined; + /** @type {ServerMetadata | null} set at build time once analysis has finished */ + let build_metadata = null; /** @type {UserConfig} */ let initial_config; @@ -297,7 +298,7 @@ function kit({ svelte_config }) { * in which chunks, so that we can later determine which routes use which features * @type {Record} */ - const tracked_features = {}; + let tracked_features = {}; const sourcemapIgnoreList = /** @param {string} relative_path */ (relative_path) => relative_path.includes('node_modules') || relative_path.includes(kit.outDir); @@ -338,7 +339,7 @@ function kit({ svelte_config }) { const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }); if (resolved) return resolved; - const aliases = svelte_config.kit.alias; + const aliases = kit.alias; for (const { name, pattern, message } of removed_modules) { if (!pattern.test(id)) continue; @@ -881,7 +882,7 @@ function kit({ svelte_config }) { let dev_server; /** @type {RemoteChunk[]} */ - const remotes = []; + let remotes = []; /** @type {Map} Maps remote hash -> original module id */ const remote_original_by_hash = new Map(); @@ -894,7 +895,7 @@ function kit({ svelte_config }) { name: 'vite-plugin-sveltekit-remote', applyToEnvironment(environment) { - return svelte_config.kit.experimental.remoteFunctions && environment.name !== 'serviceWorker'; + return kit.experimental.remoteFunctions && environment.name !== 'serviceWorker'; }, // prevent other plugins from resolving our remote virtual module @@ -1046,7 +1047,7 @@ function kit({ svelte_config }) { name: 'vite-plugin-sveltekit-remote-guard', applyToEnvironment() { - return !svelte_config.kit.experimental.remoteFunctions; + return !kit.experimental.remoteFunctions; }, transform: { @@ -1068,10 +1069,10 @@ function kit({ svelte_config }) { /** @type {Prerendered} */ let prerendered; - /** @type {Array<{ path: string }>} */ - let immutable; - /** @type {string} */ - let manifest_data_code; + /** @type {Array<{ path: string }> | null} */ + let immutable = null; + /** @type {string | null} */ + let manifest_data_code = null; /** * Creates the service worker virtual modules @@ -1268,8 +1269,8 @@ function kit({ svelte_config }) { let server_chunks_from_watched_build; /** @type {Rolldown.RolldownOutput['output']} */ let client_chunks_from_watched_build; - /** @type {() => Promise | undefined} */ - let finalise; + /** @type {(() => Promise) | null} */ + let finalise = null; /** @type {Plugin} */ const plugin_compile = { @@ -1352,7 +1353,7 @@ function kit({ svelte_config }) { /** @type {Record} */ const client_input = {}; - if (svelte_config.kit.output.bundleStrategy !== 'split') { + if (kit.output.bundleStrategy !== 'split') { client_input['bundle'] = `${runtime_directory}/client/bundle.js`; } else { client_input['entry/start'] = `${runtime_directory}/client/entry.js`; @@ -1365,7 +1366,7 @@ function kit({ svelte_config }) { }); } - const inline = svelte_config.kit.output.bundleStrategy === 'inline'; + const inline = kit.output.bundleStrategy === 'inline'; /** @type {string} */ const base = (kit.paths.assets || kit.paths.base) + '/'; @@ -1464,7 +1465,7 @@ function kit({ svelte_config }) { return `${app_immutable}/chunks/[hash].js`; }, codeSplitting: - svelte_config.kit.output.bundleStrategy === 'split' + kit.output.bundleStrategy === 'split' ? { groups: [ { @@ -1489,7 +1490,7 @@ function kit({ svelte_config }) { }, define: { __SVELTEKIT_PAYLOAD__: - svelte_config.kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined' + kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined' } } }, @@ -1616,449 +1617,520 @@ function kit({ svelte_config }) { }, async buildApp(builder) { - // clears the output directories - if (!builder.config.build.watch) { - fs.rmSync(out, { force: true, recursive: true }); - } - fs.mkdirSync(out, { recursive: true }); - - await load_and_validate_params({ - routes: manifest_data.routes, - params_path: manifest_data.params, - root - }); - - const server_build = await builder.build(builder.environments.ssr); - const server_chunks = - (await normalise_build(server_build)) ?? server_chunks_from_watched_build; - - // Replace manifest placeholders in SSR output. `assets` and `routes` - // are known from `manifest_data`. `immutable` and `prerendered` are not - // known yet — they get sentinel strings that are replaced after - // the client build and after prerendering respectively. - replace_manifest_placeholder_variables(server_chunks, `${out}/server`, { - assets: manifest_data.assets.map((asset) => ({ path: asset.file })), - routes: get_manifest_routes(manifest_data.routes) - }); + fs.rmSync(out, { force: true, recursive: true }); const verbose = builder.config.logLevel === 'info'; const log = logger({ verbose }); - /** @type {Manifest} */ - vite_server_manifest = JSON.parse(read(`${out}/server/.vite/manifest.json`)); - - /** @type {BuildData} */ - const build_data = { - app_dir: kit.appDir, - app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, - manifest_data, - out_dir: out, - service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? - client: null, - server_manifest: vite_server_manifest - }; - - const manifest_path = `${out}/server/manifest-full.js`; - fs.writeFileSync( - manifest_path, - `export const manifest = ${generate_manifest({ - build_data, - prerendered: [], - relative_path: '.', - routes: manifest_data.routes, - remotes, - root - })};\n` - ); - - const assets_path = `${kit.appDir}/immutable/assets`; - - // first, build server nodes without the client manifest so we can analyse it - build_server_nodes( - out, - kit, - manifest_data, - vite_server_manifest, - null, - assets_path, - server_chunks, - root - ); + /** @param {Rolldown.RolldownOutput['output']} server_chunks */ + const process_server_build = async (server_chunks) => { + // Replace manifest placeholders in SSR output. `assets` and `routes` + // are known from `manifest_data`. `immutable` and `prerendered` are not + // known yet — they get sentinel strings that are replaced after + // the client build and after prerendering respectively. + replace_manifest_placeholder_variables(server_chunks, `${out}/server`, { + assets: manifest_data.assets.map((asset) => ({ path: asset.file })), + routes: get_manifest_routes(manifest_data.routes) + }); - 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, - remotes, - vite_config_file: vite_config.configFile - }); + vite_server_manifest = /** @type {Manifest} */ ( + JSON.parse(read(`${out}/server/.vite/manifest.json`)) + ); - build_metadata = metadata; + const manifest_path = `${out}/server/manifest-full.js`; + const assets_path = `${kit.appDir}/immutable/assets`; - log.info('Building app'); + /** @type {BuildData} */ + const build_data = { + app_dir: kit.appDir, + app_path: `${kit.paths.base.slice(1)}${kit.paths.base ? '/' : ''}${kit.appDir}`, + manifest_data, + out_dir: out, + service_worker: service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable? + client: null, + server_manifest: vite_server_manifest + }; - // create client build - write_client_manifest( - kit, - manifest_data, - `${out_dir}/generated/client-optimized`, - metadata.nodes - ); + fs.writeFileSync( + manifest_path, + `export const manifest = ${generate_manifest({ + build_data, + prerendered: [], + relative_path: '.', + routes: manifest_data.routes, + remotes, + root + })};\n` + ); - const server_assets = `${out}/server/${assets_path}`; - const client_assets = `${out}/client/${assets_path}`; + // first, build server nodes without the client manifest so we can analyse it + build_server_nodes( + out, + kit, + manifest_data, + vite_server_manifest, + null, + assets_path, + server_chunks, + root + ); - const skip_client_build = manifest_data.nodes.every( - (node) => node.page_options?.csr === false - ); + log.info('Analysing routes'); - if (!skip_client_build) { - const nodes = Object.values(build_metadata.nodes); + const { metadata } = await analyse({ + hash: kit.router.type === 'hash', + manifest_path, + manifest_data, + server_manifest: vite_server_manifest, + tracked_features, + env, + remotes, + vite_config_file: vite_config.configFile + }); + build_metadata = metadata; - // Through the finished analysis we can now check if any node has server or universal load functions - const has_server_load = nodes.some((node) => node.has_server_load); - const has_universal_load = nodes.some((node) => node.has_universal_load); + log.info('Building app'); - if (builder.environments.client.config.define) { - builder.environments.client.config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = - s(has_server_load); - builder.environments.client.config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = - s(has_universal_load); - } + const server_assets = `${out}/server/${assets_path}`; + const client_assets = `${out}/client/${assets_path}`; - const client_build = await builder.build(builder.environments.client); - const client_chunks = - (await normalise_build(client_build)) ?? client_chunks_from_watched_build; - - // We use `build.ssrEmitAssets` so that asset URLs created from - // imports in server-only modules correspond to files in the build, - // but we don't want to copy over CSS imports as these are already - // accounted for in the client bundle. In most cases it would be - // a no-op, but for SSR builds `url(...)` paths are handled - // differently (relative for client, absolute for server) - // resulting in different hashes, and thus duplication - const ssr_stylesheets = new Set( - Object.values(vite_server_manifest) - .map((chunk) => chunk.css ?? []) - .flat() + const skip_client_build = manifest_data.nodes.every( + (node) => node.page_options?.csr === false ); - if (fs.existsSync(server_assets)) { - for (const file of fs.readdirSync(server_assets)) { - const src = `${server_assets}/${file}`; - const dest = `${client_assets}/${file}`; + if (skip_client_build) { + copy(server_assets, client_assets); + copy(kit.files.assets, `${out}/client`); + } else { + // ...and build the client + write_client_manifest( + kit, + manifest_data, + `${out_dir}/generated/client-optimized`, + build_metadata.nodes + ); - if (fs.existsSync(dest) || ssr_stylesheets.has(`${assets_path}/${file}`)) { - continue; - } + // Through the finished analysis we can now check if any node has server or universal load functions + const nodes = Object.values(build_metadata.nodes); + const has_server_load = nodes.some((node) => node.has_server_load); + const has_universal_load = nodes.some((node) => node.has_universal_load); - copy(src, dest); + if (builder.environments.client.config.define) { + builder.environments.client.config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = + s(has_server_load); + builder.environments.client.config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = + s(has_universal_load); } - } - const vite_manifest = (vite_client_manifest = JSON.parse( - read(`${out}/client/.vite/manifest.json`) - )); + const client_build = await builder.build(builder.environments.client); + const client_chunks = + (await normalise_build(client_build)) ?? client_chunks_from_watched_build; + + // We use `build.ssrEmitAssets` so that asset URLs created from + // imports in server-only modules correspond to files in the build, + // but we don't want to copy over CSS imports as these are already + // accounted for in the client bundle. In most cases it would be + // a no-op, but for SSR builds `url(...)` paths are handled + // differently (relative for client, absolute for server) + // resulting in different hashes, and thus duplication + const ssr_stylesheets = new Set( + Object.values(vite_server_manifest) + .map((chunk) => chunk.css ?? []) + .flat() + ); - /** - * @param {string} entry - * @param {boolean} [add_dynamic_css] - */ - const deps_of = (entry, add_dynamic_css = false) => - find_deps(vite_manifest, posixify(path.relative(root, entry)), add_dynamic_css, root); + if (fs.existsSync(server_assets)) { + for (const file of fs.readdirSync(server_assets)) { + const src = `${server_assets}/${file}`; + const dest = `${client_assets}/${file}`; - // the inline bundle and stylesheet are deleted further down, after - // being inlined into the page, so they must not appear in `immutable` - /** @type {Set} */ - const inlined = new Set(); - /** @type {Rolldown.OutputAsset | undefined} */ - let inline_style; - - if (kit.output.bundleStrategy === 'inline') { - inline_style = /** @type {Rolldown.OutputAsset | undefined} */ ( - client_chunks.find( - (chunk) => - chunk.type === 'asset' && chunk.names.length === 1 && chunk.names[0] === 'style.css' - ) + if (fs.existsSync(dest) || ssr_stylesheets.has(`${assets_path}/${file}`)) { + continue; + } + + copy(src, dest); + } + } + + vite_client_manifest = /** @type {Manifest} */ ( + JSON.parse(read(`${out}/client/.vite/manifest.json`)) ); - inlined.add(deps_of(`${runtime_directory}/client/bundle.js`).file); - if (inline_style) inlined.add(inline_style.fileName); - } + /** + * @param {string} entry + * @param {boolean} [add_dynamic_css] + */ + const deps_of = (entry, add_dynamic_css = false) => + find_deps( + /** @type {Manifest} */ (vite_client_manifest), + posixify(path.relative(root, entry)), + add_dynamic_css, + root + ); - // Replace manifest placeholders in client output. `immutable` is - // computed from the Vite client manifest, `assets` and `routes` - // from `manifest_data`. `prerendered` is left as a placeholder - // for now — it's replaced after prerendering completes. - immutable = collect_immutable(vite_manifest, kit.appDir, inlined); + // the inline bundle and stylesheet are deleted further down, after + // being inlined into the page, so they must not appear in `immutable` + /** @type {Set} */ + const inlined = new Set(); + /** @type {Rolldown.OutputAsset | undefined} */ + let inline_style; + + if (kit.output.bundleStrategy === 'inline') { + inline_style = /** @type {Rolldown.OutputAsset | undefined} */ ( + client_chunks.find( + (chunk) => + chunk.type === 'asset' && + chunk.names.length === 1 && + chunk.names[0] === 'style.css' + ) + ); - replace_manifest_placeholder_variables(client_chunks, `${out}/client`, { - immutable, - assets: manifest_data.assets.map((asset) => ({ path: asset.file })), - routes: get_manifest_routes(manifest_data.routes) - }); + inlined.add(deps_of(`${runtime_directory}/client/bundle.js`).file); + if (inline_style) inlined.add(inline_style.fileName); + } - // Now that the client build is done, replace the `build` sentinel - // in the SSR output with the real build files - replace_manifest_placeholder_strings(`${out}/server`, { immutable }); + // Replace manifest placeholders in client output. `immutable` is + // computed from the Vite client manifest, `assets` and `routes` + // from `manifest_data`. `prerendered` is left as a placeholder + // for now — it's replaced after prerendering completes. + immutable = collect_immutable(vite_client_manifest, kit.appDir, inlined); - const has_explicit_dynamic_public_env = Object.values(explicit_env_config ?? {}).some( - (variable) => variable.public && !variable.static - ); + replace_manifest_placeholder_variables(client_chunks, `${out}/client`, { + immutable, + assets: manifest_data.assets.map((asset) => ({ path: asset.file })), + routes: get_manifest_routes(manifest_data.routes) + }); + + // Now that the client build is done, replace the `build` sentinel + // in the SSR output with the real build files + replace_manifest_placeholder_strings(`${out}/server`, { immutable }); - // the app only depends on runtime public env if it imports `$app/env/public` - // *and* at least one public env var is actually dynamic (non-static) - const uses_env_dynamic_public = - has_explicit_dynamic_public_env && - client_chunks.some( - (chunk) => chunk.type === 'chunk' && chunk.modules[sveltekit_env_public_client] + const has_explicit_dynamic_public_env = Object.values(explicit_env_config ?? {}).some( + (variable) => variable.public && !variable.static ); - if (svelte_config.kit.output.bundleStrategy === 'split') { - const start_entry = posixify(path.relative(root, `${runtime_directory}/client/entry.js`)); - const start = find_deps(vite_manifest, start_entry, false, root); - const runtime_entry = resolve_symlinks(vite_manifest, start_entry, root).chunk - .dynamicImports?.[0]; // client/entry.js dynamically imports client/client-entry.js - if (!runtime_entry) throw new Error('Could not find the client runtime chunk'); - const runtime = find_deps(vite_manifest, runtime_entry, false, root); - const app = deps_of(`${out_dir}/generated/client-optimized/app.js`); - - build_data.client = { - start: start.file, - app: app.file, - imports: Array.from( - new Set([ - ...start.imports, - runtime.file, - ...runtime.imports, - app.file, - ...app.imports - ]) - ), - stylesheets: [...start.stylesheets, ...runtime.stylesheets, ...app.stylesheets], - fonts: [...start.fonts, ...runtime.fonts, ...app.fonts], - uses_env_dynamic_public - }; + // the app only depends on runtime public env if it imports `$app/env/public` + // *and* at least one public env var is actually dynamic (non-static) + const uses_env_dynamic_public = + has_explicit_dynamic_public_env && + client_chunks.some( + (chunk) => chunk.type === 'chunk' && chunk.modules[sveltekit_env_public_client] + ); - // In case of server-side route resolution, we create a purpose-built route manifest that is - // similar to that on the client, with as much information computed upfront so that we - // don't need to include any code of the actual routes in the server bundle. - if (svelte_config.kit.router.resolution === 'server') { - const nodes = manifest_data.nodes.map((node, i) => { - if (node.component || node.universal) { - const entry = `${out_dir}/generated/client-optimized/nodes/${i}.js`; - const deps = deps_of(entry, true); - const file = resolve_symlinks( - vite_manifest, - `${out_dir}/generated/client-optimized/nodes/${i}.js`, - root - ).chunk.file; - - return { file, css: deps.stylesheets }; - } - }); - build_data.client.nodes = nodes.map((node) => node?.file); - build_data.client.css = nodes.map((node) => node?.css); - - build_data.client.routes = compact( - manifest_data.routes.map((route) => { - if (!route.page) return; - - return { - id: route.id, - pattern: route.pattern, - params: route.params, - layouts: route.page.layouts.map((l) => - l !== undefined ? [metadata.nodes[l].has_server_load, l] : undefined - ), - errors: route.page.errors, - leaf: [metadata.nodes[route.page.leaf].has_server_load, route.page.leaf] - }; - }) + if (kit.output.bundleStrategy === 'split') { + const start_entry = posixify( + path.relative(root, `${runtime_directory}/client/entry.js`) ); - } - } else { - const start = deps_of(`${runtime_directory}/client/bundle.js`); - - build_data.client = { - start: start.file, - imports: start.imports, - stylesheets: start.stylesheets, - fonts: start.fonts, - uses_env_dynamic_public - }; + const start = find_deps(vite_client_manifest, start_entry, false, root); + const runtime_entry = resolve_symlinks(vite_client_manifest, start_entry, root).chunk + .dynamicImports?.[0]; // client/entry.js dynamically imports client/client-entry.js + if (!runtime_entry) throw new Error('Could not find the client runtime chunk'); + const runtime = find_deps(vite_client_manifest, runtime_entry, false, root); + const app = deps_of(`${out_dir}/generated/client-optimized/app.js`); + + build_data.client = { + start: start.file, + app: app.file, + imports: Array.from( + new Set([ + ...start.imports, + runtime.file, + ...runtime.imports, + app.file, + ...app.imports + ]) + ), + stylesheets: [...start.stylesheets, ...runtime.stylesheets, ...app.stylesheets], + fonts: [...start.fonts, ...runtime.fonts, ...app.fonts], + uses_env_dynamic_public + }; - if (svelte_config.kit.output.bundleStrategy === 'inline') { - build_data.client.inline = { - script: read(`${out}/client/${start.file}`), - style: /** @type {string | undefined} */ (inline_style?.source) + // In case of server-side route resolution, we create a purpose-built route manifest that is + // similar to that on the client, with as much information computed upfront so that we + // don't need to include any code of the actual routes in the server bundle. + if (kit.router.resolution === 'server') { + const nodes = manifest_data.nodes.map((node, i) => { + if (node.component || node.universal) { + const entry = `${out_dir}/generated/client-optimized/nodes/${i}.js`; + const deps = deps_of(entry, true); + const file = resolve_symlinks( + /** @type {Manifest} */ (vite_client_manifest), + `${out_dir}/generated/client-optimized/nodes/${i}.js`, + root + ).chunk.file; + + return { file, css: deps.stylesheets }; + } + }); + build_data.client.nodes = nodes.map((node) => node?.file); + build_data.client.css = nodes.map((node) => node?.css); + + build_data.client.routes = compact( + manifest_data.routes.map((route) => { + if (!route.page) return; + + return { + id: route.id, + pattern: route.pattern, + params: route.params, + layouts: route.page.layouts.map((l) => + l !== undefined ? [metadata.nodes[l].has_server_load, l] : undefined + ), + errors: route.page.errors, + leaf: [metadata.nodes[route.page.leaf].has_server_load, route.page.leaf] + }; + }) + ); + } + } else { + const start = deps_of(`${runtime_directory}/client/bundle.js`); + + build_data.client = { + start: start.file, + imports: start.imports, + stylesheets: start.stylesheets, + fonts: start.fonts, + uses_env_dynamic_public }; - // the bundle and stylesheet are inlined into the page, so the - // emitted files are never loaded - fs.unlinkSync(`${out}/client/${start.file}`); - fs.rmSync(`${out}/client/${start.file}.map`, { force: true }); - if (inline_style) fs.unlinkSync(`${out}/client/${inline_style.fileName}`); + if (kit.output.bundleStrategy === 'inline') { + build_data.client.inline = { + script: read(`${out}/client/${start.file}`), + style: /** @type {string | undefined} */ (inline_style?.source) + }; + + // the bundle and stylesheet are inlined into the page, so the + // emitted files are never loaded + fs.unlinkSync(`${out}/client/${start.file}`); + fs.rmSync(`${out}/client/${start.file}.map`, { force: true }); + if (inline_style) fs.unlinkSync(`${out}/client/${inline_style.fileName}`); + } + } + + // regenerate manifest now that we have client entry... + fs.writeFileSync( + manifest_path, + `export const manifest = ${generate_manifest({ + build_data, + prerendered: [], + relative_path: '.', + routes: manifest_data.routes, + remotes, + root + })};\n` + ); + + // regenerate nodes with the client manifest... + build_server_nodes( + out, + kit, + manifest_data, + vite_server_manifest, + vite_client_manifest, + assets_path, + client_chunks, + root + ); + } + + // ...and prerender + let prerender_results; + try { + prerender_results = await prerender({ + hash: kit.router.type === 'hash', + out, + manifest_path, + metadata, + verbose, + env, + vite_config_file: vite_config.configFile + }); + + // this silly hack is necessary to ensure that stderr from prerender is flushed before we continue + await new Promise((f) => setTimeout(f, 0)); + } catch (e) { + if (e instanceof Error && e.message === '__handled__') { + // error details are already logged inside `prerender`, don't duplicate them + throw stackless('Prerendering failed'); + } else { + // Unforeseen error, rethrow as-is + throw e; } } - // regenerate manifest now that we have client entry... + prerendered = prerender_results.prerendered; + + // Replace the `prerendered` sentinel in both SSR and client output + // with the real prerendered paths. The other sentinels (`build`) + // were already replaced after the client build. + const prerendered_paths = prerendered.paths.map((p) => { + return { path: p.replace(kit.paths.base, '').slice(1) }; + }); + + replace_manifest_placeholder_strings(`${out}/server`, { prerendered: prerendered_paths }); + replace_manifest_placeholder_strings(`${out}/client`, { prerendered: prerendered_paths }); + + // For `inline` strategy, the entry file was deleted and read into + // `build_data.client.inline.script` — replace the sentinel there too + if (build_data.client?.inline?.script) { + build_data.client.inline.script = build_data.client.inline.script.replaceAll( + '"__sveltekit_manifest_prerendered__"', + JSON.stringify(prerendered_paths) + ); + } + + // generate a new manifest that doesn't include prerendered pages fs.writeFileSync( - manifest_path, + `${out}/server/manifest.js`, `export const manifest = ${generate_manifest({ build_data, - prerendered: [], + prerendered: prerendered.paths, relative_path: '.', - routes: manifest_data.routes, + routes: manifest_data.routes.filter( + (route) => prerender_results.prerender_map.get(route.id) !== true + ), remotes, root })};\n` ); - // regenerate nodes with the client manifest... - build_server_nodes( + await treeshake_prerendered_remotes( + vite, out, - kit, - manifest_data, - vite_server_manifest, - vite_client_manifest, - assets_path, - client_chunks, - root + remotes, + remote_original_by_hash, + metadata, + process.cwd(), + server_chunks, + vite_config.build.sourcemap ); - } else { - copy(server_assets, client_assets); - copy(kit.files.assets, `${out}/client`); - } - // ...and prerender - let prerender_results; - try { - prerender_results = await prerender({ - hash: kit.router.type === 'hash', - out, - manifest_path, - metadata, - verbose, - env, - vite_config_file: vite_config.configFile - }); + // defer until after other buildApp hooks have run + finalise = async () => { + // defer creating the service worker to avoid other plugins from + // overwriting it if they run a client environment build + if (service_worker_entry_file) { + log.info('Building service worker'); + + // mirror client settings that we couldn't set per environment in the config hook + builder.environments.serviceWorker.config.define = + builder.environments.client.config.define; + builder.environments.serviceWorker.config.resolve.alias = [ + ...get_config_aliases(kit, vite_config.root) + ]; + + // we have to overwrite this because it can't be configured per environment in the config hook + builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { + return { + runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` + }; + }; - // this silly hack is necessary to ensure that stderr from prerender is flushed before we continue - await new Promise((f) => setTimeout(f, 0)); - } catch (e) { - if (e instanceof Error && e.message === '__handled__') { - // error details are already logged inside `prerender`, don't duplicate them - throw stackless('Prerendering failed'); - } else { - // Unforeseen error, rethrow as-is - throw e; - } - } + const service_worker_build = await builder.build(builder.environments.serviceWorker); + await normalise_build(service_worker_build); + } - prerendered = prerender_results.prerendered; + console.log( + `\nRun ${styleText(['bold', 'cyan'], 'npm run preview')} to preview your production build locally.` + ); - // Replace the `prerendered` sentinel in both SSR and client output - // with the real prerendered paths. The other sentinels (`build`) - // were already replaced after the client build. - const prerendered_paths = prerendered.paths.map((p) => { - return { path: p.replace(kit.paths.base, '').slice(1) }; - }); + if (kit.adapter) { + await adapt( + svelte_config, + build_data, + metadata, + prerendered, + prerender_results.prerender_map, + log, + remotes, + vite_config, + explicit_env_config + ); + } else { + log.warn('\nNo adapter specified'); - replace_manifest_placeholder_strings(`${out}/server`, { prerendered: prerendered_paths }); - replace_manifest_placeholder_strings(`${out}/client`, { prerendered: prerendered_paths }); + const link = styleText(['bold', 'cyan'], 'https://svelte.dev/docs/kit/adapters'); + console.log( + `See ${link} to learn how to configure your app to run on the platform of your choosing` + ); + } + }; + }; - // For `inline` strategy, the entry file was deleted and read into - // `build_data.client.inline.script` — replace the sentinel there too - if (build_data.client?.inline?.script) { - build_data.client.inline.script = build_data.client.inline.script.replaceAll( - '"__sveltekit_manifest_prerendered__"', - JSON.stringify(prerendered_paths) - ); - } + const server_build = await builder.build(builder.environments.ssr); - await treeshake_prerendered_remotes( - vite, - out, - remotes, - remote_original_by_hash, - metadata, - process.cwd(), - server_chunks, - vite_config.build.sourcemap - ); + // `vite build` + if ('output' in server_build || Array.isArray(server_build)) { + fs.mkdirSync(out, { recursive: true }); - // generate a new manifest that doesn't include prerendered pages - fs.writeFileSync( - `${out}/server/manifest.js`, - `export const manifest = ${generate_manifest({ - build_data, - prerendered: prerendered.paths, - relative_path: '.', - routes: manifest_data.routes.filter( - (route) => prerender_results.prerender_map.get(route.id) !== true - ), - remotes, + await load_and_validate_params({ + routes: manifest_data.routes, + params_path: manifest_data.params, root - })};\n` - ); + }); - // defer the adapt step to run after any buildApp hooks the adapter might have - finalise = async () => { - // defer creating the service worker too because other plugins might build - // the client environment again and overwrite our service worker which - // outputs to the same directory - if (service_worker_entry_file) { - log.info('Building service worker'); + const { output } = 'output' in server_build ? server_build : server_build[0]; + return await process_server_build(output); + } - // mirror client settings that we couldn't set per environment in the config hook - builder.environments.serviceWorker.config.define = - builder.environments.client.config.define; - builder.environments.serviceWorker.config.resolve.alias = [ - ...get_config_aliases(kit, vite_config.root) - ]; + let building_again = false; + + const clean_up = async () => { + building_again = true; + + // these are reset once per plugin initialisation or when the config hook + // runs. However, those don't re-run on watch mode, so we need to + // re-initialise them manually here + manifest_data = sync.all(svelte_config, root).manifest_data; + service_worker_entry_file = resolve_entry(kit.files.serviceWorker); + immutable = null; + tracked_features = {}; + remotes = []; + remote_original_by_hash.clear(); + emitted_remote_hashes.clear(); + explicit_env_entry = resolve_explicit_env_entry(kit); + explicit_env_config = await sync.env( + vite, + kit, + explicit_env_entry, + vite_config.root, + vite_config.mode + ); + import_map.clear(); + manifest_data_code = null; + finalise = null; - // we have to overwrite this because it can't be configured per environment in the config hook - builder.environments.serviceWorker.config.experimental.renderBuiltUrl = (filename) => { - return { - runtime: `new URL(${JSON.stringify(filename)}, location.href).pathname` - }; - }; + await load_and_validate_params({ + routes: manifest_data.routes, + params_path: manifest_data.params, + root + }); + }; - await builder.build(builder.environments.serviceWorker); - } + // `vite build --watch` + server_build.on('change', clean_up); + server_build.on('restart', clean_up); - console.log( - `\nRun ${styleText(['bold', 'cyan'], 'npm run preview')} to preview your production build locally.` - ); + /** @type {PromiseWithResolvers} */ + const task = Promise.withResolvers(); - if (kit.adapter) { - const { adapt } = await import('../../core/adapt/index.js'); - await adapt( - svelte_config, - build_data, - metadata, - prerendered, - prerender_results.prerender_map, - log, - remotes, - vite_config, - explicit_env_config - ); - } else { - log.warn('\nNo adapter specified'); + server_build.on('event', async (event) => { + if (event.code === 'BUNDLE_START') { + fs.mkdirSync(out, { recursive: true }); + return; + } - const link = styleText(['bold', 'cyan'], 'https://svelte.dev/docs/kit/adapters'); - console.log( - `See ${link} to learn how to configure your app to run on the platform of your choosing` - ); + if (event.code === 'BUNDLE_END') { + await process_server_build(server_chunks_from_watched_build); + // buildApp hooks don't rerun in watch mode so we need to run + // the deferred steps here on subsequent builds + if (building_again) await finalise?.(); + await event.result.close(); + return task.resolve(); } - }; + }); + + await task.promise; } }; @@ -2365,15 +2437,23 @@ async function normalise_build(build) { } /** @type {PromiseWithResolvers} */ - const bundle = Promise.withResolvers(); + const bundling = Promise.withResolvers(); + + build.on('event', async (event) => { + if (event.code === 'ERROR') { + return bundling.reject(event.error); + } + + if (event.code === 'BUNDLE_END') { + return bundling.resolve(); + } - build.on('event', (event) => { - if (event.code === 'BUNDLE_END' || event.code === 'ERROR') { - bundle.resolve(); + if (event.code === 'END') { + return await build.close(); } }); - await bundle.promise; + await bundling.promise; return null; } diff --git a/playgrounds/basic/src/routes/+page.svelte b/playgrounds/basic/src/routes/+page.svelte index e594d86f514b..028e49a7501e 100644 --- a/playgrounds/basic/src/routes/+page.svelte +++ b/playgrounds/basic/src/routes/+page.svelte @@ -1,4 +1,4 @@ -

hello!

+

hello world

From fccd6de812df29bca02bd357f139872edff66f91 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Sat, 8 Aug 2026 01:28:03 +0800 Subject: [PATCH 04/15] Update +page.svelte --- playgrounds/basic/src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/basic/src/routes/+page.svelte b/playgrounds/basic/src/routes/+page.svelte index 028e49a7501e..e594d86f514b 100644 --- a/playgrounds/basic/src/routes/+page.svelte +++ b/playgrounds/basic/src/routes/+page.svelte @@ -1,4 +1,4 @@ -

hello world

+

hello!

From c53be80e6e7549ea6a97eec3275baf0ef59402cc Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Sat, 8 Aug 2026 01:36:40 +0800 Subject: [PATCH 05/15] safer this way? --- packages/kit/src/exports/vite/index.js | 67 ++++++++++++++------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index cda609c8448b..732c97119a10 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2074,42 +2074,13 @@ function kit({ svelte_config }) { } let building_again = false; - - const clean_up = async () => { + const rerun = () => { building_again = true; - - // these are reset once per plugin initialisation or when the config hook - // runs. However, those don't re-run on watch mode, so we need to - // re-initialise them manually here - manifest_data = sync.all(svelte_config, root).manifest_data; - service_worker_entry_file = resolve_entry(kit.files.serviceWorker); - immutable = null; - tracked_features = {}; - remotes = []; - remote_original_by_hash.clear(); - emitted_remote_hashes.clear(); - explicit_env_entry = resolve_explicit_env_entry(kit); - explicit_env_config = await sync.env( - vite, - kit, - explicit_env_entry, - vite_config.root, - vite_config.mode - ); - import_map.clear(); - manifest_data_code = null; - finalise = null; - - await load_and_validate_params({ - routes: manifest_data.routes, - params_path: manifest_data.params, - root - }); }; // `vite build --watch` - server_build.on('change', clean_up); - server_build.on('restart', clean_up); + server_build.on('change', rerun); + server_build.on('restart', rerun); /** @type {PromiseWithResolvers} */ const task = Promise.withResolvers(); @@ -2117,9 +2088,41 @@ function kit({ svelte_config }) { server_build.on('event', async (event) => { if (event.code === 'BUNDLE_START') { fs.mkdirSync(out, { recursive: true }); + + // these are set once per plugin initialisation or when the config hook + // runs. However, those don't re-run during watch mode. So, we need to + // re-initialise them manually here + manifest_data = sync.all(svelte_config, root).manifest_data; + service_worker_entry_file = resolve_entry(kit.files.serviceWorker); + immutable = null; + tracked_features = {}; + remotes = []; + remote_original_by_hash.clear(); + emitted_remote_hashes.clear(); + explicit_env_entry = resolve_explicit_env_entry(kit); + explicit_env_config = await sync.env( + vite, + kit, + explicit_env_entry, + vite_config.root, + vite_config.mode + ); + import_map.clear(); + manifest_data_code = null; + finalise = null; + + await load_and_validate_params({ + routes: manifest_data.routes, + params_path: manifest_data.params, + root + }); return; } + if (event.code === 'ERROR') { + return task.reject(); + } + if (event.code === 'BUNDLE_END') { await process_server_build(server_chunks_from_watched_build); // buildApp hooks don't rerun in watch mode so we need to run From cadc58363abb730c5e119ea01ac6afdea292af66 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Sat, 8 Aug 2026 01:42:33 +0800 Subject: [PATCH 06/15] fix and add todos --- packages/kit/src/exports/vite/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 732c97119a10..9da63ed101af 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1722,6 +1722,7 @@ function kit({ svelte_config }) { s(has_universal_load); } + // TODO: handle rerun const client_build = await builder.build(builder.environments.client); const client_chunks = (await normalise_build(client_build)) ?? client_chunks_from_watched_build; @@ -2026,6 +2027,7 @@ function kit({ svelte_config }) { }; }; + // TODO: handle rerun const service_worker_build = await builder.build(builder.environments.serviceWorker); await normalise_build(service_worker_build); } @@ -2442,7 +2444,7 @@ async function normalise_build(build) { /** @type {PromiseWithResolvers} */ const bundling = Promise.withResolvers(); - build.on('event', async (event) => { + build.on('event', (event) => { if (event.code === 'ERROR') { return bundling.reject(event.error); } @@ -2450,10 +2452,6 @@ async function normalise_build(build) { if (event.code === 'BUNDLE_END') { return bundling.resolve(); } - - if (event.code === 'END') { - return await build.close(); - } }); await bundling.promise; From 8f29fdccd32e7e4ee1ec1d766d51e858b3a6edb7 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 10 Aug 2026 23:28:40 +0800 Subject: [PATCH 07/15] only rebuild on server file changes --- packages/kit/src/exports/vite/index.js | 107 ++++++++++++++----------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 9da63ed101af..cc2c10f75286 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -875,6 +875,11 @@ function kit({ svelte_config }) { // No chain from this server-only module to a client entrypoint was found — // the module is only imported from server code, which is valid. } + }, + + // avoid watch mode rebuilds using stale import map data + buildEnd() { + import_map.clear(); } }; @@ -943,7 +948,7 @@ function kit({ svelte_config }) { if (this.environment.name === 'ssr') remotes.push(remote); - if (this.environment.config.consumer !== 'client') { + if (this.environment.config.consumer === 'server') { // we need to add an `await Promise.resolve()` because if the user imports this function // on the client AND in a load function when loading the client module we will trigger // an import during dev. During a link preload, the module can be mistakenly @@ -1039,6 +1044,12 @@ function kit({ svelte_config }) { map: null }; } + }, + + buildEnd() { + if (this.environment.config.consumer === 'server') { + emitted_remote_hashes.clear(); + } } }; @@ -1622,6 +1633,8 @@ function kit({ svelte_config }) { const verbose = builder.config.logLevel === 'info'; const log = logger({ verbose }); + let server_build = await builder.build(builder.environments.ssr); + /** @param {Rolldown.RolldownOutput['output']} server_chunks */ const process_server_build = async (server_chunks) => { // Replace manifest placeholders in SSR output. `assets` and `routes` @@ -1722,7 +1735,6 @@ function kit({ svelte_config }) { s(has_universal_load); } - // TODO: handle rerun const client_build = await builder.build(builder.environments.client); const client_chunks = (await normalise_build(client_build)) ?? client_chunks_from_watched_build; @@ -2027,7 +2039,6 @@ function kit({ svelte_config }) { }; }; - // TODO: handle rerun const service_worker_build = await builder.build(builder.environments.serviceWorker); await normalise_build(service_worker_build); } @@ -2059,10 +2070,9 @@ function kit({ svelte_config }) { }; }; - const server_build = await builder.build(builder.environments.ssr); - // `vite build` - if ('output' in server_build || Array.isArray(server_build)) { + server_build = Array.isArray(server_build) ? server_build[0] : server_build; + if ('output' in server_build) { fs.mkdirSync(out, { recursive: true }); await load_and_validate_params({ @@ -2071,56 +2081,53 @@ function kit({ svelte_config }) { root }); - const { output } = 'output' in server_build ? server_build : server_build[0]; - return await process_server_build(output); + return await process_server_build(server_build.output); } - let building_again = false; - const rerun = () => { - building_again = true; + // `vite build --watch` + let rebuild = false; + + const before_server_rerun = async () => { + rebuild = true; + + // these are set once per plugin initialisation or when the config hook + // runs. However, those don't re-run during watch mode. So, we need to + // re-initialise them manually here + manifest_data = sync.all(svelte_config, root).manifest_data; + + tracked_features = {}; + + remotes = []; + remote_original_by_hash.clear(); + emitted_remote_hashes.clear(); + + finalise = null; + + fs.mkdirSync(out, { recursive: true }); + + explicit_env_entry = resolve_explicit_env_entry(kit); + explicit_env_config = await sync.env( + vite, + kit, + explicit_env_entry, + vite_config.root, + vite_config.mode + ); + + await load_and_validate_params({ + routes: manifest_data.routes, + params_path: manifest_data.params, + root + }); }; - // `vite build --watch` - server_build.on('change', rerun); - server_build.on('restart', rerun); + server_build.on('change', before_server_rerun); + server_build.on('restart', before_server_rerun); /** @type {PromiseWithResolvers} */ const task = Promise.withResolvers(); server_build.on('event', async (event) => { - if (event.code === 'BUNDLE_START') { - fs.mkdirSync(out, { recursive: true }); - - // these are set once per plugin initialisation or when the config hook - // runs. However, those don't re-run during watch mode. So, we need to - // re-initialise them manually here - manifest_data = sync.all(svelte_config, root).manifest_data; - service_worker_entry_file = resolve_entry(kit.files.serviceWorker); - immutable = null; - tracked_features = {}; - remotes = []; - remote_original_by_hash.clear(); - emitted_remote_hashes.clear(); - explicit_env_entry = resolve_explicit_env_entry(kit); - explicit_env_config = await sync.env( - vite, - kit, - explicit_env_entry, - vite_config.root, - vite_config.mode - ); - import_map.clear(); - manifest_data_code = null; - finalise = null; - - await load_and_validate_params({ - routes: manifest_data.routes, - params_path: manifest_data.params, - root - }); - return; - } - if (event.code === 'ERROR') { return task.reject(); } @@ -2129,7 +2136,7 @@ function kit({ svelte_config }) { await process_server_build(server_chunks_from_watched_build); // buildApp hooks don't rerun in watch mode so we need to run // the deferred steps here on subsequent builds - if (building_again) await finalise?.(); + if (rebuild) await finalise?.(); await event.result.close(); return task.resolve(); } @@ -2444,12 +2451,14 @@ async function normalise_build(build) { /** @type {PromiseWithResolvers} */ const bundling = Promise.withResolvers(); - build.on('event', (event) => { + build.on('event', async (event) => { if (event.code === 'ERROR') { + await build.close(); return bundling.reject(event.error); } if (event.code === 'BUNDLE_END') { + await build.close(); return bundling.resolve(); } }); From db3160909e8e61026dcf879e1cdbac9d39addd26 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 10 Aug 2026 23:34:23 +0800 Subject: [PATCH 08/15] changeset --- .changeset/twenty-actors-relax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/twenty-actors-relax.md b/.changeset/twenty-actors-relax.md index a5b85aaf3ad0..4923d09caf46 100644 --- a/.changeset/twenty-actors-relax.md +++ b/.changeset/twenty-actors-relax.md @@ -2,4 +2,4 @@ '@sveltejs/kit': patch --- -fix: correctly build when the `--watch` flag is enabled +fix: avoid infinite loop when building with `--watch` flag From 14edb44bfa7979621eba56eb25956ee1d98c257b Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 10 Aug 2026 23:50:04 +0800 Subject: [PATCH 09/15] Update index.js --- packages/kit/src/exports/vite/index.js | 63 ++++++++++++++------------ 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index cc2c10f75286..302962244841 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -41,6 +41,7 @@ import { server_only_module_pattern } from './utils.js'; import { stackless } from '../../utils/error.js'; +import { adapt } from '../../core/adapt/index.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; @@ -69,7 +70,6 @@ import { should_ignore, has_children } from './static_analysis/utils.js'; import { process_config, split_config, validate_config } from '../../core/config/index.js'; import { treeshake_prerendered_remotes } from './build/remote.js'; import { get_runner } from '../../runner.js'; -import { adapt } from '../../core/adapt/index.js'; /** @type {import('./types.js').EnforcedConfig} */ const enforced_config = { @@ -339,7 +339,7 @@ function kit({ svelte_config }) { const resolved = await this.resolve(id, importer, { ...options, skipSelf: true }); if (resolved) return resolved; - const aliases = kit.alias; + const aliases = svelte_config.kit.alias; for (const { name, pattern, message } of removed_modules) { if (!pattern.test(id)) continue; @@ -900,7 +900,7 @@ function kit({ svelte_config }) { name: 'vite-plugin-sveltekit-remote', applyToEnvironment(environment) { - return kit.experimental.remoteFunctions && environment.name !== 'serviceWorker'; + return svelte_config.kit.experimental.remoteFunctions && environment.name !== 'serviceWorker'; }, // prevent other plugins from resolving our remote virtual module @@ -948,7 +948,7 @@ function kit({ svelte_config }) { if (this.environment.name === 'ssr') remotes.push(remote); - if (this.environment.config.consumer === 'server') { + if (this.environment.config.consumer !== 'client') { // we need to add an `await Promise.resolve()` because if the user imports this function // on the client AND in a load function when loading the client module we will trigger // an import during dev. During a link preload, the module can be mistakenly @@ -1058,7 +1058,7 @@ function kit({ svelte_config }) { name: 'vite-plugin-sveltekit-remote-guard', applyToEnvironment() { - return !kit.experimental.remoteFunctions; + return !svelte_config.kit.experimental.remoteFunctions; }, transform: { @@ -1276,10 +1276,8 @@ function kit({ svelte_config }) { } }; - /** @type {Rolldown.RolldownOutput['output']} */ - let server_chunks_from_watched_build; - /** @type {Rolldown.RolldownOutput['output']} */ - let client_chunks_from_watched_build; + /** @type {Map} */ + const watch_build_output = new Map(); /** @type {(() => Promise) | null} */ let finalise = null; @@ -1364,7 +1362,7 @@ function kit({ svelte_config }) { /** @type {Record} */ const client_input = {}; - if (kit.output.bundleStrategy !== 'split') { + if (svelte_config.kit.output.bundleStrategy !== 'split') { client_input['bundle'] = `${runtime_directory}/client/bundle.js`; } else { client_input['entry/start'] = `${runtime_directory}/client/entry.js`; @@ -1377,7 +1375,7 @@ function kit({ svelte_config }) { }); } - const inline = kit.output.bundleStrategy === 'inline'; + const inline = svelte_config.kit.output.bundleStrategy === 'inline'; /** @type {string} */ const base = (kit.paths.assets || kit.paths.base) + '/'; @@ -1476,7 +1474,7 @@ function kit({ svelte_config }) { return `${app_immutable}/chunks/[hash].js`; }, codeSplitting: - kit.output.bundleStrategy === 'split' + svelte_config.kit.output.bundleStrategy === 'split' ? { groups: [ { @@ -1501,7 +1499,7 @@ function kit({ svelte_config }) { }, define: { __SVELTEKIT_PAYLOAD__: - kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined' + svelte_config.kit.output.bundleStrategy !== 'split' ? kit_global : 'undefined' } } }, @@ -1606,16 +1604,13 @@ function kit({ svelte_config }) { }, generateBundle(_options, bundle) { + // a watched build returns a watcher rather than the build output from + // `builder.build` so we need to retrieve it from the generateBundle hook if (this.meta.watchMode) { - if (this.environment.name === 'ssr') { - server_chunks_from_watched_build = /** @type {Rolldown.RolldownOutput['output']} */ ( - Object.values(bundle) - ); - } else if (this.environment.name === 'client') { - client_chunks_from_watched_build = /** @type {Rolldown.RolldownOutput['output']} */ ( - Object.values(bundle) - ); - } + watch_build_output.set( + this.environment.name, + /** @type {Rolldown.RolldownOutput['output']} */ (Object.values(bundle)) + ); } if (this.environment.config.consumer !== 'client') return; @@ -1736,8 +1731,11 @@ function kit({ svelte_config }) { } const client_build = await builder.build(builder.environments.client); - const client_chunks = - (await normalise_build(client_build)) ?? client_chunks_from_watched_build; + const client_chunks = await normalise_build( + builder.environments.client.name, + client_build, + watch_build_output + ); // We use `build.ssrEmitAssets` so that asset URLs created from // imports in server-only modules correspond to files in the build, @@ -2040,7 +2038,11 @@ function kit({ svelte_config }) { }; const service_worker_build = await builder.build(builder.environments.serviceWorker); - await normalise_build(service_worker_build); + await normalise_build( + builder.environments.serviceWorker.name, + service_worker_build, + watch_build_output + ); } console.log( @@ -2133,7 +2135,7 @@ function kit({ svelte_config }) { } if (event.code === 'BUNDLE_END') { - await process_server_build(server_chunks_from_watched_build); + await process_server_build(watch_build_output.get(builder.environments.ssr.name)); // buildApp hooks don't rerun in watch mode so we need to run // the deferred steps here on subsequent builds if (rebuild) await finalise?.(); @@ -2436,10 +2438,13 @@ const replace_manifest_placeholder_strings = (dir, values) => { }; /** + * Normalises the build output to a consistent format, handling watch mode and multiple environments + * @param {string} name The name of the environment * @param {Rolldown.RolldownOutput | Rolldown.RolldownOutput[] | Rolldown.RolldownWatcher} build The return value of builder.build - * @returns {Promise} The output chunks from the build or `null` if watch mode is enabled + * @param {Map} build_output_map + * @returns {Promise} */ -async function normalise_build(build) { +async function normalise_build(name, build, build_output_map) { if ('output' in build) { return build.output; } @@ -2465,5 +2470,5 @@ async function normalise_build(build) { await bundling.promise; - return null; + return build_output_map.get(name); } From bb51a9350071e30da3a8240c38319051de89f792 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 10 Aug 2026 23:56:40 +0800 Subject: [PATCH 10/15] rename --- packages/kit/src/exports/vite/index.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 302962244841..151f72060f6c 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1624,14 +1624,15 @@ function kit({ svelte_config }) { async buildApp(builder) { fs.rmSync(out, { force: true, recursive: true }); + fs.mkdirSync(out, { recursive: true }); const verbose = builder.config.logLevel === 'info'; const log = logger({ verbose }); - let server_build = await builder.build(builder.environments.ssr); + let ssr_build = await builder.build(builder.environments.ssr); /** @param {Rolldown.RolldownOutput['output']} server_chunks */ - const process_server_build = async (server_chunks) => { + const process_ssr_build = async (server_chunks) => { // Replace manifest placeholders in SSR output. `assets` and `routes` // are known from `manifest_data`. `immutable` and `prerendered` are not // known yet — they get sentinel strings that are replaced after @@ -2073,23 +2074,21 @@ function kit({ svelte_config }) { }; // `vite build` - server_build = Array.isArray(server_build) ? server_build[0] : server_build; - if ('output' in server_build) { - fs.mkdirSync(out, { recursive: true }); - + ssr_build = Array.isArray(ssr_build) ? ssr_build[0] : ssr_build; + if ('output' in ssr_build) { await load_and_validate_params({ routes: manifest_data.routes, params_path: manifest_data.params, root }); - return await process_server_build(server_build.output); + return await process_ssr_build(ssr_build.output); } // `vite build --watch` let rebuild = false; - const before_server_rerun = async () => { + const before_ssr_build_rerun = async () => { rebuild = true; // these are set once per plugin initialisation or when the config hook @@ -2123,19 +2122,19 @@ function kit({ svelte_config }) { }); }; - server_build.on('change', before_server_rerun); - server_build.on('restart', before_server_rerun); + ssr_build.on('change', before_ssr_build_rerun); + ssr_build.on('restart', before_ssr_build_rerun); /** @type {PromiseWithResolvers} */ const task = Promise.withResolvers(); - server_build.on('event', async (event) => { + ssr_build.on('event', async (event) => { if (event.code === 'ERROR') { return task.reject(); } if (event.code === 'BUNDLE_END') { - await process_server_build(watch_build_output.get(builder.environments.ssr.name)); + await process_ssr_build(watch_build_output.get(builder.environments.ssr.name)); // buildApp hooks don't rerun in watch mode so we need to run // the deferred steps here on subsequent builds if (rebuild) await finalise?.(); From ad855b3f9352cebf7b699a143db04f0b256b6859 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 11 Aug 2026 00:06:18 +0800 Subject: [PATCH 11/15] Update index.js --- 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 151f72060f6c..c1b953ec3b51 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2441,7 +2441,7 @@ const replace_manifest_placeholder_strings = (dir, values) => { * @param {string} name The name of the environment * @param {Rolldown.RolldownOutput | Rolldown.RolldownOutput[] | Rolldown.RolldownWatcher} build The return value of builder.build * @param {Map} build_output_map - * @returns {Promise} + * @returns {Promise} */ async function normalise_build(name, build, build_output_map) { if ('output' in build) { From 0c4e7963b85a1eed5fa4a5d07e43be6bf70601d2 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 11 Aug 2026 00:07:18 +0800 Subject: [PATCH 12/15] fix type --- 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 c1b953ec3b51..ce1ee123ba5f 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2469,5 +2469,5 @@ async function normalise_build(name, build, build_output_map) { await bundling.promise; - return build_output_map.get(name); + return /** @type {Rolldown.RolldownOutput['output']} */ (build_output_map.get(name)); } From c7ff00373a6836628c7e37dd6d304dcf7c36f2a2 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 11 Aug 2026 00:29:41 +0800 Subject: [PATCH 13/15] fix lint --- packages/kit/src/exports/vite/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index ce1ee123ba5f..e1a654552e96 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2134,7 +2134,11 @@ function kit({ svelte_config }) { } if (event.code === 'BUNDLE_END') { - await process_ssr_build(watch_build_output.get(builder.environments.ssr.name)); + await process_ssr_build( + /** @type {Rolldown.RolldownOutput['output']} */ ( + watch_build_output.get(builder.environments.ssr.name) + ) + ); // buildApp hooks don't rerun in watch mode so we need to run // the deferred steps here on subsequent builds if (rebuild) await finalise?.(); From 59b10688dd406b4ba36f19dcbc0f0d398020ee0b Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 11 Aug 2026 09:49:53 +0800 Subject: [PATCH 14/15] oops --- packages/kit/src/exports/vite/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0e3ec148a81e..21992b6e1c78 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2113,6 +2113,9 @@ function kit({ svelte_config }) { remote_original_by_hash.clear(); emitted_remote_hashes.clear(); + immutable = null; + manifest_data_code = null; + finalise = null; fs.mkdirSync(out, { recursive: true }); From 0427c54f6d25bc6ab20cee63d07ea780611614f8 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Tue, 11 Aug 2026 09:53:56 +0800 Subject: [PATCH 15/15] reject promise if build error --- packages/kit/src/exports/vite/index.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 21992b6e1c78..1a35a25217b0 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -2148,15 +2148,20 @@ function kit({ svelte_config }) { } if (event.code === 'BUNDLE_END') { - await process_ssr_build( - /** @type {Rolldown.RolldownOutput['output']} */ ( - watch_build_output.get(builder.environments.ssr.name) - ) - ); - // buildApp hooks don't rerun in watch mode so we need to run - // the deferred steps here on subsequent builds - if (rebuild) await finalise?.(); - await event.result.close(); + try { + await process_ssr_build( + /** @type {Rolldown.RolldownOutput['output']} */ ( + watch_build_output.get(builder.environments.ssr.name) + ) + ); + // buildApp hooks don't rerun in watch mode so we need to run + // the deferred steps here on subsequent builds + if (rebuild) await finalise?.(); + } catch (e) { + return task.reject(e); + } finally { + await event.result.close(); + } return task.resolve(); } });