From 69e056223b5b26f10cf7885dd5db73f42dd6acf6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 09:58:52 -0400 Subject: [PATCH 01/17] start implementing pushState/replaceState --- packages/kit/src/runtime/app/navigation.js | 4 + packages/kit/src/runtime/client/client.js | 103 +++++++++++++++--- packages/kit/src/runtime/client/constants.js | 5 +- packages/kit/src/runtime/client/types.d.ts | 6 +- packages/kit/src/runtime/client/utils.js | 8 +- .../kit/src/runtime/server/page/render.js | 3 +- packages/kit/types/ambient.d.ts | 10 ++ packages/kit/types/index.d.ts | 4 + 8 files changed, 125 insertions(+), 18 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 30fa41375531..14cd5deb895b 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -15,3 +15,7 @@ export const preloadCode = /* @__PURE__ */ client_method('preload_code'); export const beforeNavigate = /* @__PURE__ */ client_method('before_navigate'); export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); + +export const pushState = /* @__PURE__ */ client_method('push_state'); + +export const replaceState = /* @__PURE__ */ client_method('replace_state'); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 9d8997d34ef4..684c8ae80988 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -30,7 +30,13 @@ import { HttpError, Redirect } from '../control.js'; import { stores } from './singletons.js'; import { unwrap_promises } from '../../utils/promises.js'; import * as devalue from 'devalue'; -import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js'; +import { + INDEX_KEY, + PRELOAD_PRIORITIES, + SCROLL_KEY, + STATES_KEY, + SNAPSHOT_KEY +} from './constants.js'; import { validate_common_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; import { INVALIDATED_PARAM, validate_depends } from '../shared.js'; @@ -46,6 +52,9 @@ let errored = false; /** @type {Record} */ const scroll_positions = storage.get(SCROLL_KEY) ?? {}; +/** @type {Record>} */ +const states = storage.get(STATES_KEY) ?? {}; + /** @type {Record} */ const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; @@ -54,6 +63,20 @@ function update_scroll_positions(index) { scroll_positions[index] = scroll_state(); } +/** + * @param {number} current_history_index + */ +function clear_onward_history(current_history_index) { + // if we navigated back, then pushed a new state, we can + // release memory by pruning the scroll/snapshot lookup + let i = current_history_index + 1; + while (snapshots[i] || scroll_positions[i]) { + delete snapshots[i]; + delete scroll_positions[i]; + i += 1; + } +} + /** * @param {import('./types').SvelteKitApp} app * @param {HTMLElement} target @@ -223,6 +246,7 @@ export function create_client(app, target) { return navigate({ url, scroll: noScroll ? scroll_state() : null, + state, keepfocus: keepFocus, redirect_chain, details: { @@ -387,6 +411,7 @@ export function create_client(app, target) { route: { id: route?.id ?? null }, + state: {}, status, url: new URL(url), form: form ?? null, @@ -944,6 +969,7 @@ export function create_client(app, target) { * @param {{ * url: URL; * scroll: { x: number, y: number } | null; + * state: Record; * keepfocus: boolean; * redirect_chain: string[]; * details: { @@ -960,6 +986,7 @@ export function create_client(app, target) { async function navigate({ url, scroll, + state, keepfocus, redirect_chain, details, @@ -1066,20 +1093,17 @@ export function create_client(app, target) { history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); if (!details.replaceState) { - // if we navigated back, then pushed a new state, we can - // release memory by pruning the scroll/snapshot lookup - let i = current_history_index + 1; - while (snapshots[i] || scroll_positions[i]) { - delete snapshots[i]; - delete scroll_positions[i]; - i += 1; - } + clear_onward_history(current_history_index); } } // reset preload synchronously after the history state has been set to avoid race conditions load_cache = null; + if (navigation_result.props.page) { + navigation_result.props.page.state = state; + } + if (started) { current = navigation_result.state; @@ -1372,6 +1396,37 @@ export function create_client(app, target) { preload_code, + push_state: (state, url = current.url) => { + history.pushState( + { + [INDEX_KEY]: (current_history_index += 1) + }, + '', + new URL(url).href + ); + + page = { ...page, state }; + root.$set({ page }); + + states[current_history_index] = state; + clear_onward_history(current_history_index); + }, + + replace_state: (state, url = current.url) => { + history.replaceState( + { + [INDEX_KEY]: current_history_index + }, + '', + new URL(url).href + ); + + page = { ...page, state }; + root.$set({ page }); + + states[current_history_index] = state; + }, + apply_action: async (result) => { if (result.type === 'error') { const url = new URL(location.href); @@ -1550,6 +1605,7 @@ export function create_client(app, target) { navigate({ url, scroll: options.noscroll ? scroll_state() : null, + state: {}, keepfocus: options.keep_focus ?? false, redirect_chain: [], details: { @@ -1604,6 +1660,7 @@ export function create_client(app, target) { navigate({ url, scroll: noscroll ? scroll_state() : null, + state: {}, keepfocus: keep_focus ?? false, redirect_chain: [], details: { @@ -1619,32 +1676,47 @@ export function create_client(app, target) { addEventListener('popstate', async (event) => { if (event.state?.[INDEX_KEY]) { + const index = event.state[INDEX_KEY]; + // if a popstate-driven navigation is cancelled, we need to counteract it // with history.go, which means we end up back here, hence this check - if (event.state[INDEX_KEY] === current_history_index) return; + if (index === current_history_index) return; + + const scroll = scroll_positions[index]; + const state = states[index] ?? {}; - const scroll = scroll_positions[event.state[INDEX_KEY]]; + console.log({ index, current_history_index, state }); // if the only change is the hash, we don't need to do anything... if (current.url.href.split('#')[0] === location.href.split('#')[0]) { + // TODO I think we need to track current_history_index and (let's call it) + // current_navigation_index separately. navigating causes current_history_index + // and current_navigation_index to both increment, but clicking on a hash link + // or calling pushState/replaceState only increments current_history_index. + // we use it (instead of comparing hrefs, as above) to determine whether + // we should conduct a navigation, or simply update `$page.state`. + // ...except handle scroll scroll_positions[current_history_index] = scroll_state(); - current_history_index = event.state[INDEX_KEY]; + current_history_index = index; scrollTo(scroll.x, scroll.y); return; } - const delta = event.state[INDEX_KEY] - current_history_index; + const delta = index - current_history_index; let blocked = false; + console.log('navigating', state); + await navigate({ url: new URL(location.href), scroll, + state, keepfocus: false, redirect_chain: [], details: null, accepted: () => { - current_history_index = event.state[INDEX_KEY]; + current_history_index = index; }, blocked: () => { history.go(-delta); @@ -1657,6 +1729,8 @@ export function create_client(app, target) { if (!blocked) { restore_snapshot(current_history_index); } + } else { + console.log('popstate to a non-SvelteKit index'); } }); @@ -1762,6 +1836,7 @@ export function create_client(app, target) { }); } + result.props.page.state = {}; initialize(result); } }; diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js index cbb2353ed2be..f7321e76d506 100644 --- a/packages/kit/src/runtime/client/constants.js +++ b/packages/kit/src/runtime/client/constants.js @@ -1,6 +1,9 @@ export const SNAPSHOT_KEY = 'sveltekit:snapshot'; export const SCROLL_KEY = 'sveltekit:scroll'; -export const INDEX_KEY = 'sveltekit:index'; +export const STATES_KEY = 'sveltekit:states'; + +export const HISTORY_INDEX = 'sveltekit:history'; +export const NAVIGATION_INDEX = 'sveltekit:navigation'; export const PRELOAD_PRIORITIES = /** @type {const} */ ({ tap: 1, diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index c3e2a916e9b6..373c21f3b4a4 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -6,7 +6,9 @@ import { invalidate, invalidateAll, preloadCode, - preloadData + preloadData, + pushState, + replaceState } from '$app/navigation'; import { SvelteComponent } from 'svelte'; import { @@ -57,6 +59,8 @@ export interface Client { invalidate_all: typeof invalidateAll; preload_code: typeof preloadCode; preload_data: typeof preloadData; + push_state: typeof pushState; + replace_state: typeof replaceState; apply_action: typeof applyAction; // private API diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 7818d6ac91ed..74809a913cb1 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -202,6 +202,12 @@ export function notifiable_store(value) { store.set(new_value); } + /** @param {(value: any) => any} fn */ + function update(fn) { + ready = false; + store.update(fn); + } + /** @param {(value: any) => void} run */ function subscribe(run) { /** @type {any} */ @@ -213,7 +219,7 @@ export function notifiable_store(value) { }); } - return { notify, set, subscribe }; + return { notify, set, update, subscribe }; } export function create_updated_store() { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 33fc2f8f268a..076b88fa65bf 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -140,7 +140,8 @@ export async function render_response({ status, url: event.url, data, - form: form_value + form: form_value, + state: {} }; // use relative paths during rendering, so that the resulting HTML is as diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index 9e6b92fa27e0..bedbc78c73dc 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -263,6 +263,16 @@ declare module '$app/navigation' { * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. */ export function afterNavigate(callback: (navigation: AfterNavigate) => void): void; + + /** + * TODO + */ + export function pushState(state: Record, url: string | URL): void; + + /** + * TODO + */ + export function replaceState(state: Record, url: string | URL): void; } declare module '$app/paths' { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index c24174f45862..4a0b85fcf1c7 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -885,6 +885,10 @@ export interface Page< */ id: RouteId; }; + /** + * Arbitrary state associated with the current history entry. It can be set programmatically with `pushState` and `replaceState` from `$app/navigation`. + */ + state: Record; /** * Http status code of the current page */ From 02579237050f514497cf5f3bca3564ed4e52e511 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 10:09:10 -0400 Subject: [PATCH 02/17] undo temporary change --- packages/kit/src/runtime/client/constants.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js index f7321e76d506..9cd5636c29ef 100644 --- a/packages/kit/src/runtime/client/constants.js +++ b/packages/kit/src/runtime/client/constants.js @@ -1,9 +1,7 @@ export const SNAPSHOT_KEY = 'sveltekit:snapshot'; export const SCROLL_KEY = 'sveltekit:scroll'; export const STATES_KEY = 'sveltekit:states'; - -export const HISTORY_INDEX = 'sveltekit:history'; -export const NAVIGATION_INDEX = 'sveltekit:navigation'; +export const INDEX_KEY = 'sveltekit:index'; export const PRELOAD_PRIORITIES = /** @type {const} */ ({ tap: 1, From 0fdddb86351936d768e5fafdde5d9985fca763aa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 10:40:22 -0400 Subject: [PATCH 03/17] i think this kinda works --- packages/kit/src/runtime/client/client.js | 119 ++++++++++++------- packages/kit/src/runtime/client/constants.js | 4 +- 2 files changed, 78 insertions(+), 45 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 684c8ae80988..0bb182c3c224 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -31,7 +31,8 @@ import { stores } from './singletons.js'; import { unwrap_promises } from '../../utils/promises.js'; import * as devalue from 'devalue'; import { - INDEX_KEY, + HISTORY_INDEX, + NAVIGATION_INDEX, PRELOAD_PRIORITIES, SCROLL_KEY, STATES_KEY, @@ -43,19 +44,29 @@ import { INVALIDATED_PARAM, validate_depends } from '../shared.js'; let errored = false; +/** @typedef {{ x: number, y: number }} ScrollPosition */ + // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by // popstate it's too late to update the scroll position associated with the +// popstate it's too late to update the scroll position associated with the // state we're navigating from - -/** @typedef {{ x: number, y: number }} ScrollPosition */ -/** @type {Record} */ +/** + * history index -> { x, y } + * @type {Record} + */ const scroll_positions = storage.get(SCROLL_KEY) ?? {}; -/** @type {Record>} */ +/** + * history index -> any + * @type {Record>} + */ const states = storage.get(STATES_KEY) ?? {}; -/** @type {Record} */ +/** + * navigation index -> any + * @type {Record} + */ const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; /** @param {number} index */ @@ -65,16 +76,22 @@ function update_scroll_positions(index) { /** * @param {number} current_history_index + * @param {number} current_navigation_index */ -function clear_onward_history(current_history_index) { +function clear_onward_history(current_history_index, current_navigation_index) { // if we navigated back, then pushed a new state, we can // release memory by pruning the scroll/snapshot lookup let i = current_history_index + 1; - while (snapshots[i] || scroll_positions[i]) { - delete snapshots[i]; + while (scroll_positions[i]) { delete scroll_positions[i]; i += 1; } + + i = current_navigation_index + 1; + while (snapshots[i]) { + delete snapshots[i]; + i += 1; + } } /** @@ -138,16 +155,24 @@ export function create_client(app, target) { let root; // keeping track of the history index in order to prevent popstate navigation events if needed - let current_history_index = history.state?.[INDEX_KEY]; + /** @type {number} */ + let current_history_index = history.state?.[HISTORY_INDEX]; + + /** @type {number} */ + let current_navigation_index = history.state?.[NAVIGATION_INDEX]; if (!current_history_index) { // we use Date.now() as an offset so that cross-document navigations // within the app don't result in data loss - current_history_index = Date.now(); + current_history_index = current_navigation_index = Date.now(); // create initial history entry, so we can return here history.replaceState( - { ...history.state, [INDEX_KEY]: current_history_index }, + { + ...history.state, + [HISTORY_INDEX]: current_history_index, + [NAVIGATION_INDEX]: current_navigation_index + }, '', location.href ); @@ -217,7 +242,7 @@ export function create_client(app, target) { update_scroll_positions(current_history_index); storage.set(SCROLL_KEY, scroll_positions); - capture_snapshot(current_history_index); + capture_snapshot(current_navigation_index); storage.set(SNAPSHOT_KEY, snapshots); } @@ -308,7 +333,7 @@ export function create_client(app, target) { hydrate: true }); - restore_snapshot(current_history_index); + restore_snapshot(current_navigation_index); /** @type {import('types').AfterNavigate} */ const navigation = { @@ -1006,6 +1031,7 @@ export function create_client(app, target) { // store this before calling `accepted()`, which may change the index const previous_history_index = current_history_index; + const previous_navigation_index = current_navigation_index; accepted(); @@ -1077,7 +1103,7 @@ export function create_client(app, target) { updating = true; update_scroll_positions(previous_history_index); - capture_snapshot(previous_history_index); + capture_snapshot(previous_navigation_index); // ensure the url pathname matches the page's trailing slash option if ( @@ -1089,11 +1115,12 @@ export function create_client(app, target) { if (details) { const change = details.replaceState ? 0 : 1; - details.state[INDEX_KEY] = current_history_index += change; + details.state[HISTORY_INDEX] = current_history_index += change; + details.state[NAVIGATION_INDEX] = current_navigation_index += change; history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); if (!details.replaceState) { - clear_onward_history(current_history_index); + clear_onward_history(current_history_index, current_navigation_index); } } @@ -1399,7 +1426,8 @@ export function create_client(app, target) { push_state: (state, url = current.url) => { history.pushState( { - [INDEX_KEY]: (current_history_index += 1) + [HISTORY_INDEX]: (current_history_index += 1), + [NAVIGATION_INDEX]: current_navigation_index }, '', new URL(url).href @@ -1409,13 +1437,14 @@ export function create_client(app, target) { root.$set({ page }); states[current_history_index] = state; - clear_onward_history(current_history_index); + clear_onward_history(current_history_index, current_navigation_index); }, replace_state: (state, url = current.url) => { history.replaceState( { - [INDEX_KEY]: current_history_index + [HISTORY_INDEX]: current_history_index, + [NAVIGATION_INDEX]: current_navigation_index }, '', new URL(url).href @@ -1675,39 +1704,36 @@ export function create_client(app, target) { }); addEventListener('popstate', async (event) => { - if (event.state?.[INDEX_KEY]) { - const index = event.state[INDEX_KEY]; + if (event.state?.[HISTORY_INDEX]) { + const history_index = event.state[HISTORY_INDEX]; // if a popstate-driven navigation is cancelled, we need to counteract it // with history.go, which means we end up back here, hence this check - if (index === current_history_index) return; + if (history_index === current_history_index) return; - const scroll = scroll_positions[index]; - const state = states[index] ?? {}; + const scroll = scroll_positions[history_index]; + const state = states[history_index] ?? {}; - console.log({ index, current_history_index, state }); + const navigation_index = event.state[NAVIGATION_INDEX]; - // if the only change is the hash, we don't need to do anything... - if (current.url.href.split('#')[0] === location.href.split('#')[0]) { - // TODO I think we need to track current_history_index and (let's call it) - // current_navigation_index separately. navigating causes current_history_index - // and current_navigation_index to both increment, but clicking on a hash link - // or calling pushState/replaceState only increments current_history_index. - // we use it (instead of comparing hrefs, as above) to determine whether - // we should conduct a navigation, or simply update `$page.state`. - - // ...except handle scroll + if (navigation_index === current_navigation_index) { + // We don't need to navigate, we just need to update scroll and/or state. + // This happens with hash links and `pushState`/`replaceState` scroll_positions[current_history_index] = scroll_state(); - current_history_index = index; - scrollTo(scroll.x, scroll.y); + if (scroll) scrollTo(scroll.x, scroll.y); + + if (state !== page.state) { + page = { ...page, state }; + root.$set({ page }); + } + + current_history_index = history_index; return; } - const delta = index - current_history_index; + const delta = history_index - current_history_index; let blocked = false; - console.log('navigating', state); - await navigate({ url: new URL(location.href), scroll, @@ -1716,7 +1742,8 @@ export function create_client(app, target) { redirect_chain: [], details: null, accepted: () => { - current_history_index = index; + current_history_index = history_index; + current_navigation_index = navigation_index; }, blocked: () => { history.go(-delta); @@ -1727,7 +1754,7 @@ export function create_client(app, target) { }); if (!blocked) { - restore_snapshot(current_history_index); + restore_snapshot(current_navigation_index); } } else { console.log('popstate to a non-SvelteKit index'); @@ -1740,7 +1767,11 @@ export function create_client(app, target) { if (hash_navigating) { hash_navigating = false; history.replaceState( - { ...history.state, [INDEX_KEY]: ++current_history_index }, + { + ...history.state, + [HISTORY_INDEX]: ++current_history_index, + [NAVIGATION_INDEX]: current_navigation_index + }, '', location.href ); diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js index 9cd5636c29ef..f7321e76d506 100644 --- a/packages/kit/src/runtime/client/constants.js +++ b/packages/kit/src/runtime/client/constants.js @@ -1,7 +1,9 @@ export const SNAPSHOT_KEY = 'sveltekit:snapshot'; export const SCROLL_KEY = 'sveltekit:scroll'; export const STATES_KEY = 'sveltekit:states'; -export const INDEX_KEY = 'sveltekit:index'; + +export const HISTORY_INDEX = 'sveltekit:history'; +export const NAVIGATION_INDEX = 'sveltekit:navigation'; export const PRELOAD_PRIORITIES = /** @type {const} */ ({ tap: 1, From 38014cbee103964c23cdece5487fc8c3388cc6e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 10:48:00 -0400 Subject: [PATCH 04/17] fix --- packages/kit/src/runtime/client/client.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 0bb182c3c224..c2a43c0f7a65 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1124,6 +1124,8 @@ export function create_client(app, target) { } } + states[current_history_index] = state; + // reset preload synchronously after the history state has been set to avoid race conditions load_cache = null; From 866101d9525b988b4389f5ad6b5fb73949c03692 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 11:35:04 -0400 Subject: [PATCH 05/17] return stuff from preloadData --- packages/kit/src/runtime/client/client.js | 11 ++++++++++- packages/kit/types/ambient.d.ts | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c2a43c0f7a65..fc5ded2d5c54 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1420,7 +1420,16 @@ export function create_client(app, target) { throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); } - await preload_data(intent); + const result = await preload_data(intent); + if (result.type === 'redirect') { + return { + type: result.type, + location: result.location + }; + } + + const { status, data } = result.props.page ?? page; + return { type: result.type, status, data }; }, preload_code, diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index bedbc78c73dc..62af5bcf832b 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -234,7 +234,11 @@ declare module '$app/navigation' { * * @param href Page to preload */ - export function preloadData(href: string): Promise; + export function preloadData( + href: string + ): Promise< + { type: 'redirect'; location: string } | { type: 'loaded'; status: number; data: App.PageData } + >; /** * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. From e4df971b2af4037bbf55d9f6c9dd5c0c42099680 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 11:41:49 -0400 Subject: [PATCH 06/17] fix reloads following modal navigation --- packages/kit/src/runtime/client/client.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index fc5ded2d5c54..842606c5af8b 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1526,6 +1526,11 @@ export function create_client(app, target) { addEventListener('beforeunload', (e) => { let should_block = false; + // if the user reloads the page after a modal navigation, we need to + // sever the link between the current state and the previous one, + // otherwise hitting back/forward will have no effect + history.replaceState({ ...history.state, [NAVIGATION_INDEX]: -1 }, ''); + persist_state(); if (!navigating) { From 18105d1252befbe0d87b2c580e05b9911707d5a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 16:39:08 -0400 Subject: [PATCH 07/17] found the source of the failing tests i think --- packages/kit/src/runtime/client/client.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 9e7e7ba526ea..380dceeb0e3a 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1526,11 +1526,6 @@ export function create_client(app, target) { addEventListener('beforeunload', (e) => { let should_block = false; - // if the user reloads the page after a modal navigation, we need to - // sever the link between the current state and the previous one, - // otherwise hitting back/forward will have no effect - history.replaceState({ ...history.state, [NAVIGATION_INDEX]: -1 }, ''); - persist_state(); if (!navigating) { From 44f2f97359a029a42cdcc32bb96180952abc99b2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 16:42:19 -0400 Subject: [PATCH 08/17] Update packages/kit/src/runtime/client/client.js --- packages/kit/src/runtime/client/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 380dceeb0e3a..bb5eca0b81a8 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -49,7 +49,6 @@ let errored = false; // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by // popstate it's too late to update the scroll position associated with the -// popstate it's too late to update the scroll position associated with the // state we're navigating from /** * history index -> { x, y } From 2de7afc3eaf782f2cd0c1187c5120e1dd781a700 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 17:07:15 -0400 Subject: [PATCH 09/17] store underlying page URL --- packages/kit/src/runtime/client/client.js | 11 +++++++---- packages/kit/src/runtime/client/constants.js | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index bb5eca0b81a8..635d2fea6558 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -36,7 +36,8 @@ import { PRELOAD_PRIORITIES, SCROLL_KEY, STATES_KEY, - SNAPSHOT_KEY + SNAPSHOT_KEY, + PAGE_URL_KEY } from './constants.js'; import { validate_page_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; @@ -1437,7 +1438,8 @@ export function create_client(app, target) { history.pushState( { [HISTORY_INDEX]: (current_history_index += 1), - [NAVIGATION_INDEX]: current_navigation_index + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href }, '', new URL(url).href @@ -1454,7 +1456,8 @@ export function create_client(app, target) { history.replaceState( { [HISTORY_INDEX]: current_history_index, - [NAVIGATION_INDEX]: current_navigation_index + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href }, '', new URL(url).href @@ -1745,7 +1748,7 @@ export function create_client(app, target) { let blocked = false; await navigate({ - url: new URL(location.href), + url: new URL(event.state[PAGE_URL_KEY] ?? location.href), scroll, state, keepfocus: false, diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js index f7321e76d506..3e2de41a585b 100644 --- a/packages/kit/src/runtime/client/constants.js +++ b/packages/kit/src/runtime/client/constants.js @@ -1,6 +1,7 @@ export const SNAPSHOT_KEY = 'sveltekit:snapshot'; export const SCROLL_KEY = 'sveltekit:scroll'; export const STATES_KEY = 'sveltekit:states'; +export const PAGE_URL_KEY = 'sveltekit:pageurl'; export const HISTORY_INDEX = 'sveltekit:history'; export const NAVIGATION_INDEX = 'sveltekit:navigation'; From 147bd15201a0bed18e2e32a18a505130e7662aa4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 17:52:13 -0400 Subject: [PATCH 10/17] use devalue to serialize states --- packages/kit/src/runtime/client/client.js | 22 ++++++++++++++++++- .../kit/src/runtime/client/session-storage.js | 12 +++++----- packages/kit/src/runtime/client/utils.js | 1 + 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 635d2fea6558..9b43c0f8e1d7 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -61,7 +61,7 @@ const scroll_positions = storage.get(SCROLL_KEY) ?? {}; * history index -> any * @type {Record>} */ -const states = storage.get(STATES_KEY) ?? {}; +const states = storage.get(STATES_KEY, devalue.parse) ?? {}; /** * navigation index -> any @@ -244,6 +244,8 @@ export function create_client(app, target) { capture_snapshot(current_navigation_index); storage.set(SNAPSHOT_KEY, snapshots); + + storage.set(STATES_KEY, states, devalue.stringify); } /** @@ -1435,6 +1437,15 @@ export function create_client(app, target) { preload_code, push_state: (state, url = current.url) => { + if (DEV) { + try { + devalue.stringify(state); + } catch (error) { + // @ts-expect-error + throw new Error(`Could not serialize state${error.path}`); + } + } + history.pushState( { [HISTORY_INDEX]: (current_history_index += 1), @@ -1453,6 +1464,15 @@ export function create_client(app, target) { }, replace_state: (state, url = current.url) => { + if (DEV) { + try { + devalue.stringify(state); + } catch (error) { + // @ts-expect-error + throw new Error(`Could not serialize state${error.path}`); + } + } + history.replaceState( { [HISTORY_INDEX]: current_history_index, diff --git a/packages/kit/src/runtime/client/session-storage.js b/packages/kit/src/runtime/client/session-storage.js index dc2639ef1b5e..e49543bc8b82 100644 --- a/packages/kit/src/runtime/client/session-storage.js +++ b/packages/kit/src/runtime/client/session-storage.js @@ -1,10 +1,11 @@ /** * Read a value from `sessionStorage` * @param {string} key + * @param {(value: string) => any} parse */ -export function get(key) { +export function get(key, parse = JSON.parse) { try { - return JSON.parse(sessionStorage[key]); + return parse(sessionStorage[key]); } catch { // do nothing } @@ -14,11 +15,12 @@ export function get(key) { * Write a value to `sessionStorage` * @param {string} key * @param {any} value + * @param {(value: any) => string} stringify */ -export function set(key, value) { - const json = JSON.stringify(value); +export function set(key, value, stringify = JSON.stringify) { + const data = stringify(value); try { - sessionStorage[key] = json; + sessionStorage[key] = data; } catch { // do nothing } diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 74809a913cb1..cba176832e24 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -1,5 +1,6 @@ import { BROWSER, DEV } from 'esm-env'; import { writable } from 'svelte/store'; +import { stringify } from 'devalue'; import { assets } from '__sveltekit/paths'; import { version } from '__sveltekit/environment'; import { PRELOAD_PRIORITIES } from './constants.js'; From 62e768640746abe496bba0b98f08335fcb789b8e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 18:34:37 -0400 Subject: [PATCH 11/17] bugfix --- packages/kit/src/runtime/client/client.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 9b43c0f8e1d7..032bf7fcc0b5 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -161,6 +161,8 @@ export function create_client(app, target) { /** @type {number} */ let current_navigation_index = history.state?.[NAVIGATION_INDEX]; + let has_navigated = false; + if (!current_history_index) { // we use Date.now() as an offset so that cross-document navigations // within the app don't result in data loss @@ -1193,6 +1195,7 @@ export function create_client(app, target) { stores.navigating.set(null); updating = false; + has_navigated = true; } /** @@ -1749,9 +1752,13 @@ export function create_client(app, target) { const navigation_index = event.state[NAVIGATION_INDEX]; - if (navigation_index === current_navigation_index) { + const shallow = navigation_index === current_navigation_index && has_navigated; + + if (shallow) { // We don't need to navigate, we just need to update scroll and/or state. - // This happens with hash links and `pushState`/`replaceState` + // This happens with hash links and `pushState`/`replaceState`. The + // exception is if we haven't navigated yet, since we could have + // got here after a modal navigation then a reload scroll_positions[current_history_index] = scroll_state(); if (scroll) scrollTo(scroll.x, scroll.y); From e1ed3cd6b07e668b27516041440f6ed822350ab4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 18:37:48 -0400 Subject: [PATCH 12/17] lint --- packages/kit/src/runtime/client/client.js | 5 ++++- packages/kit/src/runtime/client/utils.js | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 032bf7fcc0b5..a74e9da30b47 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1907,7 +1907,10 @@ export function create_client(app, target) { }); } - result.props.page.state = {}; + if (result.props.page) { + result.props.page.state = {}; + } + initialize(result); } }; diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index cba176832e24..74809a913cb1 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -1,6 +1,5 @@ import { BROWSER, DEV } from 'esm-env'; import { writable } from 'svelte/store'; -import { stringify } from 'devalue'; import { assets } from '__sveltekit/paths'; import { version } from '__sveltekit/environment'; import { PRELOAD_PRIORITIES } from './constants.js'; From 6d96432a9c6994f26102d87ac38dff4ed7ecc65f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 4 May 2023 19:01:54 -0400 Subject: [PATCH 13/17] always set page, so that state is nuked if you click a self-link --- packages/kit/src/runtime/client/client.js | 18 ++++++++---------- packages/kit/src/runtime/client/types.d.ts | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a74e9da30b47..15476db921fa 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -396,7 +396,8 @@ export function create_client(app, target) { }, props: { // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up - constructors: compact(branch).map((branch_node) => branch_node.node.component) + constructors: compact(branch).map((branch_node) => branch_node.node.component), + page } }; @@ -1092,7 +1093,7 @@ export function create_client(app, target) { ); return false; } - } else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) { + } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) { const updated = await stores.updated.check(); if (updated) { await native_navigation(url); @@ -1110,10 +1111,7 @@ export function create_client(app, target) { capture_snapshot(previous_navigation_index); // ensure the url pathname matches the page's trailing slash option - if ( - navigation_result.props.page?.url && - navigation_result.props.page.url.pathname !== url.pathname - ) { + if (navigation_result.props.page.url.pathname !== url.pathname) { url.pathname = navigation_result.props.page?.url.pathname; } @@ -1133,9 +1131,7 @@ export function create_client(app, target) { // reset preload synchronously after the history state has been set to avoid race conditions load_cache = null; - if (navigation_result.props.page) { - navigation_result.props.page.state = state; - } + navigation_result.props.page.state = state; if (started) { current = navigation_result.state; @@ -1752,7 +1748,9 @@ export function create_client(app, target) { const navigation_index = event.state[NAVIGATION_INDEX]; - const shallow = navigation_index === current_navigation_index && has_navigated; + const is_hash_change = location.href.split('#')[0] === current.url.href.split('#')[0]; + const shallow = + navigation_index === current_navigation_index && (has_navigated || is_hash_change); if (shallow) { // We don't need to navigate, we just need to update scroll and/or state. diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 373c21f3b4a4..69dbdbeedaf8 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -101,7 +101,7 @@ export type NavigationFinished = { state: NavigationState; props: { components: Array; - page?: Page; + page: Page; form?: Record | null; [key: `data_${number}`]: Record; }; From 55d7251d32ade3e5a94088904ee0e8e343a5bc3c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 5 May 2023 17:57:41 -0400 Subject: [PATCH 14/17] fix --- packages/kit/src/runtime/client/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 98b2c3b23f2e..85cb85ace985 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1190,7 +1190,7 @@ export function create_client(app, target) { navigating = false; if (type === 'popstate') { - restore_snapshot(current_history_index); + restore_snapshot(current_navigation_index); } callbacks.after_navigate.forEach((fn) => From e8332ca607cec79da1411345d4912af1d7d85bf5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 5 May 2023 19:31:42 -0400 Subject: [PATCH 15/17] URLs are optional --- packages/kit/types/ambient.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index 62af5bcf832b..6eebc00d7ce6 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -271,12 +271,12 @@ declare module '$app/navigation' { /** * TODO */ - export function pushState(state: Record, url: string | URL): void; + export function pushState(state: Record, url?: string | URL): void; /** * TODO */ - export function replaceState(state: Record, url: string | URL): void; + export function replaceState(state: Record, url?: string | URL): void; } declare module '$app/paths' { From b42f774b2c90cf758a829c4cc2a6850bdfc063ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 14:48:32 -0500 Subject: [PATCH 16/17] oops --- packages/kit/src/runtime/client/types.d.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 8cc1bb9e99d4..2d6937e79eba 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -7,15 +7,10 @@ import { invalidate, invalidateAll, preloadCode, -<<<<<<< HEAD preloadData, pushState, replaceState -} from '$app/navigation'; -======= - preloadData } from '../app/navigation.js'; ->>>>>>> master import { SvelteComponent } from 'svelte'; import { ClientHooks, CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types'; import { Page, ParamMatcher } from '@sveltejs/kit'; @@ -99,14 +94,9 @@ export type NavigationFinished = { type: 'loaded'; state: NavigationState; props: { -<<<<<<< HEAD + constructors: Array; components: Array; page: Page; -======= - constructors: Array; - components?: Array; - page?: Page; ->>>>>>> master form?: Record | null; [key: `data_${number}`]: Record; }; From b6d9738d7ff5a21ad9488fbcb46ed2a4e1a9f292 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 14:54:16 -0500 Subject: [PATCH 17/17] fix some stuff --- packages/kit/src/runtime/client/client.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index bc2c7af6485f..04bd1a264590 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -39,11 +39,9 @@ import { } from './constants.js'; import { validate_page_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; -import { validate_page_exports } from '../../utils/exports.js'; import { unwrap_promises } from '../../utils/promises.js'; import { HttpError, Redirect, NotFound } from '../control.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js'; -import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js'; import { stores } from './singletons.js'; let errored = false; @@ -98,7 +96,7 @@ function clear_onward_history(current_history_index, current_navigation_index) { } /** - * @param {import('./types').SvelteKitApp} app + * @param {import('./types.js').SvelteKitApp} app * Loads `href` the old-fashioned way, with a full page reload. * Returns a `Promise` that never resolves (to prevent any * subsequent work, e.g. history manipulation, from happening)