Skip to content

Commit 1ddb40e

Browse files
committed
feat: use devalue for pushState/replaceState
1 parent d89d6e2 commit 1ddb40e

File tree

5 files changed

+46
-32
lines changed

5 files changed

+46
-32
lines changed

packages/kit/src/runtime/client/client.js

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ import { compact } from '../../utils/array.js';
4242
import {
4343
INVALIDATED_PARAM,
4444
TRAILING_SLASH_PARAM,
45+
validate_load_response,
46+
stringify,
4547
validate_depends,
46-
validate_load_response
48+
parse as unstringify
4749
} from '../shared.js';
4850
import { get_message, get_status } from '../../utils/error.js';
4951
import { writable } from 'svelte/store';
@@ -1585,7 +1587,7 @@ async function navigate({
15851587
const entry = {
15861588
[HISTORY_INDEX]: (current_history_index += change),
15871589
[NAVIGATION_INDEX]: (current_navigation_index += change),
1588-
[STATES_KEY]: state
1590+
[STATES_KEY]: stringify(state, app.hooks.transport)
15891591
};
15901592

15911593
const fn = replace_state ? history.replaceState : history.pushState;
@@ -2139,18 +2141,8 @@ export function pushState(url, state) {
21392141
throw new Error('Cannot call pushState(...) on the server');
21402142
}
21412143

2142-
if (DEV) {
2143-
if (!started) {
2144-
throw new Error('Cannot call pushState(...) before router is initialized');
2145-
}
2146-
2147-
try {
2148-
// use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2149-
devalue.stringify(state);
2150-
} catch (error) {
2151-
// @ts-expect-error
2152-
throw new Error(`Could not serialize state${error.path}`);
2153-
}
2144+
if (DEV && !started) {
2145+
throw new Error('Cannot call pushState(...) before router is initialized');
21542146
}
21552147

21562148
update_scroll_positions(current_history_index);
@@ -2159,7 +2151,7 @@ export function pushState(url, state) {
21592151
[HISTORY_INDEX]: (current_history_index += 1),
21602152
[NAVIGATION_INDEX]: current_navigation_index,
21612153
[PAGE_URL_KEY]: page.url.href,
2162-
[STATES_KEY]: state
2154+
[STATES_KEY]: stringify(state, app.hooks.transport)
21632155
};
21642156

21652157
history.pushState(opts, '', resolve_url(url));
@@ -2186,25 +2178,15 @@ export function replaceState(url, state) {
21862178
throw new Error('Cannot call replaceState(...) on the server');
21872179
}
21882180

2189-
if (DEV) {
2190-
if (!started) {
2191-
throw new Error('Cannot call replaceState(...) before router is initialized');
2192-
}
2193-
2194-
try {
2195-
// use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2196-
devalue.stringify(state);
2197-
} catch (error) {
2198-
// @ts-expect-error
2199-
throw new Error(`Could not serialize state${error.path}`);
2200-
}
2181+
if (DEV && !started) {
2182+
throw new Error('Cannot call replaceState(...) before router is initialized');
22012183
}
22022184

22032185
const opts = {
22042186
[HISTORY_INDEX]: current_history_index,
22052187
[NAVIGATION_INDEX]: current_navigation_index,
22062188
[PAGE_URL_KEY]: page.url.href,
2207-
[STATES_KEY]: state
2189+
[STATES_KEY]: stringify(state, app.hooks.transport)
22082190
};
22092191

22102192
history.replaceState(opts, '', resolve_url(url));
@@ -2514,7 +2496,7 @@ function _start_router() {
25142496
if (history_index === current_history_index) return;
25152497

25162498
const scroll = scroll_positions[history_index];
2517-
const state = event.state[STATES_KEY] ?? {};
2499+
const state = unstringify(event.state[STATES_KEY], app.hooks.transport) ?? {};
25182500
const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
25192501
const navigation_index = event.state[NAVIGATION_INDEX];
25202502
const is_hash_change = current.url ? strip_hash(location) === strip_hash(current.url) : false;

packages/kit/src/runtime/shared.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ export function stringify_remote_arg(value, transport) {
6666
return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
6767
}
6868

69+
/**
70+
* Parses `string` with `devalue.parse`, using the provided transport decoders.
71+
* @param {string} string
72+
* @param {Transport} transport
73+
*/
74+
export function parse(string, transport) {
75+
const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));
76+
77+
return devalue.parse(string, decoders);
78+
}
79+
6980
/**
7081
* Parses the argument (if any) for a remote function
7182
* @param {string} string
@@ -79,9 +90,7 @@ export function parse_remote_arg(string, transport) {
7990
base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
8091
);
8192

82-
const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));
83-
84-
return devalue.parse(json_string, decoders);
93+
return parse(json_string, transport);
8594
}
8695

8796
/**

packages/kit/test/apps/basics/src/app.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Foo } from '$lib';
2+
13
declare global {
24
namespace App {
35
interface Locals {
@@ -12,6 +14,7 @@ declare global {
1214
interface PageState {
1315
active?: boolean;
1416
count?: number;
17+
foo?: Foo;
1518
}
1619
}
1720
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
import { pushState } from '$app/navigation';
3+
import { page } from '$app/state';
4+
import { Foo } from '$lib';
5+
</script>
6+
7+
<button onclick={() => pushState('', { foo: new Foo('it works?') })}>do the thing</button>
8+
9+
<p>foo: {page.state.foo?.bar() ?? 'nope'}</p>

packages/kit/test/apps/basics/test/client.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,17 @@ test.describe('Shallow routing', () => {
14971497
await page.locator('button').click();
14981498
await expect(page.locator('p')).toHaveText('count: 1');
14991499
});
1500+
1501+
test('pushState properly serializes objects', async ({ page }) => {
1502+
await page.goto('/shallow-routing/push-state/foo');
1503+
await expect(page.locator('p')).toHaveText('foo: nope');
1504+
1505+
await page.locator('button').click();
1506+
await expect(page.locator('p')).toHaveText('foo: it works?!');
1507+
1508+
await page.goBack();
1509+
await expect(page.locator('p')).toHaveText('foo: nope');
1510+
});
15001511
});
15011512

15021513
test.describe('reroute', () => {

0 commit comments

Comments
 (0)