diff --git a/.changeset/angry-ravens-eat.md b/.changeset/angry-ravens-eat.md new file mode 100644 index 000000000000..539c2cc946d7 --- /dev/null +++ b/.changeset/angry-ravens-eat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: only call `afterNavigate` once on app start when SSR is disabled diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 5d41031b1613..732df205d0ae 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1025,7 +1025,7 @@ export interface NavigationTarget { } /** - * - `enter`: The app has hydrated + * - `enter`: The app has hydrated/started * - `form`: The user submitted a `
` with a GET method * - `leave`: The user is leaving the app by closing the tab or using the back/forward buttons to go to a different document * - `link`: Navigation was triggered by a link click @@ -1101,7 +1101,7 @@ export interface OnNavigate extends Navigation { export interface AfterNavigate extends Omit { /** * The type of navigation: - * - `enter`: The app has hydrated + * - `enter`: The app has hydrated/started * - `form`: The user submitted a `` * - `link`: Navigation was triggered by a link click * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index ecfb49b74da8..f831afa5db34 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -320,8 +320,10 @@ export async function start(_app, _target, hydrate) { if (hydrate) { await _hydrate(target, hydrate); } else { - await goto(app.hash ? decode_hash(new URL(location.href)) : location.href, { - replaceState: true + await navigate({ + type: 'enter', + url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href), + replace_state: true }); } @@ -479,20 +481,22 @@ function initialize(result, target, hydrate) { restore_snapshot(current_navigation_index); - /** @type {import('@sveltejs/kit').AfterNavigate} */ - const navigation = { - from: null, - to: { - params: current.params, - route: { id: current.route?.id ?? null }, - url: new URL(location.href) - }, - willUnload: false, - type: 'enter', - complete: Promise.resolve() - }; + if (hydrate) { + /** @type {import('@sveltejs/kit').AfterNavigate} */ + const navigation = { + from: null, + to: { + params: current.params, + route: { id: current.route?.id ?? null }, + url: new URL(location.href) + }, + willUnload: false, + type: 'enter', + complete: Promise.resolve() + }; - after_navigate_callbacks.forEach((fn) => fn(navigation)); + after_navigate_callbacks.forEach((fn) => fn(navigation)); + } started = true; } @@ -1373,7 +1377,7 @@ function _before_navigate({ url, type, intent, delta }) { /** * @param {{ - * type: import('@sveltejs/kit').Navigation["type"]; + * type: import('@sveltejs/kit').NavigationType; * url: URL; * popped?: { * state: Record; @@ -1407,7 +1411,10 @@ async function navigate({ token = nav_token; const intent = await get_navigation_intent(url, false); - const nav = _before_navigate({ url, type, delta: popped?.delta, intent }); + const nav = + type === 'enter' + ? create_navigation(current, intent, url, type) + : _before_navigate({ url, type, delta: popped?.delta, intent }); if (!nav) { block(); @@ -1423,7 +1430,7 @@ async function navigate({ is_navigating = true; - if (started) { + if (started && nav.navigation.type !== 'enter') { stores.navigating.set((navigating.current = nav.navigation)); } @@ -2847,10 +2854,11 @@ function reset_focus() { } /** + * @template {import('@sveltejs/kit').NavigationType} T * @param {import('./types.js').NavigationState} current * @param {import('./types.js').NavigationIntent | undefined} intent * @param {URL | null} url - * @param {Exclude} type + * @param {T} type */ function create_navigation(current, intent, url, type) { /** @type {(value: any) => void} */ @@ -2867,7 +2875,7 @@ function create_navigation(current, intent, url, type) { // Handle any errors off-chain so that it doesn't show up as an unhandled rejection complete.catch(() => {}); - /** @type {import('@sveltejs/kit').Navigation} */ + /** @type {Omit & { type: T }} */ const navigation = { from: { params: current.params, diff --git a/packages/kit/test/apps/basics/src/routes/no-ssr/after-navigate/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-ssr/after-navigate/+page.svelte new file mode 100644 index 000000000000..0e99d8405c54 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/no-ssr/after-navigate/+page.svelte @@ -0,0 +1,13 @@ + + +

{type.toString()} {count}

diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 84683d43ebcd..28173e7c1d51 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -335,8 +335,8 @@ test.describe('Load', () => { } }); -test.describe('Page options', () => { - test('applies generated component styles with ssr=false (hides announcer)', async ({ +test.describe('SPA mode / no SSR', () => { + test('applies generated component styles (hides announcer)', async ({ page, clicknav, get_computed_style @@ -346,9 +346,7 @@ test.describe('Page options', () => { expect(await get_computed_style('#svelte-announcer', 'position')).toBe('absolute'); }); -}); -test.describe('SPA mode / no SSR', () => { test('Can use browser-only global on client-only page through ssr config in handle', async ({ page, read_errors @@ -358,7 +356,7 @@ test.describe('SPA mode / no SSR', () => { expect(read_errors('/no-ssr/browser-only-global')).toBe(undefined); }); - test('Can use browser-only global on client-only page through ssr config in +layout.js', async ({ + test('can use browser-only global on client-only page through ssr config in +layout.js', async ({ page, read_errors }) => { @@ -367,7 +365,7 @@ test.describe('SPA mode / no SSR', () => { expect(read_errors('/no-ssr/ssr-page-config')).toBe(undefined); }); - test('Can use browser-only global on client-only page through ssr config in +page.js', async ({ + test('can use browser-only global on client-only page through ssr config in +page.js', async ({ page, read_errors }) => { @@ -376,7 +374,7 @@ test.describe('SPA mode / no SSR', () => { expect(read_errors('/no-ssr/ssr-page-config/layout/inherit')).toBe(undefined); }); - test('Cannot use browser-only global on page because of ssr config in +page.js', async ({ + test('cannot use browser-only global on page because of ssr config in +page.js', async ({ page }) => { await page.goto('/no-ssr/ssr-page-config/layout/overwrite'); @@ -384,6 +382,11 @@ test.describe('SPA mode / no SSR', () => { 'This is your custom error page saying: "document is not defined (500 Internal Error)"' ); }); + + test('afterNavigate is only called once during start', async ({ page }) => { + await page.goto('/no-ssr/after-navigate'); + await expect(page.locator('p')).toHaveText('enter 1'); + }); }); // TODO SvelteKit 3: remove these tests diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 87c010a07f96..50cc68bc8659 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -257,7 +257,7 @@ test.describe('Navigation lifecycle functions', () => { ); }); - test('afterNavigate properly removed', async ({ page, clicknav }) => { + test('onNavigate returned function is only called once', async ({ page, clicknav }) => { await page.goto('/navigation-lifecycle/after-navigate-properly-removed/b'); await clicknav('[href="/navigation-lifecycle/after-navigate-properly-removed/a"]'); await clicknav('[href="/navigation-lifecycle/after-navigate-properly-removed/b"]'); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index deed8779b9a9..38653d067a51 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1007,7 +1007,7 @@ declare module '@sveltejs/kit' { } /** - * - `enter`: The app has hydrated + * - `enter`: The app has hydrated/started * - `form`: The user submitted a `` with a GET method * - `leave`: The user is leaving the app by closing the tab or using the back/forward buttons to go to a different document * - `link`: Navigation was triggered by a link click @@ -1083,7 +1083,7 @@ declare module '@sveltejs/kit' { export interface AfterNavigate extends Omit { /** * The type of navigation: - * - `enter`: The app has hydrated + * - `enter`: The app has hydrated/started * - `form`: The user submitted a `` * - `link`: Navigation was triggered by a link click * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect