From 5d31e0100af12e40e3246c84229d2b3e0a5f5806 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 8 Jul 2026 21:37:45 -0400 Subject: [PATCH 01/29] WIP --- packages/kit/src/core/sync/write_root.js | 32 ++++++++----------- packages/kit/src/runtime/client/client.js | 26 ++++++++++++++- .../kit/src/runtime/server/page/render.js | 24 ++++++++++++++ packages/kit/src/runtime/types.d.ts | 8 +++++ 4 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 packages/kit/src/runtime/types.d.ts diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js index 2c264870aa29..f96754ee8deb 100644 --- a/packages/kit/src/core/sync/write_root.js +++ b/packages/kit/src/core/sync/write_root.js @@ -27,10 +27,7 @@ export function write_root(manifest_data, output) { - {#snippet pyramid(depth)} - {@const Pyramid = constructors[depth]} - {#snippet failed(error)} - {@const ErrorPage = errors[depth]} - - {/snippet} - - {#if constructors[depth + 1]} - {@const d = data[depth]} + {#snippet node(n, depth)} + + {#if n.child} - - {@render pyramid(depth + 1)} - + + {@render node(n.child, depth + 1)} + {:else} - {@const d = data[depth]} - + {/if} + + {#snippet failed(error)} + + {/snippet} {/snippet} - {@render pyramid(0)} + {@render node(root, 0)} {#if mounted}
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 3cb7b4a53f45..11f07fca0af8 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1,4 +1,4 @@ -/** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */ +/** @import { CSRPageNodeLoader, RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */ /** @import { NavigationIntent } from './types.js' */ /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */ /** @import { Query } from './remote-functions/query/instance.svelte.js' */ @@ -874,6 +874,30 @@ async function get_navigation_result_from_branch({ }; } + const root = /** @type {import('../types.js').RenderNode} */ ({}); + let current_node = root; + let current_data = {}; + + for (let i = 0; i < branch.length; i += 1) { + const node = branch[i]; + if (!node) continue; + + const data = { ...current_data, ...node.data }; + + const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; + + current_node.error = (await error_loader())?.component; + current_node.component = node.node.component; + current_node.data = current_data = data; + + if (i < branch.length - 1) { + current_node.child = /** @type {import('../types.js').RenderNode} */ ({}); + current_node = current_node.child; + } + } + + result.props.root = root; + return result; } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 583d4584324f..7060ceb3d0dc 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -186,6 +186,30 @@ export async function render_response({ state: {} }; + const root = /** @type {import('../../types.js').RenderNode} */ ({}); + let current_node = root; + let current_data = {}; + + for (let i = 0; i < branch.length; i += 1) { + const node = branch[i]; + if (!node) continue; + + const data = { ...current_data, ...node.data }; + + // const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; + + // current_node.error = (await error_loader())?.component; + current_node.component = await node.node.component?.(); + current_node.data = current_data = data; + + if (i < branch.length - 1) { + current_node.child = /** @type {import('../../types.js').RenderNode} */ ({}); + current_node = current_node.child; + } + } + + props.root = root; + const render_state = { ...event_state, is_in_render: true }; const render_opts = { diff --git a/packages/kit/src/runtime/types.d.ts b/packages/kit/src/runtime/types.d.ts new file mode 100644 index 000000000000..71afa916565e --- /dev/null +++ b/packages/kit/src/runtime/types.d.ts @@ -0,0 +1,8 @@ +import { Component } from 'svelte'; + +export interface RenderNode { + component: Component; + error: Component; + data: Record; + child?: RenderNode; +} From afefba89b2fa8affbd38e4acc1abca09a0439b45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 09:55:51 -0400 Subject: [PATCH 02/29] WIP --- packages/kit/src/runtime/client/client.js | 4 ++-- packages/kit/src/runtime/server/page/render.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 11f07fca0af8..cfd966834325 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -880,13 +880,13 @@ async function get_navigation_result_from_branch({ for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; - if (!node) continue; + if (!node?.node.component) continue; const data = { ...current_data, ...node.data }; const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; - current_node.error = (await error_loader())?.component; + current_node.error = (await error_loader()).component; current_node.component = node.node.component; current_node.data = current_data = data; diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 7060ceb3d0dc..6d2be9049a72 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -192,14 +192,15 @@ export async function render_response({ for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; - if (!node) continue; + if (!node.node.component) continue; const data = { ...current_data, ...node.data }; - // const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; + // TODO this is undefined sometimes... where does the default error component come from? + const error = error_components?.slice(0, i + 1).findLast((x) => x); - // current_node.error = (await error_loader())?.component; - current_node.component = await node.node.component?.(); + current_node.error = error; + current_node.component = await node.node.component(); current_node.data = current_data = data; if (i < branch.length - 1) { From bae66914564fef66512dd49b65e1ef8d87ecd74a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 12:50:12 -0400 Subject: [PATCH 03/29] fix --- packages/kit/src/core/sync/write_root.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js index f96754ee8deb..9e1aa3344190 100644 --- a/packages/kit/src/core/sync/write_root.js +++ b/packages/kit/src/core/sync/write_root.js @@ -44,19 +44,22 @@ export function write_root(manifest_data, output) { {#snippet node(n, depth)} + {@const Component = n.component} + {#if n.child} - + {@render node(n.child, depth + 1)} - + {:else} - + {/if} {#snippet failed(error)} - + {@const ErrorPage = n.error} + {/snippet} {/snippet} From 81ddc27d5cd3a7f49957b923fd9aeb3d5f736034 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 14:06:11 -0400 Subject: [PATCH 04/29] make root.svelte a real component --- packages/kit/src/core/sync/write_root.js | 14 ---- packages/kit/src/runtime/client/client.js | 6 +- .../kit/src/runtime/components/root.svelte | 64 +++++++++++++++++++ 3 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 packages/kit/src/runtime/components/root.svelte diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js index 9e1aa3344190..1d8a93a297f2 100644 --- a/packages/kit/src/core/sync/write_root.js +++ b/packages/kit/src/core/sync/write_root.js @@ -5,20 +5,6 @@ import { dedent, write_if_changed } from './utils.js'; * @param {string} output */ export function write_root(manifest_data, output) { - // TODO remove default layout altogether - - const max_depth = Math.max( - ...manifest_data.routes.map((route) => - route.page ? route.page.layouts.filter(Boolean).length + 1 : 0 - ), - 1 - ); - - const levels = []; - for (let i = 0; i <= max_depth; i += 1) { - levels.push(i); - } - write_if_changed( `${output}/root.svelte`, dedent` diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index cfd966834325..cbe48e0ac37f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -46,6 +46,10 @@ import { page, update, navigating, updated } from './state.svelte.js'; import { add_data_suffix, add_resolution_suffix } from '../pathname.js'; import { noop_span } from '../telemetry/noop.js'; import { read_ndjson } from './ndjson.js'; +import RootModern from '../components/root.svelte'; +import { asClassComponent } from 'svelte/legacy'; + +const Root = asClassComponent(RootModern); export { load_css }; const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']); @@ -703,7 +707,7 @@ async function initialize(result, target, hydrate) { update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page)); // TODO: use mount() - root = new app.root({ + root = new Root({ target, props: { ...result.props, components }, hydrate, diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte new file mode 100644 index 000000000000..9e44a486810a --- /dev/null +++ b/packages/kit/src/runtime/components/root.svelte @@ -0,0 +1,64 @@ + + +{#snippet node(n: RenderNode, depth: number)} + {@const Component = n.component} + + + {#if n.child} + + + {@render node(n.child, depth + 1)} + + {:else} + + + {/if} + + {#snippet failed(error)} + {@const ErrorPage = n.error} + + {/snippet} + +{/snippet} + +{@render node(root, 0)} + +{#if mounted} +
+ {#if navigated} + {title} + {/if} +
+{/if} From abea74f675348a8600ac682270fdda5b352458b0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 14:52:28 -0400 Subject: [PATCH 05/29] use svelte/server --- packages/kit/src/core/sync/write_server.js | 2 -- packages/kit/src/runtime/server/page/render.js | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 5f0ccf0519c2..2609925a443d 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -24,7 +24,6 @@ const server_template = ({ has_service_worker, template }) => ` -import root from '../root.js'; import { set_building, set_prerendering } from '$app/env/internal'; import { set_assets } from '$app/paths/internal/server'; import { set_manifest, set_read_implementation } from '__sveltekit/server'; @@ -40,7 +39,6 @@ export const options = { hooks: null, // added lazily, via \`get_hooks\` link_header_preload: ${s(config.kit.output.linkHeaderPreload)}, paths_origin: ${s(config.kit.paths.origin)}, - root, service_worker: ${has_service_worker}, service_worker_options: ${config.kit.serviceWorker.register ? s(config.kit.serviceWorker.options) : 'null'}, templates: { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 6d2be9049a72..453457e0c420 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -22,6 +22,8 @@ import { } from '../utils.js'; import * as env from '__sveltekit/env'; import { collect_remote_data } from '../remote.js'; +import Root from '../../components/root.svelte'; +import { render } from 'svelte/server'; // TODO rename this function/module @@ -275,11 +277,10 @@ export async function render_response({ // We have to invoke .then eagerly here in order to kick off rendering: it's only starting on access, // and `await maybe_promise` would eagerly access the .then property but call its function only after a tick, which is too late // for the paths.reset() below and for any eager getRequestEvent() calls during rendering without AsyncLocalStorage available. - // TODO use render from 'svelte/server' here - const rendered = options.root.render(props, render_opts).then((r) => r); + const rendered = render(Root, { ...render_opts, props }); // @ts-expect-error the legacy `render` API only returns html still, but the new API uses body - const { head, html: body, css, hashes } = await rendered; + const { head, body, css, hashes } = await rendered; if (hashes) { csp.add_script_hashes(hashes.script); From 26e8445f538e87a5f91c53ba48095dfea7658644 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 15:08:49 -0400 Subject: [PATCH 06/29] tweak --- packages/kit/src/runtime/server/page/render.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 453457e0c420..972852fb7bfc 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -93,7 +93,8 @@ export async function render_response({ // TODO if we add a client entry point one day, we will need to include inline_styles with the entry, otherwise stylesheets will be linked even if they are below inlineStyleThreshold const inline_styles = new Map(); - /** @type {Awaited>} */ + // TODO `svelte/server` should expose `RenderOutput` + /** @type {{ head: string, body: string, css: { code: string, map: any }, hashes: { script: string[] } }} */ let rendered; const form_value = From ca93c91898e56ad79023adfbabfe4b8e616edd2d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 15:13:51 -0400 Subject: [PATCH 07/29] unused --- packages/kit/src/core/sync/sync.js | 2 - .../src/core/sync/write_client_manifest.js | 2 - packages/kit/src/core/sync/write_root.js | 73 ------------------- packages/kit/src/runtime/client/client.js | 2 +- 4 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 packages/kit/src/core/sync/write_root.js diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js index a53b6832c370..0adf76e3c5ac 100644 --- a/packages/kit/src/core/sync/sync.js +++ b/packages/kit/src/core/sync/sync.js @@ -2,7 +2,6 @@ import path from 'node:path'; import process from 'node:process'; import create_manifest_data from './create_manifest_data/index.js'; import { write_client_manifest } from './write_client_manifest.js'; -import { write_root } from './write_root.js'; import { write_tsconfig } from './write_tsconfig.js'; import { write_types, write_all_types } from './write_types/index.js'; import { write_ambient } from './write_ambient.js'; @@ -37,7 +36,6 @@ export function create(config, root) { write_client_manifest(config.kit, manifest_data, `${output}/client`); write_server(config, output, root); - write_root(manifest_data, output); write_all_types(config, manifest_data, root); write_non_ambient(config.kit, manifest_data); diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 696f72c72306..42b3e940282d 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -181,8 +181,6 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { export const decode = (type, value) => decoders[type](value); - export { default as root } from '../root.js'; - export const get_error_template = () => import('../shared/error-template.js').then(m => m.default); ` ); diff --git a/packages/kit/src/core/sync/write_root.js b/packages/kit/src/core/sync/write_root.js deleted file mode 100644 index 1d8a93a297f2..000000000000 --- a/packages/kit/src/core/sync/write_root.js +++ /dev/null @@ -1,73 +0,0 @@ -import { dedent, write_if_changed } from './utils.js'; - -/** - * @param {import('types').ManifestData} manifest_data - * @param {string} output - */ -export function write_root(manifest_data, output) { - write_if_changed( - `${output}/root.svelte`, - dedent` - - - - - {#snippet node(n, depth)} - {@const Component = n.component} - - - {#if n.child} - - - {@render node(n.child, depth + 1)} - - {:else} - - - {/if} - - {#snippet failed(error)} - {@const ErrorPage = n.error} - - {/snippet} - - {/snippet} - - {@render node(root, 0)} - - {#if mounted} -
- {#if navigated} - {title} - {/if} -
- {/if} - ` - ); - - write_if_changed( - `${output}/root.js`, - dedent` - import { asClassComponent } from 'svelte/legacy'; - import Root from './root.svelte'; - export default asClassComponent(Root); - ` - ); -} diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index cbe48e0ac37f..f9887029cb6f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1,4 +1,4 @@ -/** @import { CSRPageNodeLoader, RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */ +/** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */ /** @import { NavigationIntent } from './types.js' */ /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */ /** @import { Query } from './remote-functions/query/instance.svelte.js' */ From 7b5bec1108a93e7cb12d623a6f466df3d48674fe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 15:35:09 -0400 Subject: [PATCH 08/29] update --- packages/kit/src/runtime/client/client.js | 32 +++------------------- packages/kit/src/runtime/client/types.d.ts | 5 ++-- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index f9887029cb6f..bfc296255eed 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1,5 +1,6 @@ /** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */ /** @import { NavigationIntent } from './types.js' */ +/** @import { RenderNode } from '../types.js' */ /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */ /** @import { Query } from './remote-functions/query/instance.svelte.js' */ /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */ @@ -798,33 +799,11 @@ async function get_navigation_result_from_branch({ route }, props: { - constructors: compact(branch).map((branch_node) => branch_node.node.component), - page + page, + root: /** @type {RenderNode} */ ({}) } }; - if (errors) { - let last_idx = -1; - result.props.errors = await Promise.all( - // eslint-disable-next-line @typescript-eslint/await-thenable - branch - .map((b, i) => { - if (i === 0) return undefined; // root layout wraps root error component, not the other way around - if (!b) return null; - - i--; - // Find the closest error component up to the previous branch - while (i > last_idx + 1 && !errors[i]) i -= 1; - last_idx = i; - return errors[i]?.() - .then((e) => e.component) - .catch(() => undefined); - }) - // filter out indexes where there was no branch, but keep indexes where there was a branch but no error component - .filter((e) => e !== null) - ); - } - if (error) { result.props.error = error; } @@ -878,8 +857,7 @@ async function get_navigation_result_from_branch({ }; } - const root = /** @type {import('../types.js').RenderNode} */ ({}); - let current_node = root; + let current_node = result.props.root; let current_data = {}; for (let i = 0; i < branch.length; i += 1) { @@ -900,8 +878,6 @@ async function get_navigation_result_from_branch({ } } - result.props.root = root; - return result; } diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 9075d7b7054a..605ae791b497 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -10,6 +10,7 @@ import { Uses } from 'types'; import { Page, ParamMatcher } from '@sveltejs/kit'; +import { RenderNode } from '../types.js'; export interface SvelteKitApp { /** @@ -91,13 +92,11 @@ export type NavigationFinished = { type: 'loaded'; state: NavigationState; props: { - constructors: Array; - errors?: Array; components?: SvelteComponent[]; page: Page; form?: Record | null; error?: App.Error; - [key: `data_${number}`]: Record; + root: RenderNode; }; }; From cd1da8da68928d72e0ceb521dff7e51b54922127 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 15:43:44 -0400 Subject: [PATCH 09/29] tidy up --- packages/kit/src/runtime/client/client.js | 40 +++++++---------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index bfc296255eed..ac7d9780282f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -815,23 +815,28 @@ async function get_navigation_result_from_branch({ let data = {}; let data_changed = !page; - let p = 0; + let current_node = result.props.root; - for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) { + for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; const prev = current.branch[i]; if (node?.data !== prev?.data) data_changed = true; if (!node) continue; + if (!node.node.component) continue; data = { ...data, ...node.data }; - // Only set props if the node actually updated. This prevents needless rerenders. - if (data_changed) { - result.props[`data_${p}`] = data; - } + const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; + + current_node.error = (await error_loader()).component; + current_node.component = node.node.component; + current_node.data = data; - p += 1; + if (i < branch.length - 1) { + current_node.child = /** @type {import('../types.js').RenderNode} */ ({}); + current_node = current_node.child; + } } const page_changed = @@ -857,27 +862,6 @@ async function get_navigation_result_from_branch({ }; } - let current_node = result.props.root; - let current_data = {}; - - for (let i = 0; i < branch.length; i += 1) { - const node = branch[i]; - if (!node?.node.component) continue; - - const data = { ...current_data, ...node.data }; - - const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; - - current_node.error = (await error_loader()).component; - current_node.component = node.node.component; - current_node.data = current_data = data; - - if (i < branch.length - 1) { - current_node.child = /** @type {import('../types.js').RenderNode} */ ({}); - current_node = current_node.child; - } - } - return result; } From 966ec80c85cd9e0210bd9cec45869f108b4689f3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 15:47:30 -0400 Subject: [PATCH 10/29] simplify --- .../kit/src/runtime/server/page/render.js | 66 +++++-------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 972852fb7bfc..21b20a2a9f39 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -1,5 +1,6 @@ +/** @import { RenderNode } from '../../types.js' */ import * as devalue from 'devalue'; -import { readable, writable } from 'svelte/store'; +import { readable } from 'svelte/store'; import { DEV } from 'esm-env'; import { isRedirect, text } from '@sveltejs/kit'; import * as paths from '$app/paths/internal/server'; @@ -145,66 +146,26 @@ export async function render_response({ if (page_config.ssr) { /** @type {Record} */ const props = { - stores: { - page: writable(null), - navigating: writable(null), - updated - }, - constructors: await Promise.all( - branch.map(({ node }) => { - if (!node.component) { - // Can only be the leaf, layouts have a fallback component generated - throw new Error(`Missing +page.svelte component for route ${event.route.id}`); - } - return node.component(); - }) - ), - form: form_value - }; - - if (error_components) { - if (error) { - props.error = error; - } - props.errors = error_components; - } - - let data = {}; - - // props_n (instead of props[n]) makes it easy to avoid - // unnecessary updates for layout components - for (let i = 0; i < branch.length; i += 1) { - data = { ...data, ...branch[i].data }; - props[`data_${i}`] = data; - } - - props.page = { - error, - params: /** @type {Record} */ (event.params), - route: event.route, - status, - url: event.url, - data, form: form_value, - state: {} + root: /** @type {RenderNode} */ ({}), + error }; - const root = /** @type {import('../../types.js').RenderNode} */ ({}); - let current_node = root; - let current_data = {}; + let current_node = props.root; + let data = {}; for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; if (!node.node.component) continue; - const data = { ...current_data, ...node.data }; + data = { ...data, ...node.data }; // TODO this is undefined sometimes... where does the default error component come from? const error = error_components?.slice(0, i + 1).findLast((x) => x); current_node.error = error; current_node.component = await node.node.component(); - current_node.data = current_data = data; + current_node.data = data; if (i < branch.length - 1) { current_node.child = /** @type {import('../../types.js').RenderNode} */ ({}); @@ -212,7 +173,16 @@ export async function render_response({ } } - props.root = root; + props.page = { + error, + params: /** @type {Record} */ (event.params), + route: event.route, + status, + url: event.url, + data, + form: form_value, + state: {} + }; const render_state = { ...event_state, is_in_render: true }; From 95b6e2ebc053694841e7f77ebf97f997564ce52f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 17:36:31 -0400 Subject: [PATCH 11/29] fixes --- packages/kit/src/runtime/client/client.js | 1 - packages/kit/src/runtime/client/types.d.ts | 2 -- packages/kit/src/runtime/components/root.svelte | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index ac7d9780282f..5869359eed77 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -34,7 +34,6 @@ import { } from './constants.js'; import { validate_page_exports } from '../../utils/exports.js'; import { noop } from '../../utils/functions.js'; -import { compact } from '../../utils/array.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 605ae791b497..6826c8441a44 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -58,8 +58,6 @@ export interface SvelteKitApp { */ hash: boolean; - root: typeof SvelteComponent; - /** * Lazily loads the contents of src/error.html, used as a last-resort * error page when the root layout's load function throws during client-side rendering. diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte index 9e44a486810a..beda3c59c0a4 100644 --- a/packages/kit/src/runtime/components/root.svelte +++ b/packages/kit/src/runtime/components/root.svelte @@ -7,8 +7,8 @@ page: Page; root: RenderNode; components: any[]; - form: any; - error: App.Error | undefined; + form?: any; + error?: App.Error; } const { page, root, components = [], form, error }: Props = $props(); From 296aa2eb4dbb022327c9776a01559a14ad0ec9cc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 9 Jul 2026 19:58:53 -0400 Subject: [PATCH 12/29] try this --- packages/kit/src/runtime/client/client.js | 1 - packages/kit/src/runtime/server/page/render.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 5869359eed77..889ae2426b49 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -822,7 +822,6 @@ async function get_navigation_result_from_branch({ if (node?.data !== prev?.data) data_changed = true; if (!node) continue; - if (!node.node.component) continue; data = { ...data, ...node.data }; diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 21b20a2a9f39..142396603e5a 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -156,7 +156,6 @@ export async function render_response({ for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; - if (!node.node.component) continue; data = { ...data, ...node.data }; @@ -164,7 +163,7 @@ export async function render_response({ const error = error_components?.slice(0, i + 1).findLast((x) => x); current_node.error = error; - current_node.component = await node.node.component(); + current_node.component = await node.node.component?.(); current_node.data = data; if (i < branch.length - 1) { From 3f4d1d39d70df2256ce897c19f1e9f315549bfe8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 11:03:47 -0400 Subject: [PATCH 13/29] fix --- packages/kit/src/runtime/client/client.js | 3 ++- packages/kit/src/types/internal.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 889ae2426b49..367036519f7e 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1155,7 +1155,8 @@ function preload_error({ error, url, route, params }) { ...page, status: error.status }, - constructors: [] + // TODO understand why this is never used + root: /** @type {RenderNode} */ ({}) } }; } diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 096593aede22..a06c49f075c3 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -1,4 +1,4 @@ -import { SvelteComponent } from 'svelte'; +import { Component } from 'svelte'; import { Config, ServerLoad, @@ -113,7 +113,7 @@ export interface BuildData { } export interface CSRPageNode { - component: typeof SvelteComponent; + component: Component; universal: { load?: Load; trailingSlash?: TrailingSlash; From d09768ade28f07d06e3da3d52745940d18b8f399 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 11:04:32 -0400 Subject: [PATCH 14/29] unused --- packages/kit/src/runtime/server/page/render.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 142396603e5a..3b119c8b2274 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -28,11 +28,6 @@ import { render } from 'svelte/server'; // TODO rename this function/module -const updated = { - ...readable(false), - check: () => false -}; - /** * Creates the HTML response. * @param {{ From 3e820a785ea5541a5de5606eea634f1a2c8859ff Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 11:04:51 -0400 Subject: [PATCH 15/29] fix --- packages/kit/src/runtime/server/page/render.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 3b119c8b2274..42f7169f398d 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -1,6 +1,5 @@ /** @import { RenderNode } from '../../types.js' */ import * as devalue from 'devalue'; -import { readable } from 'svelte/store'; import { DEV } from 'esm-env'; import { isRedirect, text } from '@sveltejs/kit'; import * as paths from '$app/paths/internal/server'; From 4e5dbcd4ec2c89389913daeed1f8dbeb7776508c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 16:41:48 -0400 Subject: [PATCH 16/29] simplify --- packages/kit/src/runtime/client/client.js | 62 ++++--------------- .../kit/src/runtime/components/root.svelte | 2 +- 2 files changed, 13 insertions(+), 51 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 14014e1ca126..aaaf099112f3 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -636,17 +636,14 @@ async function _preload_data(intent) { load_cache = { id: intent.id, token: preload, - promise: load_route({ ...intent, preload }).then((result) => { + promise: load_route({ ...intent, preload }).finally(() => { preload_tokens.delete(preload); - if (result.type === 'loaded' && result.state.error) { - // Don't cache errors, because they might be transient - discard_load_cache(); - } - return result; }), fork: null }; + load_cache.promise.catch(discard_load_cache); + if (__SVELTEKIT_FORK_PRELOADS__ && svelte.fork) { const lc = load_cache; @@ -1145,33 +1142,6 @@ function diff_search_params(old_url, new_url) { return changed; } -/** - * @param {Omit & { error: App.Error }} opts - * @returns {import('./types.js').NavigationFinished} - */ -function preload_error({ error, url, route, params }) { - return { - type: 'loaded', - state: { - error, - url, - route, - params, - branch: [] - }, - props: { - page: { - // we skipped loading the error page, so we have to use the current page - // store, but update the status received while preloading - ...page, - status: error.status - }, - // TODO understand why this is never used - root: /** @type {RenderNode} */ ({}) - } - }; -} - /** * @overload * @param {import('./types.js').NavigationIntent} intent @@ -1242,7 +1212,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) { const handled_error = await handle_error(error, { url, params, route: { id } }); if (preload && preload_tokens.has(preload)) { - return preload_error({ error: handled_error, url, params, route }); + throw handled_error; } return load_root_error_page({ @@ -1332,13 +1302,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) { } if (preload && preload_tokens.has(preload)) { - const error = await handle_error(err, { params, url, route: { id: route.id } }); - return preload_error({ - error, - url, - params, - route - }); + throw await handle_error(err, { params, url, route: { id: route.id } }); } /** @type {App.Error} */ @@ -2166,15 +2130,13 @@ function setup_preload() { if (!intent) return; if (DEV) { - void _preload_data(intent).then((result) => { - if (result.type === 'loaded' && result.state.error) { - console.warn( - `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` + - 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' + - 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' + - 'See https://svelte.dev/docs/kit/link-options for more info' - ); - } + void _preload_data(intent).catch((error) => { + console.warn( + `Preloading data for ${intent.url.pathname} failed with the following error: ${error.message}\n` + + 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' + + 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' + + 'See https://svelte.dev/docs/kit/link-options for more info' + ); }); } else { void _preload_data(intent); diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte index 79d21ded0e6b..6b6d91291f72 100644 --- a/packages/kit/src/runtime/components/root.svelte +++ b/packages/kit/src/runtime/components/root.svelte @@ -7,7 +7,7 @@ page: Page; root: RenderNode; components: any[]; - resetters: Array<() => void>; + resetters: Array<(() => void) | undefined>; form?: any; error?: App.Error; } From b951cb4d9c15913cd5f7a4be6ba3214e9dfce280 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 16:44:11 -0400 Subject: [PATCH 17/29] unused --- packages/kit/src/runtime/client/types.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 6826c8441a44..ea17a57d2eef 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -90,7 +90,6 @@ export type NavigationFinished = { type: 'loaded'; state: NavigationState; props: { - components?: SvelteComponent[]; page: Page; form?: Record | null; error?: App.Error; From 4df2672f93655e85a6488a118f359deaaeba1205 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 16:47:02 -0400 Subject: [PATCH 18/29] tidy up --- packages/kit/src/runtime/components/root.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte index 6b6d91291f72..9f178d9a816f 100644 --- a/packages/kit/src/runtime/components/root.svelte +++ b/packages/kit/src/runtime/components/root.svelte @@ -29,7 +29,8 @@ {#snippet node(n: RenderNode, depth: number)} - {@const Component = n.component} + {const Component = $derived(n.component)} + {const Error = $derived(n.error)} (resetters[depth] = reset)}> {#if n.child} @@ -43,8 +44,7 @@ {/if} {#snippet failed(error)} - {@const ErrorPage = n.error} - + {/snippet} {/snippet} From 9503dacdc8eb4ef14376096a9b6cd9947a1869a2 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:40:22 +0000 Subject: [PATCH 19/29] Fix: `preloadData()` rejects instead of resolving to the documented `{ type: 'error', error, status }` shape when a preloaded route's `load` throws, because the `simplify` commit made `load_route` throw on preload errors but left `preloadData`'s old return-based error handling in place (now dead code). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at packages/kit/src/runtime/client/client.js:2412 ## Confirmed regression Commit `4e5dbcd` ("simplify") changed the client preload flow in `packages/kit/src/runtime/client/client.js`. ### Before `load_route` converted preload errors into a resolved result via `preload_error(...)`: ```js if (preload && preload_tokens.has(preload)) { return preload_error({ error: handled_error, url, params, route }); } ``` which produced `{ type: 'loaded', state: { error }, props: { page: { ...page, status: error.status } } }`. So `_preload_data` resolved, and `preloadData` could read `result.state.error` and return `{ type: 'error', status, error }`. ### After `load_route` now throws on preload errors (lines ~1214 and ~1304): ```js if (preload && preload_tokens.has(preload)) { throw handled_error; } // ... if (preload && preload_tokens.has(preload)) { throw await handle_error(err, { params, url, route: { id: route.id } }); } ``` The commit correctly updated `setup_preload` (the `data-sveltekit-preload-data` path) to use `.catch(...)`, but **forgot to update the public `preloadData` API**, which still did: ```js const result = await _preload_data(intent); // <-- now rejects ... if (result.type === 'loaded' && result.state.error) { // <-- dead code return { type: 'error', status, error: result.state.error }; } ``` ### Failure mode Concrete trigger: call `preloadData('/some/route')` where that route's `load` (or `+page.server.js` load) throws. Previously it resolved to `{ type: 'error', status, error }`; now `_preload_data` rejects, so the `await` throws and `preloadData` **rejects** instead of resolving. The `if (result.state.error)` branch is now unreachable dead code. ### Contract violation The public type declaration (`packages/kit/types/index.d.ts:~3413`) and the JSDoc still promise a resolution of shape: ```ts Promise<({ type: 'loaded'; data } | { type: 'redirect'; location } | { type: 'error'; error: App.Error }) & { status: number }> ``` No changeset documents dropping the `{ type: 'error' }` resolution, so this is an unintended breaking change to a public API rather than a deliberate contract change. ## Fix Wrap the `_preload_data(intent)` call in `preloadData` in a `try/catch`. On rejection, the caught value is the handled error — an `App.Error` that carries a `status` (both `handle_error`'s non-`HttpError` return `{ ...app_error, status }` and `HttpError.body` include `status`). Return it in the documented shape, with a `500` fallback for safety, and remove the now-dead `result.state.error` branch: ```js */ (error); return { type: 'error', status: handled?.status ?? 500, error: handled }; } ``` This restores the documented `{ type: 'error', error, status }` resolution. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/kit/src/runtime/client/client.js | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index aaaf099112f3..a5809803725d 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2409,7 +2409,22 @@ export async function preloadData(href) { throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); } - const result = await _preload_data(intent); + /** @type {Awaited>} */ + let result; + + try { + result = await _preload_data(intent); + } catch (error) { + // `load_route` throws the handled error (an `App.Error` with a `status`) + // when a preload fails, so surface it in the documented `{ type: 'error' }` shape + const handled = /** @type {App.Error & { status?: number }} */ (error); + return { + type: 'error', + status: handled?.status ?? 500, + error: handled + }; + } + if (result.type === 'redirect') { return { type: result.type, @@ -2420,14 +2435,6 @@ export async function preloadData(href) { const { status, data } = result.props.page ?? page; - if (result.type === 'loaded' && result.state.error) { - return { - type: 'error', - status, - error: result.state.error - }; - } - return { type: result.type, status, data }; } From 797539b596c4b0b4cae3003a21765614fb298802 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 19:56:27 -0400 Subject: [PATCH 20/29] retire SSRComponent --- packages/kit/src/runtime/server/page/index.js | 5 ++-- .../kit/src/runtime/server/page/render.js | 2 +- packages/kit/src/types/internal.d.ts | 24 ++----------------- packages/kit/types/index.d.ts | 23 ++---------------- 4 files changed, 8 insertions(+), 46 deletions(-) diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 3e6d5a8c408a..a091d9c4a169 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,5 +1,6 @@ +/** @import { Component } from 'svelte' */ /** @import { ActionResult, RequestEvent, SSRManifest } from '@sveltejs/kit' */ -/** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRComponent, SSRNode, SSROptions, SSRState } from 'types' */ +/** @import { PageNodeIndexes, RequestState, RequiredResolveOptions, ServerDataNode, SSRNode, SSROptions, SSRState } from 'types' */ import { text } from '@sveltejs/kit'; import { HttpError, Redirect } from '@sveltejs/kit/internal'; import { compact } from '../../../utils/array.js'; @@ -398,7 +399,7 @@ export async function render_page( * @param {SSRManifest} manifest */ async function load_error_components(ssr, branch, page, manifest) { - /** @type {Array | undefined} */ + /** @type {Array | undefined} */ let error_components; if (ssr) { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 42f7169f398d..32fc5f2e498d 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -43,7 +43,7 @@ import { render } from 'svelte/server'; * resolve_opts: import('types').RequiredResolveOptions; * action_result?: import('@sveltejs/kit').ActionResult; * data_serializer: import('./types.js').ServerDataSerializer; - * error_components?: Array + * error_components?: Array * }} opts */ export async function render_response({ diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index a06c49f075c3..534af246bbce 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -417,27 +417,7 @@ export interface ServerMetadata { remotes: Map>; } -// TODO get rid of this in favor us using just import('svelte').Component -export interface SSRComponent { - default: { - render( - props: Record, - opts: { context: Map; csp?: { nonce?: string; hash?: boolean } } - ): Promise<{ - body: string; - head: string; - css: { - code: string; - map: any; // TODO - }; - hashes: { - script: Array<`sha256-${string}`>; - }; - }>; - }; -} - -export type SSRComponentLoader = () => Promise; +export type SSRComponentLoader = () => Promise; export interface UniversalNode { /** Is `null` in case static analysis succeeds but the node is ssr=false */ @@ -501,7 +481,7 @@ export interface SSROptions { hooks: ServerHooks; link_header_preload: ValidatedConfig['kit']['output']['linkHeaderPreload']; paths_origin: string | undefined; - root: SSRComponent['default']; + root: Component; service_worker: boolean; service_worker_options: RegistrationOptions; templates: { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 6892b0aab145..97a3296e398b 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -6,6 +6,7 @@ declare module '@sveltejs/kit' { import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { Plugin } from 'vite'; import type { RouteId as AppRouteId, LayoutParams as AppLayoutParams, ResolvedPathname } from '$app/types'; + import type { Component } from 'svelte'; // @ts-ignore this is an optional peer dependency so could be missing. Written like this so dts-buddy preserves the ts-ignore type Span = import('@opentelemetry/api').Span; @@ -2836,27 +2837,7 @@ declare module '@sveltejs/kit' { } | null; } - // TODO get rid of this in favor us using just import('svelte').Component - interface SSRComponent { - default: { - render( - props: Record, - opts: { context: Map; csp?: { nonce?: string; hash?: boolean } } - ): Promise<{ - body: string; - head: string; - css: { - code: string; - map: any; // TODO - }; - hashes: { - script: Array<`sha256-${string}`>; - }; - }>; - }; - } - - type SSRComponentLoader = () => Promise; + type SSRComponentLoader = () => Promise; interface UniversalNode { /** Is `null` in case static analysis succeeds but the node is ssr=false */ From e6403ca059c647d5b1abd9373b5d783caadb50e8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 20:01:42 -0400 Subject: [PATCH 21/29] tidy up --- packages/kit/src/runtime/server/page/render.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 32fc5f2e498d..a7060302f827 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -89,7 +89,7 @@ export async function render_response({ const inline_styles = new Map(); // TODO `svelte/server` should expose `RenderOutput` - /** @type {{ head: string, body: string, css: { code: string, map: any }, hashes: { script: string[] } }} */ + /** @type {{ head: string, body: string, hashes: { script: string[] } }} */ let rendered; const form_value = @@ -243,14 +243,13 @@ export async function render_response({ // for the paths.reset() below and for any eager getRequestEvent() calls during rendering without AsyncLocalStorage available. const rendered = render(Root, { ...render_opts, props }); - // @ts-expect-error the legacy `render` API only returns html still, but the new API uses body - const { head, body, css, hashes } = await rendered; + const { head, body, hashes } = await rendered; if (hashes) { csp.add_script_hashes(hashes.script); } - return { head, body, css, hashes }; + return { head, body, hashes }; }); } finally { if (DEV) { @@ -258,7 +257,7 @@ export async function render_response({ } } } else { - rendered = { head: '', body: '', css: { code: '', map: null }, hashes: { script: [] } }; + rendered = { head: '', body: '', hashes: { script: [] } }; } for (const { node } of branch) { From d3087d2c88deb1bb0a7f19a4641a036a4eb5e9df Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 20:02:52 -0400 Subject: [PATCH 22/29] unused --- packages/kit/src/types/internal.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 534af246bbce..b71a05673375 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -481,7 +481,6 @@ export interface SSROptions { hooks: ServerHooks; link_header_preload: ValidatedConfig['kit']['output']['linkHeaderPreload']; paths_origin: string | undefined; - root: Component; service_worker: boolean; service_worker_options: RegistrationOptions; templates: { From a7dc5c4dedfdfb5d65eaa566968b16a7f8a504c3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 20:11:45 -0400 Subject: [PATCH 23/29] bump required svelte peer dep version, so we can use declaration tags --- packages/kit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/package.json b/packages/kit/package.json index 2b2bf749dbbc..989cb5792750 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -47,7 +47,7 @@ "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "svelte": "^5.48.0", + "svelte": "^5.56.4", "typescript": "^6.0.0", "vite": "^8.0.12" }, From b2c2cdc6ff9e1b43bd66adaf8d446e90c0ec86cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 20:22:09 -0400 Subject: [PATCH 24/29] make components/resetters required --- packages/kit/src/runtime/components/root.svelte | 2 +- packages/kit/src/runtime/server/page/render.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte index 9f178d9a816f..35ef0feaee85 100644 --- a/packages/kit/src/runtime/components/root.svelte +++ b/packages/kit/src/runtime/components/root.svelte @@ -12,7 +12,7 @@ error?: App.Error; } - const { page, components = [], resetters = [], root, form, error }: Props = $props(); + const { page, components, resetters, root, form, error }: Props = $props(); let mounted = $state(false); let navigated = $state(false); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index a7060302f827..2aec8127debb 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -140,6 +140,8 @@ export async function render_response({ if (page_config.ssr) { /** @type {Record} */ const props = { + components: [], + resetters: [], form: form_value, root: /** @type {RenderNode} */ ({}), error From a31be6628dc85e30081f88fb74faa2e6586a6499 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 10 Jul 2026 20:27:54 -0400 Subject: [PATCH 25/29] tweak --- .../kit/src/runtime/server/page/render.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 2aec8127debb..26f5d618b3c8 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -144,11 +144,21 @@ export async function render_response({ resetters: [], form: form_value, root: /** @type {RenderNode} */ ({}), - error + error, + page: { + error, + params: /** @type {Record} */ (event.params), + route: event.route, + status, + url: event.url, + data: {}, + form: form_value, + state: {} + } }; let current_node = props.root; - let data = {}; + let data = props.page.data; for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; @@ -168,16 +178,7 @@ export async function render_response({ } } - props.page = { - error, - params: /** @type {Record} */ (event.params), - route: event.route, - status, - url: event.url, - data, - form: form_value, - state: {} - }; + props.page.data = data; const render_state = { ...event_state, is_in_render: true }; From 6f2924b9c17ccd83d769a999fdf0edbb65270129 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 11 Jul 2026 09:50:02 -0400 Subject: [PATCH 26/29] rename, for sanity --- packages/kit/src/runtime/client/client.js | 4 ++-- packages/kit/src/runtime/client/types.d.ts | 2 +- packages/kit/src/runtime/components/root.svelte | 6 +++--- packages/kit/src/runtime/server/page/render.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a5809803725d..c24071b01add 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -807,7 +807,7 @@ async function get_navigation_result_from_branch({ }, props: { page, - root: /** @type {RenderNode} */ ({}) + tree: /** @type {RenderNode} */ ({}) } }; @@ -822,7 +822,7 @@ async function get_navigation_result_from_branch({ let data = {}; let data_changed = !page; - let current_node = result.props.root; + let current_node = result.props.tree; for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index ea17a57d2eef..33595faab083 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -93,7 +93,7 @@ export type NavigationFinished = { page: Page; form?: Record | null; error?: App.Error; - root: RenderNode; + tree: RenderNode; }; }; diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte index 35ef0feaee85..f419b68cbd7d 100644 --- a/packages/kit/src/runtime/components/root.svelte +++ b/packages/kit/src/runtime/components/root.svelte @@ -5,14 +5,14 @@ interface Props { page: Page; - root: RenderNode; + tree: RenderNode; components: any[]; resetters: Array<(() => void) | undefined>; form?: any; error?: App.Error; } - const { page, components, resetters, root, form, error }: Props = $props(); + const { page, components, resetters, tree, form, error }: Props = $props(); let mounted = $state(false); let navigated = $state(false); @@ -49,7 +49,7 @@ {/snippet} -{@render node(root, 0)} +{@render node(tree, 0)} {#if mounted}
Date: Sat, 11 Jul 2026 10:22:04 -0400 Subject: [PATCH 27/29] preserve fine-grained-ness, albeit hackily --- packages/kit/src/runtime/client/client.js | 36 +++++++++++++++---- .../kit/src/runtime/components/root.svelte | 5 +-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c24071b01add..3a5248f6d1aa 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -91,6 +91,11 @@ const scroll_positions = storage.get(SCROLL_KEY) ?? {}; */ const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; +/** + * @deprecated this is a temporary measure to avoid a regression, replace with nested `RenderNode` classes + */ +let current_tree = /** @type {RenderNode} */ ({}); + if (DEV && BROWSER) { let warned = false; @@ -487,6 +492,7 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru current = { ...navigation_result.state, nav: current.nav }; reset_invalidation(); root.$set(navigation_result.props); + current_tree = navigation_result.props.tree; } else { reset_invalidation(); } @@ -655,6 +661,7 @@ async function _preload_data(intent) { return svelte.fork(() => { root.$set(result.props); update(result.props.page); + current_tree = result.props.tree; }); } catch { // if it errors, it's because the experimental flag isn't enabled in Svelte @@ -713,6 +720,7 @@ async function initialize(result, target, hydrate) { } update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page)); + current_tree = result.props.tree; // TODO: use mount() root = new Root({ @@ -824,24 +832,39 @@ async function get_navigation_result_from_branch({ let current_node = result.props.tree; + /** @type {RenderNode | undefined} */ + let previous_node = current_tree; + for (let i = 0; i < branch.length; i += 1) { const node = branch[i]; const prev = current.branch[i]; - if (node?.data !== prev?.data) data_changed = true; if (!node) continue; - data = { ...data, ...node.data }; - const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader; current_node.error = (await error_loader()).component; current_node.component = node.node.component; - current_node.data = data; + + if ( + !previous_node || + node?.data !== prev?.data || + node.node.component !== prev.node.component + ) { + current_node.data = { ...data, ...node.data }; + data_changed = true; + } else { + // use existing object — prevents effects re-running unnecessarily + current_node.data = previous_node.data; + } + + data = current_node.data; if (i < branch.length - 1) { current_node.child = /** @type {import('../types.js').RenderNode} */ ({}); current_node = current_node.child; + + previous_node = previous_node?.child; } } @@ -863,8 +886,7 @@ async function get_navigation_result_from_branch({ status: status ?? error?.status ?? 200, url: new URL(url), form: form ?? null, - // The whole page store is updated, but this way the object reference stays the same - data: data_changed ? data : page.data + data }; } @@ -1927,6 +1949,7 @@ async function navigate({ } else { rendering_error = null; // TODO this can break with forks, rethink for SvelteKit 3 where we can assume Svelte 5 root.$set(navigation_result.props); + current_tree = navigation_result.props.tree; // Reset any boundaries that failed on a previous navigation now that the // new props are applied, otherwise the stale `+error.svelte` stays // mounted above the new route's content. See sveltejs/kit#15694. @@ -2625,6 +2648,7 @@ export async function set_nearest_error_page(error) { current = { ...navigation_result.state, nav: current.nav }; root.$set(navigation_result.props); + current_tree = navigation_result.props.tree; update(navigation_result.props.page); void svelte.tick().then(() => reset_focus(current.url)); diff --git a/packages/kit/src/runtime/components/root.svelte b/packages/kit/src/runtime/components/root.svelte index f419b68cbd7d..4b230208ba66 100644 --- a/packages/kit/src/runtime/components/root.svelte +++ b/packages/kit/src/runtime/components/root.svelte @@ -31,16 +31,17 @@ {#snippet node(n: RenderNode, depth: number)} {const Component = $derived(n.component)} {const Error = $derived(n.error)} + {const data = $derived(n.data)} (resetters[depth] = reset)}> {#if n.child} - + {@render node(n.child, depth + 1)} {:else} - + {/if} {#snippet failed(error)} From 3665618541c20e23216ac722fae29d790c4a1ff0 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:43:08 +0000 Subject: [PATCH 28/29] Fix: A page/layout displays STALE ancestor (layout) data when an ancestor's data changes but the descendant's own `load` does not re-run, because the data-reuse `else` branch reassigns the previous render's fully-merged data. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at packages/kit/src/runtime/client/client.js:845 ## Bug In `get_navigation_result_from_branch` (`packages/kit/src/runtime/client/client.js`, the `if` block now at ~line 845 after merge `4821929`), the fine-grained data-reuse optimization decides per-node whether to reuse the previous render's merged `data` object: ```js if ( !previous_node || node?.data !== prev?.data || node.node.component !== prev.node.component ) { current_node.data = { ...data, ...node.data }; data_changed = true; } else { // reuse previous object current_node.data = previous_node.data; } data = current_node.data; ``` The reuse condition only inspects the node's **own** segment data reference (`node.data !== prev.data`) and its component. It ignores whether the accumulated **ancestor** `data` (built from parent layouts earlier in this linear path) changed. `previous_node.data` is the previous render's fully-merged object, which contains the OLD ancestor values. ### Concrete trigger 1. A `+layout.js` load re-runs (e.g. after `invalidate()`) and returns a new object → at the layout's index `node.data !== prev.data`, so `data` becomes the fresh layout data and `data_changed` is set. 2. A descendant `+page` that does **not** call `await parent()` and isn't otherwise invalidated: in `load_route`, `has_changed` only forces a re-run when `uses.parent && parent_changed`, so it returns the **same** previous `BranchNode` (`if (valid) return previous;`). Hence at the page's index `node.data === prev.data` and the component is unchanged. 3. The page therefore hits the `else` branch and gets `current_node.data = previous_node.data` — the OLD merged data with STALE layout values — instead of `{ ...newLayoutData, ...pageOwnData }`. Result: the page's `data` prop and the top-level `page.data` store show outdated layout data, breaking the guarantee that a page's `data` reflects the merge of all ancestor layout data even without `await parent()`. ## Fix Add `data_changed ||` to the front of the condition. Because the loop walks the active linear path top-down (layout → … → page) and `data_changed` is only ever set to `true`, at any iteration `data_changed === true` means an ancestor earlier in the path changed. In that case the accumulated `data` differs from the previous render, so the node must re-merge (`{ ...data, ...node.data }`) rather than reuse `previous_node.data`. ```js if ( data_changed || // an ancestor changed → accumulated data differs → must re-merge !previous_node || node?.data !== prev?.data || node.node.component !== prev.node.component ) { current_node.data = { ...data, ...node.data }; data_changed = true; } else { current_node.data = previous_node.data; } ``` This preserves the original fine-grained reuse (object identity is retained when nothing upstream in the path changed and the node's own data/component are unchanged), while correctly propagating updated ancestor data to descendants. The existing use of `data_changed` for `page_changed` (~line 872) is unaffected, since in the newly-covered cases it was already `true` due to the changed ancestor. ## Status Verified against the post-merge code (commit `4821929`). The reuse condition still lacks `data_changed ||`; the bug is unaddressed. The existing patch applies cleanly (git fuzz absorbs the line shift). Line number updated from 858 → 845. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/kit/src/runtime/client/client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 45035454bb3e..a128ee548c62 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -843,6 +843,9 @@ async function get_navigation_result_from_branch({ current_node.component = node.node.component; if ( + // if an ancestor node in this path changed, `data_changed` is already true and the + // accumulated `data` differs from the previous render, so we must re-merge + data_changed || !previous_node || node?.data !== prev?.data || node.node.component !== prev.node.component From 21cf76ee078b0de627b662d007c70e9040c71f52 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 11 Jul 2026 20:22:24 -0400 Subject: [PATCH 29/29] fix --- packages/kit/src/runtime/client/client.js | 3 +-- .../basics/src/routes/state/data/state-update/+layout.svelte | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a128ee548c62..cd55a9c83207 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -847,8 +847,7 @@ async function get_navigation_result_from_branch({ // accumulated `data` differs from the previous render, so we must re-merge data_changed || !previous_node || - node?.data !== prev?.data || - node.node.component !== prev.node.component + node?.data !== prev?.data ) { current_node.data = { ...data, ...node.data }; data_changed = true; diff --git a/packages/kit/test/apps/basics/src/routes/state/data/state-update/+layout.svelte b/packages/kit/test/apps/basics/src/routes/state/data/state-update/+layout.svelte index 5e83135ec2a4..7c4d3062880c 100644 --- a/packages/kit/test/apps/basics/src/routes/state/data/state-update/+layout.svelte +++ b/packages/kit/test/apps/basics/src/routes/state/data/state-update/+layout.svelte @@ -15,7 +15,7 @@

page.data was updated {count} time(s)

-a -b +a +b {@render children()}