Skip to content

Commit 2c3304d

Browse files
committed
feat: use devalue for pushState/replaceState
1 parent 6c13063 commit 2c3304d

File tree

5 files changed

+50
-32
lines changed

5 files changed

+50
-32
lines changed

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

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ import {
3939
} from './constants.js';
4040
import { validate_page_exports } from '../../utils/exports.js';
4141
import { compact } from '../../utils/array.js';
42-
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js';
42+
import {
43+
INVALIDATED_PARAM,
44+
stringify,
45+
TRAILING_SLASH_PARAM,
46+
validate_depends,
47+
parse as unstringify
48+
} from '../shared.js';
4349
import { get_message, get_status } from '../../utils/error.js';
4450
import { writable } from 'svelte/store';
4551
import { page, update, navigating } from './state.svelte.js';
@@ -1590,7 +1596,7 @@ async function navigate({
15901596
const entry = {
15911597
[HISTORY_INDEX]: (current_history_index += change),
15921598
[NAVIGATION_INDEX]: (current_navigation_index += change),
1593-
[STATES_KEY]: state
1599+
[STATES_KEY]: stringify(state, app.hooks.transport)
15941600
};
15951601

15961602
const fn = replace_state ? history.replaceState : history.pushState;
@@ -2144,18 +2150,8 @@ export function pushState(url, state) {
21442150
throw new Error('Cannot call pushState(...) on the server');
21452151
}
21462152

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

21612157
update_scroll_positions(current_history_index);
@@ -2164,7 +2160,7 @@ export function pushState(url, state) {
21642160
[HISTORY_INDEX]: (current_history_index += 1),
21652161
[NAVIGATION_INDEX]: current_navigation_index,
21662162
[PAGE_URL_KEY]: page.url.href,
2167-
[STATES_KEY]: state
2163+
[STATES_KEY]: stringify(state, app.hooks.transport)
21682164
};
21692165

21702166
history.pushState(opts, '', resolve_url(url));
@@ -2191,25 +2187,15 @@ export function replaceState(url, state) {
21912187
throw new Error('Cannot call replaceState(...) on the server');
21922188
}
21932189

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

22082194
const opts = {
22092195
[HISTORY_INDEX]: current_history_index,
22102196
[NAVIGATION_INDEX]: current_navigation_index,
22112197
[PAGE_URL_KEY]: page.url.href,
2212-
[STATES_KEY]: state
2198+
[STATES_KEY]: stringify(state, app.hooks.transport)
22132199
};
22142200

22152201
history.replaceState(opts, '', resolve_url(url));
@@ -2519,7 +2505,7 @@ function _start_router() {
25192505
if (history_index === current_history_index) return;
25202506

25212507
const scroll = scroll_positions[history_index];
2522-
const state = event.state[STATES_KEY] ?? {};
2508+
const state = unstringify(event.state[STATES_KEY], app.hooks.transport) ?? {};
25232509
const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
25242510
const navigation_index = event.state[NAVIGATION_INDEX];
25252511
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
@@ -49,6 +49,17 @@ export function stringify_remote_arg(value, transport) {
4949
.replace(/\//g, '_');
5050
}
5151

52+
/**
53+
* Parses `string` with `devalue.parse`, using the provided transport decoders.
54+
* @param {string} string
55+
* @param {Transport} transport
56+
*/
57+
export function parse(string, transport) {
58+
const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));
59+
60+
return devalue.parse(string, decoders);
61+
}
62+
5263
/**
5364
* Parses the argument (if any) for a remote function
5465
* @param {string} string
@@ -57,15 +68,13 @@ export function stringify_remote_arg(value, transport) {
5768
export function parse_remote_arg(string, transport) {
5869
if (!string) return undefined;
5970

60-
const decoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.decode]));
61-
6271
// We don't need to add back the `=`-padding because atob can handle it
6372
const base64_restored = string.replace(/-/g, '+').replace(/_/g, '/');
6473
const binary_string = atob(base64_restored);
6574
const utf8_bytes = new Uint8Array([...binary_string].map((char) => char.charCodeAt(0)));
6675
const json_string = new TextDecoder().decode(utf8_bytes);
6776

68-
return devalue.parse(json_string, decoders);
77+
return parse(json_string, transport);
6978
}
7079

7180
/**

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
@@ -1491,6 +1491,17 @@ test.describe('Shallow routing', () => {
14911491
await page.locator('button').click();
14921492
await expect(page.locator('p')).toHaveText('count: 1');
14931493
});
1494+
1495+
test('pushState properly serializes objects', async ({ page }) => {
1496+
await page.goto('/shallow-routing/push-state/foo');
1497+
await expect(page.locator('p')).toHaveText('foo: nope');
1498+
1499+
await page.locator('button').click();
1500+
await expect(page.locator('p')).toHaveText('foo: it works?!');
1501+
1502+
await page.goBack();
1503+
await expect(page.locator('p')).toHaveText('foo: nope');
1504+
});
14941505
});
14951506

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

0 commit comments

Comments
 (0)