diff --git a/.changeset/brown-seals-rhyme.md b/.changeset/brown-seals-rhyme.md
new file mode 100644
index 000000000000..ecdcb7b1f756
--- /dev/null
+++ b/.changeset/brown-seals-rhyme.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: correctly decode custom types streamed from a server load function
diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js
index f4d0a352f628..1142609fe859 100644
--- a/packages/kit/src/runtime/app/forms.js
+++ b/packages/kit/src/runtime/app/forms.js
@@ -33,7 +33,7 @@ export function deserialize(result) {
if (parsed.data) {
// the decoders should never be initialised at the top-level because `app`
- // will not initialised yet if `kit.output.bundleStrategy` is 'single' or 'inline'
+ // will not be initialised yet if `kit.output.bundleStrategy` is 'single' or 'inline'
parsed.data = devalue.parse(parsed.data, BROWSER ? client_app.decoders : server_app.decoders);
}
diff --git a/packages/kit/src/runtime/client/bundle.js b/packages/kit/src/runtime/client/bundle.js
index 7a915d3110a4..f375d02e8355 100644
--- a/packages/kit/src/runtime/client/bundle.js
+++ b/packages/kit/src/runtime/client/bundle.js
@@ -13,3 +13,5 @@ import * as app from '__sveltekit/manifest';
export function start(element, options) {
void kit.start(app, element, options);
}
+
+export { app };
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 0d22352839bf..bb55ce707613 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -366,9 +366,28 @@ export async function render_response({
deferred.set(id, { fulfil, reject });
})`);
+ let app_declaration = '';
+
+ if (Object.keys(options.hooks.transport).length > 0) {
+ if (client.inline) {
+ app_declaration = `const app = __sveltekit_${options.version_hash}.app.app;`;
+ } else if (client.app) {
+ app_declaration = `const app = await import(${s(prefixed(client.app))});`;
+ } else {
+ app_declaration = `const { app } = await import(${s(prefixed(client.start))});`;
+ }
+ }
+
+ const prelude = app_declaration
+ ? `${app_declaration}
+ const [data, error] = fn(app);`
+ : `const [data, error] = fn();`;
+
// When resolving, the id might not yet be available due to the data
// be evaluated upon init of kit, so we use a timeout to retry
- properties.push(`resolve: ({ id, data, error }) => {
+ properties.push(`resolve: async (id, fn) => {
+ ${prelude}
+
const try_to_resolve = () => {
if (!deferred.has(id)) {
setTimeout(try_to_resolve, 0);
@@ -652,7 +671,7 @@ function get_data(event, event_state, options, nodes, csp, global) {
let str;
try {
- str = devalue.uneval({ id, data, error }, replacer);
+ str = devalue.uneval(error ? [, error] : [data], replacer);
} catch {
error = await handle_error_and_jsonify(
event,
@@ -661,11 +680,13 @@ function get_data(event, event_state, options, nodes, csp, global) {
new Error(`Failed to serialize promise while rendering ${event.route.id}`)
);
data = undefined;
- str = devalue.uneval({ id, data, error }, replacer);
+ str = devalue.uneval([, error], replacer);
}
const nonce = csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : '';
- push(`\n`);
+ push(
+ `\n`
+ );
if (count === 0) done();
}
);
diff --git a/packages/kit/test/apps/basics/src/routes/serialization-stream/+page.server.js b/packages/kit/test/apps/basics/src/routes/serialization-stream/+page.server.js
new file mode 100644
index 000000000000..957f34dc1b76
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/serialization-stream/+page.server.js
@@ -0,0 +1,7 @@
+import { Foo } from '$lib';
+
+export const load = () => {
+ return {
+ foo: Promise.resolve(new Foo('It works'))
+ };
+};
diff --git a/packages/kit/test/apps/basics/src/routes/serialization-stream/+page.svelte b/packages/kit/test/apps/basics/src/routes/serialization-stream/+page.svelte
new file mode 100644
index 000000000000..75c6a2f7a800
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/serialization-stream/+page.svelte
@@ -0,0 +1,11 @@
+
+
+
+ {#await data.foo}
+ Loading...
+ {:then result}
+ {result.bar()}
+ {/await}
+
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 55db0bf20db6..ac9755b2c741 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1560,6 +1560,13 @@ test.describe('Serialization', () => {
await page.click('button');
await expect(page.locator('h1')).toHaveText('It works!');
});
+
+ test('works with streaming', async ({ page, javaScriptEnabled }) => {
+ test.skip(!javaScriptEnabled, 'skip when JavaScript is disabled');
+
+ await page.goto('/serialization-stream');
+ await expect(page.locator('h1', { hasText: 'It works!' })).toBeVisible();
+ });
});
test.describe('getRequestEvent', () => {
diff --git a/packages/kit/test/apps/options-2/src/hooks.js b/packages/kit/test/apps/options-2/src/hooks.js
new file mode 100644
index 000000000000..238cc7744f2c
--- /dev/null
+++ b/packages/kit/test/apps/options-2/src/hooks.js
@@ -0,0 +1,9 @@
+import { Foo } from '$lib';
+
+/** @type {import("@sveltejs/kit").Transport} */
+export const transport = {
+ Foo: {
+ encode: (value) => value instanceof Foo && [value.message],
+ decode: ([message]) => new Foo(message)
+ }
+};
diff --git a/packages/kit/test/apps/options-2/src/lib/index.js b/packages/kit/test/apps/options-2/src/lib/index.js
new file mode 100644
index 000000000000..f9917fd877e2
--- /dev/null
+++ b/packages/kit/test/apps/options-2/src/lib/index.js
@@ -0,0 +1,9 @@
+export class Foo {
+ constructor(message) {
+ this.message = message;
+ }
+
+ bar() {
+ return this.message + '!';
+ }
+}
diff --git a/packages/kit/test/apps/options-2/src/routes/serialization-stream/+page.server.js b/packages/kit/test/apps/options-2/src/routes/serialization-stream/+page.server.js
new file mode 100644
index 000000000000..957f34dc1b76
--- /dev/null
+++ b/packages/kit/test/apps/options-2/src/routes/serialization-stream/+page.server.js
@@ -0,0 +1,7 @@
+import { Foo } from '$lib';
+
+export const load = () => {
+ return {
+ foo: Promise.resolve(new Foo('It works'))
+ };
+};
diff --git a/packages/kit/test/apps/options-2/src/routes/serialization-stream/+page.svelte b/packages/kit/test/apps/options-2/src/routes/serialization-stream/+page.svelte
new file mode 100644
index 000000000000..75c6a2f7a800
--- /dev/null
+++ b/packages/kit/test/apps/options-2/src/routes/serialization-stream/+page.svelte
@@ -0,0 +1,11 @@
+
+
+
+ {#await data.foo}
+ Loading...
+ {:then result}
+ {result.bar()}
+ {/await}
+
diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js
index 4af842e6f7ee..f3df40146a8d 100644
--- a/packages/kit/test/apps/options-2/test/test.js
+++ b/packages/kit/test/apps/options-2/test/test.js
@@ -190,4 +190,9 @@ test.describe("bundleStrategy: 'single'", () => {
await page.goto('/basepath/deserialize');
await expect(page.locator('p')).toHaveText('Hello world!');
});
+
+ test('serialization works with streaming', async ({ page }) => {
+ await page.goto('/basepath/serialization-stream');
+ await expect(page.locator('h1', { hasText: 'It works!' })).toBeVisible();
+ });
});
diff --git a/packages/kit/test/apps/options-3/package.json b/packages/kit/test/apps/options-3/package.json
new file mode 100644
index 000000000000..78543175148f
--- /dev/null
+++ b/packages/kit/test/apps/options-3/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "test-options-3",
+ "private": true,
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync",
+ "check": "svelte-kit sync && tsc && svelte-check",
+ "test": "playwright test"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-node": "workspace:^",
+ "@sveltejs/kit": "workspace:^",
+ "@sveltejs/vite-plugin-svelte": "catalog:",
+ "svelte": "^5.35.5",
+ "svelte-check": "^4.1.1",
+ "typescript": "^5.5.4",
+ "vite": "catalog:"
+ },
+ "type": "module"
+}
diff --git a/packages/kit/test/apps/options-3/playwright.config.js b/packages/kit/test/apps/options-3/playwright.config.js
new file mode 100644
index 000000000000..33d36b651014
--- /dev/null
+++ b/packages/kit/test/apps/options-3/playwright.config.js
@@ -0,0 +1 @@
+export { config as default } from '../../utils.js';
diff --git a/packages/kit/test/apps/options-3/src/app.html b/packages/kit/test/apps/options-3/src/app.html
new file mode 100644
index 000000000000..b8ba4699b2ba
--- /dev/null
+++ b/packages/kit/test/apps/options-3/src/app.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/packages/kit/test/apps/options-3/src/hooks.js b/packages/kit/test/apps/options-3/src/hooks.js
new file mode 100644
index 000000000000..238cc7744f2c
--- /dev/null
+++ b/packages/kit/test/apps/options-3/src/hooks.js
@@ -0,0 +1,9 @@
+import { Foo } from '$lib';
+
+/** @type {import("@sveltejs/kit").Transport} */
+export const transport = {
+ Foo: {
+ encode: (value) => value instanceof Foo && [value.message],
+ decode: ([message]) => new Foo(message)
+ }
+};
diff --git a/packages/kit/test/apps/options-3/src/lib/index.js b/packages/kit/test/apps/options-3/src/lib/index.js
new file mode 100644
index 000000000000..f9917fd877e2
--- /dev/null
+++ b/packages/kit/test/apps/options-3/src/lib/index.js
@@ -0,0 +1,9 @@
+export class Foo {
+ constructor(message) {
+ this.message = message;
+ }
+
+ bar() {
+ return this.message + '!';
+ }
+}
diff --git a/packages/kit/test/apps/options-3/src/routes/+layout.svelte b/packages/kit/test/apps/options-3/src/routes/+layout.svelte
new file mode 100644
index 000000000000..5e1f1fed86c2
--- /dev/null
+++ b/packages/kit/test/apps/options-3/src/routes/+layout.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/packages/kit/test/apps/options-3/src/routes/serialization-stream/+page.server.js b/packages/kit/test/apps/options-3/src/routes/serialization-stream/+page.server.js
new file mode 100644
index 000000000000..957f34dc1b76
--- /dev/null
+++ b/packages/kit/test/apps/options-3/src/routes/serialization-stream/+page.server.js
@@ -0,0 +1,7 @@
+import { Foo } from '$lib';
+
+export const load = () => {
+ return {
+ foo: Promise.resolve(new Foo('It works'))
+ };
+};
diff --git a/packages/kit/test/apps/options-3/src/routes/serialization-stream/+page.svelte b/packages/kit/test/apps/options-3/src/routes/serialization-stream/+page.svelte
new file mode 100644
index 000000000000..75c6a2f7a800
--- /dev/null
+++ b/packages/kit/test/apps/options-3/src/routes/serialization-stream/+page.svelte
@@ -0,0 +1,11 @@
+
+
+
+ {#await data.foo}
+ Loading...
+ {:then result}
+ {result.bar()}
+ {/await}
+
diff --git a/packages/kit/test/apps/options-3/svelte.config.js b/packages/kit/test/apps/options-3/svelte.config.js
new file mode 100644
index 000000000000..bcfcfccabb1f
--- /dev/null
+++ b/packages/kit/test/apps/options-3/svelte.config.js
@@ -0,0 +1,10 @@
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ kit: {
+ output: {
+ bundleStrategy: 'inline'
+ }
+ }
+};
+
+export default config;
diff --git a/packages/kit/test/apps/options-3/test/test.js b/packages/kit/test/apps/options-3/test/test.js
new file mode 100644
index 000000000000..b85f5c61265b
--- /dev/null
+++ b/packages/kit/test/apps/options-3/test/test.js
@@ -0,0 +1,16 @@
+import process from 'node:process';
+import { expect } from '@playwright/test';
+import { test } from '../../../utils.js';
+
+/** @typedef {import('@playwright/test').Response} Response */
+
+test.describe.configure({ mode: 'parallel' });
+
+test.describe("bundleStrategy: 'inline'", () => {
+ test.skip(({ javaScriptEnabled }) => !javaScriptEnabled || !!process.env.DEV);
+
+ test('serialization works with streaming', async ({ page }) => {
+ await page.goto('/serialization-stream');
+ await expect(page.locator('h1', { hasText: 'It works!' })).toBeVisible();
+ });
+});
diff --git a/packages/kit/test/apps/options-3/tsconfig.json b/packages/kit/test/apps/options-3/tsconfig.json
new file mode 100644
index 000000000000..b1096bf168cd
--- /dev/null
+++ b/packages/kit/test/apps/options-3/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true
+ },
+ "extends": "./.svelte-kit/tsconfig.json"
+}
diff --git a/packages/kit/test/apps/options-3/vite.config.js b/packages/kit/test/apps/options-3/vite.config.js
new file mode 100644
index 000000000000..69200cdb7cd8
--- /dev/null
+++ b/packages/kit/test/apps/options-3/vite.config.js
@@ -0,0 +1,18 @@
+import * as path from 'node:path';
+import { sveltekit } from '@sveltejs/kit/vite';
+
+/** @type {import('vite').UserConfig} */
+const config = {
+ build: {
+ minify: false
+ },
+ clearScreen: false,
+ plugins: [sveltekit()],
+ server: {
+ fs: {
+ allow: [path.resolve('../../../src')]
+ }
+ }
+};
+
+export default config;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 643098904aaa..13b5d6dd4927 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -714,6 +714,30 @@ importers:
specifier: 'catalog:'
version: 6.3.5(@types/node@18.19.119)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
+ packages/kit/test/apps/options-3:
+ devDependencies:
+ '@sveltejs/adapter-node':
+ specifier: workspace:^
+ version: link:../../../../adapter-node
+ '@sveltejs/kit':
+ specifier: workspace:^
+ version: link:../../..
+ '@sveltejs/vite-plugin-svelte':
+ specifier: 'catalog:'
+ version: 6.0.0-next.3(svelte@5.35.5)(vite@6.3.5(@types/node@18.19.119)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))
+ svelte:
+ specifier: ^5.35.5
+ version: 5.35.5
+ svelte-check:
+ specifier: ^4.1.1
+ version: 4.1.1(picomatch@4.0.3)(svelte@5.35.5)(typescript@5.8.3)
+ typescript:
+ specifier: ^5.5.4
+ version: 5.8.3
+ vite:
+ specifier: 'catalog:'
+ version: 6.3.5(@types/node@18.19.119)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)
+
packages/kit/test/apps/prerendered-app-error-pages:
devDependencies:
'@sveltejs/kit':