Skip to content

Commit 205c678

Browse files
committed
feat: use devalue for pushState/replaceState
1 parent b5000df commit 205c678

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';
@@ -1641,7 +1643,7 @@ async function navigate({
16411643
const entry = {
16421644
[HISTORY_INDEX]: (current_history_index += change),
16431645
[NAVIGATION_INDEX]: (current_navigation_index += change),
1644-
[STATES_KEY]: state
1646+
[STATES_KEY]: stringify(state, app.hooks.transport)
16451647
};
16461648

16471649
const fn = replace_state ? history.replaceState : history.pushState;
@@ -2198,18 +2200,8 @@ export function pushState(url, state) {
21982200
throw new Error('Cannot call pushState(...) on the server');
21992201
}
22002202

2201-
if (DEV) {
2202-
if (!started) {
2203-
throw new Error('Cannot call pushState(...) before router is initialized');
2204-
}
2205-
2206-
try {
2207-
// use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2208-
devalue.stringify(state);
2209-
} catch (error) {
2210-
// @ts-expect-error
2211-
throw new Error(`Could not serialize state${error.path}`);
2212-
}
2203+
if (DEV && !started) {
2204+
throw new Error('Cannot call pushState(...) before router is initialized');
22132205
}
22142206

22152207
update_scroll_positions(current_history_index);
@@ -2218,7 +2210,7 @@ export function pushState(url, state) {
22182210
[HISTORY_INDEX]: (current_history_index += 1),
22192211
[NAVIGATION_INDEX]: current_navigation_index,
22202212
[PAGE_URL_KEY]: page.url.href,
2221-
[STATES_KEY]: state
2213+
[STATES_KEY]: stringify(state, app.hooks.transport)
22222214
};
22232215

22242216
history.pushState(opts, '', resolve_url(url));
@@ -2245,25 +2237,15 @@ export function replaceState(url, state) {
22452237
throw new Error('Cannot call replaceState(...) on the server');
22462238
}
22472239

2248-
if (DEV) {
2249-
if (!started) {
2250-
throw new Error('Cannot call replaceState(...) before router is initialized');
2251-
}
2252-
2253-
try {
2254-
// use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2255-
devalue.stringify(state);
2256-
} catch (error) {
2257-
// @ts-expect-error
2258-
throw new Error(`Could not serialize state${error.path}`);
2259-
}
2240+
if (DEV && !started) {
2241+
throw new Error('Cannot call replaceState(...) before router is initialized');
22602242
}
22612243

22622244
const opts = {
22632245
[HISTORY_INDEX]: current_history_index,
22642246
[NAVIGATION_INDEX]: current_navigation_index,
22652247
[PAGE_URL_KEY]: page.url.href,
2266-
[STATES_KEY]: state
2248+
[STATES_KEY]: stringify(state, app.hooks.transport)
22672249
};
22682250

22692251
history.replaceState(opts, '', resolve_url(url));
@@ -2570,7 +2552,7 @@ function _start_router() {
25702552
if (history_index === current_history_index) return;
25712553

25722554
const scroll = scroll_positions[history_index];
2573-
const state = event.state[STATES_KEY] ?? {};
2555+
const state = unstringify(event.state[STATES_KEY], app.hooks.transport) ?? {};
25742556
const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
25752557
const navigation_index = event.state[NAVIGATION_INDEX];
25762558
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
@@ -1621,6 +1621,17 @@ test.describe('Shallow routing', () => {
16211621
await page.locator('button').click();
16221622
await expect(page.locator('p')).toHaveText('count: 1');
16231623
});
1624+
1625+
test('pushState properly serializes objects', async ({ page }) => {
1626+
await page.goto('/shallow-routing/push-state/foo');
1627+
await expect(page.locator('p')).toHaveText('foo: nope');
1628+
1629+
await page.locator('button').click();
1630+
await expect(page.locator('p')).toHaveText('foo: it works?!');
1631+
1632+
await page.goBack();
1633+
await expect(page.locator('p')).toHaveText('foo: nope');
1634+
});
16241635
});
16251636

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

0 commit comments

Comments
 (0)