diff --git a/.changeset/legal-peas-agree.md b/.changeset/legal-peas-agree.md new file mode 100644 index 000000000000..224d051b41ab --- /dev/null +++ b/.changeset/legal-peas-agree.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: layout load data not serialized on error page diff --git a/packages/kit/src/runtime/server/page/data_serializer.js b/packages/kit/src/runtime/server/page/data_serializer.js index 36e064ba0d35..fef0ae5bc364 100644 --- a/packages/kit/src/runtime/server/page/data_serializer.js +++ b/packages/kit/src/runtime/server/page/data_serializer.js @@ -18,61 +18,72 @@ import { */ export function server_data_serializer(event, event_state, options) { let promise_id = 1; + let max_nodes = -1; const iterator = create_async_iterator(); const global = get_global_name(options); - /** @param {any} thing */ - function replacer(thing) { - if (typeof thing?.then === 'function') { - const id = promise_id++; - - const promise = thing - .then(/** @param {any} data */ (data) => ({ data })) - .catch( - /** @param {any} error */ async (error) => ({ - error: await handle_error_and_jsonify(event, event_state, options, error) - }) - ) - .then( - /** - * @param {{data: any; error: any}} result - */ - async ({ data, error }) => { - let str; - try { - str = devalue.uneval(error ? [, error] : [data], replacer); - } catch { - error = await handle_error_and_jsonify( - event, - event_state, - options, - new Error(`Failed to serialize promise while rendering ${event.route.id}`) - ); - data = undefined; - str = devalue.uneval([, error], replacer); + /** @param {number} index */ + function get_replacer(index) { + /** @param {any} thing */ + return function replacer(thing) { + if (typeof thing?.then === 'function') { + const id = promise_id++; + + const promise = thing + .then(/** @param {any} data */ (data) => ({ data })) + .catch( + /** @param {any} error */ async (error) => ({ + error: await handle_error_and_jsonify(event, event_state, options, error) + }) + ) + .then( + /** + * @param {{data: any; error: any}} result + */ + async ({ data, error }) => { + let str; + try { + str = devalue.uneval(error ? [, error] : [data], replacer); + } catch { + error = await handle_error_and_jsonify( + event, + event_state, + options, + new Error(`Failed to serialize promise while rendering ${event.route.id}`) + ); + data = undefined; + str = devalue.uneval([, error], replacer); + } + + return { + index, + str: `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})` + }; } + ); - return `${global}.resolve(${id}, ${str.includes('app.decode') ? `(app) => ${str}` : `() => ${str}`})`; - } - ); + iterator.add(promise); - iterator.add(promise); - - return `${global}.defer(${id})`; - } else { - for (const key in options.hooks.transport) { - const encoded = options.hooks.transport[key].encode(thing); - if (encoded) { - return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`; + return `${global}.defer(${id})`; + } else { + for (const key in options.hooks.transport) { + const encoded = options.hooks.transport[key].encode(thing); + if (encoded) { + return `app.decode('${key}', ${devalue.uneval(encoded, replacer)})`; + } } } - } + }; } const strings = /** @type {string[]} */ ([]); return { + set_max_nodes(i) { + max_nodes = i; + }, + add_node(i, node) { try { if (!node) { @@ -84,7 +95,7 @@ export function server_data_serializer(event, event_state, options) { const payload = { type: 'data', data: node.data, uses: serialize_uses(node) }; if (node.slash) payload.slash = node.slash; - strings[i] = devalue.uneval(payload, replacer); + strings[i] = devalue.uneval(payload, get_replacer(i)); } catch (e) { // @ts-expect-error e.path = e.path.slice(1); @@ -97,8 +108,17 @@ export function server_data_serializer(event, event_state, options) { const close = `\n`; return { - data: `[${compact(strings).join(',')}]`, - chunks: promise_id > 1 ? iterator.iterate((str) => open + str + close) : null + data: `[${compact(max_nodes > -1 ? strings.slice(0, max_nodes) : strings).join(',')}]`, + chunks: + promise_id > 1 + ? iterator.iterate(({ index, str }) => { + if (max_nodes > -1 && index >= max_nodes) { + return ''; + } + + return open + str + close; + }) + : null }; } }; diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index c0efedefd9a9..15d315905ab6 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -281,6 +281,8 @@ export async function render_page( let j = i; while (!branch[j]) j -= 1; + data_serializer.set_max_nodes(j + 1); + const layouts = compact(branch.slice(0, j + 1)); const nodes = new PageNodes(layouts.map((layout) => layout.node)); @@ -303,7 +305,7 @@ export async function render_page( server_data: null }), fetched, - data_serializer: server_data_serializer(event, event_state, options) + data_serializer }); } } diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index bfe975fb37a9..f0d094d122c2 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -45,6 +45,7 @@ export interface Cookie { export type ServerDataSerializer = { add_node(i: number, node: ServerDataNode | null): void; get_data(csp: Csp): { data: string; chunks: AsyncIterable | null }; + set_max_nodes(i: number): void; }; export type ServerDataSerializerJson = { diff --git a/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+error.svelte b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+error.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+layout.server.js b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+layout.server.js new file mode 100644 index 000000000000..f1d54f24d996 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+layout.server.js @@ -0,0 +1,6 @@ +/** @type {import('./$types').LayoutServerLoad} */ +export function load() { + return { + answer: 42 + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+layout.svelte b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+layout.svelte new file mode 100644 index 000000000000..1030328152c1 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+layout.svelte @@ -0,0 +1,8 @@ + + + + +
{data.answer}
diff --git a/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+page.js b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+page.js new file mode 100644 index 000000000000..0b245b4eb003 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+page.js @@ -0,0 +1,6 @@ +import { error } from '@sveltejs/kit'; + +/** @type {import('@sveltejs/kit').Load} */ +export async function load() { + error(404, 'Not found'); +} diff --git a/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+page.svelte b/packages/kit/test/apps/basics/src/routes/errors/load-error-server/layout-data/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/test/apps/basics/test/cross-platform/test.js b/packages/kit/test/apps/basics/test/cross-platform/test.js index 7de5222bece0..d9bd0a929348 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/test.js @@ -331,6 +331,11 @@ test.describe('Errors', () => { expect(/** @type {Response} */ (response).status()).toBe(555); }); + test('server-side error from load() still has layout data', async ({ page }) => { + await page.goto('/errors/load-error-server/layout-data'); + expect(await page.textContent('#error-layout-data')).toBe('42'); + }); + test('error in endpoint', async ({ page, read_errors }) => { const res = await page.goto('/errors/endpoint');