From bf0afecd485fd49dc17507ab41d192f27967d640 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 14:40:29 +0100 Subject: [PATCH 1/3] feat: handleLoad hooks Adds handleLoad and handleServerLoad hooks that run before any invocation of a corresponding load function. closes #9542 --- documentation/docs/30-advanced/20-hooks.md | 62 ++++++++++++++++ .../src/core/sync/write_client_manifest.js | 3 + packages/kit/src/exports/public.d.ts | 16 +++++ packages/kit/src/runtime/client/client.js | 7 +- packages/kit/src/runtime/server/ambient.d.ts | 2 + packages/kit/src/runtime/server/data/index.js | 3 +- packages/kit/src/runtime/server/index.js | 8 ++- packages/kit/src/runtime/server/page/index.js | 4 +- .../kit/src/runtime/server/page/load_data.js | 35 +++++---- .../runtime/server/page/respond_with_error.js | 4 +- packages/kit/src/types/internal.d.ts | 7 +- .../kit/test/apps/basics/src/hooks.client.js | 18 +++++ .../kit/test/apps/basics/src/hooks.server.js | 36 ++++++++++ .../src/routes/handle-load/bypass/+page.js | 5 ++ .../routes/handle-load/bypass/+page.svelte | 5 ++ .../src/routes/handle-load/enrich/+page.js | 5 ++ .../routes/handle-load/enrich/+page.svelte | 5 ++ .../handle-server-load/bypass/+page.server.js | 5 ++ .../handle-server-load/bypass/+page.svelte | 5 ++ .../handle-server-load/enrich/+page.server.js | 5 ++ .../handle-server-load/enrich/+page.svelte | 5 ++ packages/kit/test/apps/basics/test/test.js | 72 +++++++++++++++++++ packages/kit/types/index.d.ts | 16 +++++ .../src/routes/bar/[bar]/+page.js | 14 ++++ .../src/routes/bar/[bar]/+page.svelte | 6 ++ sites/kit.svelte.dev/src/routes/foo/+page.js | 5 ++ .../src/routes/foo/+page.svelte | 17 +++++ 27 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.js create mode 100644 packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.js create mode 100644 packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.svelte create mode 100644 sites/kit.svelte.dev/src/routes/bar/[bar]/+page.js create mode 100644 sites/kit.svelte.dev/src/routes/bar/[bar]/+page.svelte create mode 100644 sites/kit.svelte.dev/src/routes/foo/+page.js create mode 100644 sites/kit.svelte.dev/src/routes/foo/+page.svelte diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index d979ec88b1b1..ba4752e74596 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -133,10 +133,72 @@ export async function handleFetch({ event, request, fetch }) { } ``` +### handleServerLoad + +This function allows you to wrap server `load` functions with custom behavior. It is called whenever a server `load` function would be called. It receives the same `event` object the `load` function would get and a `resolve` method through which you can call the original load function. + +```js +/** @type {import("@sveltejs/kit").HandleServerLoad} */ +export async function handleServerLoad({ event, resolve }) { + if (event.url.pathname.endsWith('/bypass')) { + // Do not call load function at all + return { + from: 'handleServerLoad' + }; + } else if (event.url.pathname.endsWith('/enrich')) { + // Call load function with modified inputs and adjust result + const result = await resolve({ + ...event, + parent: () => { + console.log('called parent'); + return event.parent(); + } + }); + return { + from: 'handleServerLoad and ' + /** @type {any} */ (result).from + }; + } else { + // Call load function directly + return resolve(event); + } +} +``` + ## Shared hooks The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`: +### handleLoad + +This function allows you to wrap universal `load` functions with custom behavior. It is called whenever a universal `load` function would be called. It receives the same `event` object the `load` function would get and a `resolve` method through which you can call the original load function. + +```js +/** @type {import("@sveltejs/kit").HandleLoad} */ +export async function handleLoad({ event, resolve }) { + if (event.url.pathname.endsWith('/bypass')) { + // Do not call load function at all + return { + from: 'handleLoad' + }; + } else if (event.url.pathname.endsWith('/enrich')) { + // Call load function with modified inputs and adjust result + const result = await resolve({ + ...event, + parent: () => { + console.log('called parent'); + return event.parent(); + } + }); + return { + from: 'handleLoad and ' + /** @type {any} */ (result).from + }; + } else { + // Call load function directly + return resolve(event); + } +} +``` + ### handleError If an [unexpected error](/docs/errors#unexpected-errors) is thrown during loading or rendering, this function will be called with the `error`, `event`, `status` code and `message`. This allows for two things: diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index b8b920ffb5f1..9d409c7afe69 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -141,6 +141,9 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { handleError: ${ hooks_file ? 'client_hooks.handleError || ' : '' }(({ error }) => { console.error(error) }), + handleLoad: ${ + hooks_file ? 'client_hooks.handleLoad || ' : '' + }(({ event, resolve }) => resolve(event)), }; export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index f9de82b1c393..a22777b6723e 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -683,6 +683,22 @@ export type HandleFetch = (input: { fetch: typeof fetch; }) => MaybePromise; +/** + * The `handleLoad` hook runs every time a universal `load` function (for example from +page.js) is called. + * This hook can be registered on the client as well as on the server side. + * This hook provides the load `event` and a `resolve` function to call the actual hook with the event. + */ +export type HandleLoad = (input: { event: LoadEvent; resolve: Load }) => ReturnType; + +/** + * The `handleServerLoad` hook runs every time a server-only `load` function (for example from +page.server.js) is called on the server. + * This hook provides the server load `event` and a `resolve` function to call the actual hook with the event. + */ +export type HandleServerLoad = (input: { + event: ServerLoadEvent; + resolve: ServerLoad; +}) => ReturnType; + /** * The generic form of `PageLoad` and `LayoutLoad`. You should import those from `./$types` (see [generated types](https://kit.svelte.dev/docs/types#generated-types)) * rather than using `Load` directly. diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 39b326dbc17d..968fd0b9f818 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -539,7 +539,9 @@ export function create_client(app, target) { if (DEV) { try { lock_fetch(); - data = (await node.universal.load.call(null, load_input)) ?? null; + data = + (await app.hooks.handleLoad({ event: load_input, resolve: node.universal.load })) ?? + null; if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { throw new Error( `a load function related to route '${route.id}' returned ${ @@ -557,7 +559,8 @@ export function create_client(app, target) { unlock_fetch(); } } else { - data = (await node.universal.load.call(null, load_input)) ?? null; + data = + (await app.hooks.handleLoad({ event: load_input, resolve: node.universal.load })) ?? null; } } diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index c893c94ff32b..ca17e1e05545 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -4,5 +4,7 @@ declare module '__SERVER__/internal.js' { handle?: import('@sveltejs/kit').Handle; handleError?: import('@sveltejs/kit').HandleServerError; handleFetch?: import('@sveltejs/kit').HandleFetch; + handleLoad?: import('@sveltejs/kit').HandleLoad; + handleServerLoad?: import('@sveltejs/kit').HandleServerLoad; }>; } diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index c0316d278e8d..ab0c75b94e70 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -76,7 +76,8 @@ export async function render_data( } } return data; - } + }, + handle_load: options.hooks.handleServerLoad }); } catch (e) { aborted = true; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index fde088f714b6..f537e1564ce6 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -58,7 +58,9 @@ export class Server { this.#options.hooks = { handle: module.handle || (({ event, resolve }) => resolve(event)), handleError: module.handleError || (({ error }) => console.error(error)), - handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)) + handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)), + handleLoad: module.handleLoad || (({ event, resolve }) => resolve(event)), + handleServerLoad: module.handleServerLoad || (({ event, resolve }) => resolve(event)) }; } catch (error) { if (DEV) { @@ -67,7 +69,9 @@ export class Server { throw error; }, handleError: ({ error }) => console.error(error), - handleFetch: ({ request, fetch }) => fetch(request) + handleFetch: ({ request, fetch }) => fetch(request), + handleLoad: ({ event, resolve }) => resolve(event), + handleServerLoad: ({ event, resolve }) => resolve(event) }; } else { throw error; diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index 067d60585cd9..34817b4941ed 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -149,7 +149,8 @@ export async function render_page(event, page, options, manifest, state, resolve if (parent) Object.assign(data, await parent.data); } return data; - } + }, + handle_load: options.hooks.handleServerLoad }); } catch (e) { load_error = /** @type {Error} */ (e); @@ -179,6 +180,7 @@ export async function render_page(event, page, options, manifest, state, resolve resolve_opts, server_data_promise: server_promises[i], state, + handle_load: options.hooks.handleLoad, csr }); } catch (e) { diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 1b02c9fc51e5..4b02f101f545 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -9,10 +9,11 @@ import { validate_depends } from '../../shared.js'; * state: import('types').SSRState; * node: import('types').SSRNode | undefined; * parent: () => Promise>; + * handle_load: import('@sveltejs/kit').HandleServerLoad * }} opts * @returns {Promise} */ -export async function load_server_data({ event, state, node, parent }) { +export async function load_server_data({ event, state, node, parent, handle_load }) { if (!node?.server) return null; let done = false; @@ -52,7 +53,8 @@ export async function load_server_data({ event, state, node, parent }) { disable_search(url); } - const result = await node.server.load?.call(null, { + /** @type {import('@sveltejs/kit').ServerLoadEvent} */ + const load_input = { ...event, fetch: (info, init) => { const url = new URL(info instanceof Request ? info.url : info, event.url); @@ -122,7 +124,11 @@ export async function load_server_data({ event, state, node, parent }) { } }), url - }); + }; + + const result = node.server.load + ? await handle_load({ event: load_input, resolve: node.server.load }) + : null; if (__SVELTEKIT_DEV__) { validate_load_response(result, node.server_id); @@ -148,6 +154,7 @@ export async function load_server_data({ event, state, node, parent }) { * resolve_opts: import('types').RequiredResolveOptions; * server_data_promise: Promise; * state: import('types').SSRState; + * handle_load: import('@sveltejs/kit').HandleLoad * csr: boolean; * }} opts * @returns {Promise> | null>} @@ -160,6 +167,7 @@ export async function load_data({ server_data_promise, state, resolve_opts, + handle_load, csr }) { const server_data_node = await server_data_promise; @@ -168,15 +176,18 @@ export async function load_data({ return server_data_node?.data ?? null; } - const result = await node.universal.load.call(null, { - url: event.url, - params: event.params, - data: server_data_node?.data ?? null, - route: event.route, - fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), - setHeaders: event.setHeaders, - depends: () => {}, - parent + const result = await handle_load({ + event: { + url: event.url, + params: event.params, + data: server_data_node?.data ?? null, + route: event.route, + fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), + setHeaders: event.setHeaders, + depends: () => {}, + parent + }, + resolve: node.universal.load }); if (__SVELTEKIT_DEV__) { diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index e320b01f04a4..af4276d1545d 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -50,7 +50,8 @@ export async function respond_with_error({ event, state, node: default_layout, - parent: async () => ({}) + parent: async () => ({}), + handle_load: options.hooks.handleServerLoad }); const server_data = await server_data_promise; @@ -63,6 +64,7 @@ export async function respond_with_error({ resolve_opts, server_data_promise, state, + handle_load: options.hooks.handleLoad, csr }); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 2234eca3f4ef..796be0863aa2 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -12,7 +12,9 @@ import { ServerInitOptions, HandleFetch, Actions, - HandleClientError + HandleClientError, + HandleLoad, + HandleServerLoad } from '@sveltejs/kit'; import { HttpMethod, @@ -98,10 +100,13 @@ export interface ServerHooks { handleFetch: HandleFetch; handle: Handle; handleError: HandleServerError; + handleLoad: HandleLoad; + handleServerLoad: HandleServerLoad; } export interface ClientHooks { handleError: HandleClientError; + handleLoad: HandleLoad; } export interface Env { diff --git a/packages/kit/test/apps/basics/src/hooks.client.js b/packages/kit/test/apps/basics/src/hooks.client.js index 8e18bf084c06..a3b022884fda 100644 --- a/packages/kit/test/apps/basics/src/hooks.client.js +++ b/packages/kit/test/apps/basics/src/hooks.client.js @@ -8,3 +8,21 @@ export function handleError({ error, event, status, message }) { ? undefined : { message: `${/** @type {Error} */ (error).message} (${status} ${message})` }; } + +/** @type {import("@sveltejs/kit").HandleLoad} */ +export async function handleLoad({ event, resolve }) { + if (event.url.pathname.endsWith('/handle-load/bypass')) { + return { + from: 'handleLoad', + foo: { bar: 'needed for root layout ' } + }; + } else if (event.url.pathname.endsWith('/handle-load/enrich')) { + const result = await resolve(event); + return { + from: 'handleLoad and ' + /** @type {any} */ (result).from, + foo: { bar: 'needed for root layout ' } + }; + } else { + return resolve(event); + } +} diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index 1251601e35af..7ac8a03fec8d 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -155,3 +155,39 @@ export async function handleFetch({ request, fetch }) { return fetch(request); } + +/** @type {import("@sveltejs/kit").HandleLoad} */ +export async function handleLoad({ event, resolve }) { + if (event.url.pathname.endsWith('/handle-load/bypass')) { + return { + from: 'handleLoad', + foo: { bar: 'needed for root layout ' } + }; + } else if (event.url.pathname.endsWith('/handle-load/enrich')) { + const result = await resolve(event); + return { + from: 'handleLoad and ' + /** @type {any} */ (result).from, + foo: { bar: 'needed for root layout ' } + }; + } else { + return resolve(event); + } +} + +/** @type {import("@sveltejs/kit").HandleServerLoad} */ +export async function handleServerLoad({ event, resolve }) { + if (event.url.pathname.endsWith('/handle-server-load/bypass')) { + return { + from: 'handleServerLoad', + foo: { bar: 'needed for root layout ' } + }; + } else if (event.url.pathname.endsWith('/handle-server-load/enrich')) { + const result = await resolve(event); + return { + from: 'handleServerLoad and ' + /** @type {any} */ (result).from, + foo: { bar: 'needed for root layout ' } + }; + } else { + return resolve(event); + } +} diff --git a/packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.js b/packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.js new file mode 100644 index 000000000000..8134e5a07d80 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.js @@ -0,0 +1,5 @@ +export function load() { + return { + from: 'load' + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.svelte b/packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.svelte new file mode 100644 index 000000000000..6af34e7a28f9 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-load/bypass/+page.svelte @@ -0,0 +1,5 @@ + + +

{data.from}

diff --git a/packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.js b/packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.js new file mode 100644 index 000000000000..8134e5a07d80 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.js @@ -0,0 +1,5 @@ +export function load() { + return { + from: 'load' + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.svelte b/packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.svelte new file mode 100644 index 000000000000..6af34e7a28f9 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-load/enrich/+page.svelte @@ -0,0 +1,5 @@ + + +

{data.from}

diff --git a/packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.server.js b/packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.server.js new file mode 100644 index 000000000000..9ae5e268d250 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.server.js @@ -0,0 +1,5 @@ +export function load() { + return { + from: 'serverload' + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.svelte b/packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.svelte new file mode 100644 index 000000000000..6af34e7a28f9 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-server-load/bypass/+page.svelte @@ -0,0 +1,5 @@ + + +

{data.from}

diff --git a/packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.server.js b/packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.server.js new file mode 100644 index 000000000000..9ae5e268d250 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.server.js @@ -0,0 +1,5 @@ +export function load() { + return { + from: 'serverload' + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.svelte b/packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.svelte new file mode 100644 index 000000000000..6af34e7a28f9 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/handle-server-load/enrich/+page.svelte @@ -0,0 +1,5 @@ + + +

{data.from}

diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index ccfdd3d6ca73..9e067778245a 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1306,3 +1306,75 @@ test.describe.serial('Cookies API', () => { expect(await span.innerText()).toContain('undefined'); }); }); + +test.describe('handleLoad', () => { + test('Bypasses user-defined load function (direct hit)', async ({ page }) => { + await page.goto('/handle-load/bypass'); + expect(await page.textContent('p')).toBe('handleLoad'); + }); + + test('Enriches user-defined load function (direct hit)', async ({ page }) => { + await page.goto('/handle-load/enrich'); + expect(await page.textContent('p')).toBe('handleLoad and load'); + }); + + test('Bypasses user-defined load function (client nav)', async ({ + javaScriptEnabled, + page, + app + }) => { + if (!javaScriptEnabled) return; + + await page.goto('/'); + await app.goto('/handle-load/bypass'); + expect(await page.textContent('p')).toBe('handleLoad'); + }); + + test('Enriches user-defined load function (client nav)', async ({ + javaScriptEnabled, + page, + app + }) => { + if (!javaScriptEnabled) return; + + await page.goto('/'); + await app.goto('/handle-load/enrich'); + expect(await page.textContent('p')).toBe('handleLoad and load'); + }); +}); + +test.describe('handleServerLoad', () => { + test('Bypasses user-defined load function (direct hit)', async ({ page }) => { + await page.goto('/handle-server-load/bypass'); + expect(await page.textContent('p')).toBe('handleServerLoad'); + }); + + test('Enriches user-defined load function (direct hit)', async ({ page }) => { + await page.goto('/handle-server-load/enrich'); + expect(await page.textContent('p')).toBe('handleServerLoad and serverload'); + }); + + test('Bypasses user-defined load function (client nav)', async ({ + javaScriptEnabled, + page, + app + }) => { + if (!javaScriptEnabled) return; + + await page.goto('/'); + await app.goto('/handle-server-load/bypass'); + expect(await page.textContent('p')).toBe('handleServerLoad'); + }); + + test('Enriches user-defined load function (client nav)', async ({ + javaScriptEnabled, + page, + app + }) => { + if (!javaScriptEnabled) return; + + await page.goto('/'); + await app.goto('/handle-server-load/enrich'); + expect(await page.textContent('p')).toBe('handleServerLoad and serverload'); + }); +}); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 59c18f3cf923..9239467a4994 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -665,6 +665,22 @@ declare module '@sveltejs/kit' { fetch: typeof fetch; }) => MaybePromise; + /** + * The `handleLoad` hook runs every time a universal `load` function (for example from +page.js) is called. + * This hook can be registered on the client as well as on the server side. + * This hook provides the load `event` and a `resolve` function to call the actual hook with the event. + */ + export type HandleLoad = (input: { event: LoadEvent; resolve: Load }) => ReturnType; + + /** + * The `handleServerLoad` hook runs every time a server-only `load` function (for example from +page.server.js) is called on the server. + * This hook provides the server load `event` and a `resolve` function to call the actual hook with the event. + */ + export type HandleServerLoad = (input: { + event: ServerLoadEvent; + resolve: ServerLoad; + }) => ReturnType; + /** * The generic form of `PageLoad` and `LayoutLoad`. You should import those from `./$types` (see [generated types](https://kit.svelte.dev/docs/types#generated-types)) * rather than using `Load` directly. diff --git a/sites/kit.svelte.dev/src/routes/bar/[bar]/+page.js b/sites/kit.svelte.dev/src/routes/bar/[bar]/+page.js new file mode 100644 index 000000000000..f5f3ca2c905f --- /dev/null +++ b/sites/kit.svelte.dev/src/routes/bar/[bar]/+page.js @@ -0,0 +1,14 @@ +import { browser } from '$app/environment'; + +let a = 1; + +export function load({ url }) { + const a_copy = a + 1; + if (browser) { + a = a_copy; + } + return { + url, + a + }; +} diff --git a/sites/kit.svelte.dev/src/routes/bar/[bar]/+page.svelte b/sites/kit.svelte.dev/src/routes/bar/[bar]/+page.svelte new file mode 100644 index 000000000000..b4fbb1c8ec6d --- /dev/null +++ b/sites/kit.svelte.dev/src/routes/bar/[bar]/+page.svelte @@ -0,0 +1,6 @@ + + +

{JSON.stringify(data)}

+bar/10 diff --git a/sites/kit.svelte.dev/src/routes/foo/+page.js b/sites/kit.svelte.dev/src/routes/foo/+page.js new file mode 100644 index 000000000000..68bef861735a --- /dev/null +++ b/sites/kit.svelte.dev/src/routes/foo/+page.js @@ -0,0 +1,5 @@ +export function load() { + return { + x: 1 + }; +} diff --git a/sites/kit.svelte.dev/src/routes/foo/+page.svelte b/sites/kit.svelte.dev/src/routes/foo/+page.svelte new file mode 100644 index 000000000000..3e39a5f58e2d --- /dev/null +++ b/sites/kit.svelte.dev/src/routes/foo/+page.svelte @@ -0,0 +1,17 @@ + + +

{data.x}

+

{$page.state.a}

+ +bar/20 From 4faa9696696c852963006d1515df377e5cede13e Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 16:44:58 +0100 Subject: [PATCH 2/3] use untrack --- documentation/docs/30-advanced/20-hooks.md | 20 +++++++++++++------ packages/kit/src/runtime/client/fetcher.js | 3 ++- .../kit/test/apps/basics/src/hooks.client.js | 6 ++++-- .../kit/test/apps/basics/src/hooks.server.js | 6 ++++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index ba4752e74596..705f502ddebf 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -140,18 +140,20 @@ This function allows you to wrap server `load` functions with custom behavior. I ```js /** @type {import("@sveltejs/kit").HandleServerLoad} */ export async function handleServerLoad({ event, resolve }) { - if (event.url.pathname.endsWith('/bypass')) { + const { untrack, url, parent } = event; + + if (untrack(() => url.pathname.endsWith('/bypass'))) { // Do not call load function at all return { from: 'handleServerLoad' }; - } else if (event.url.pathname.endsWith('/enrich')) { + } else if (untrack(() => url.pathname.endsWith('/enrich'))) { // Call load function with modified inputs and adjust result const result = await resolve({ ...event, parent: () => { console.log('called parent'); - return event.parent(); + return parent(); } }); return { @@ -164,6 +166,8 @@ export async function handleServerLoad({ event, resolve }) { } ``` +Not how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`. + ## Shared hooks The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`: @@ -175,18 +179,20 @@ This function allows you to wrap universal `load` functions with custom behavior ```js /** @type {import("@sveltejs/kit").HandleLoad} */ export async function handleLoad({ event, resolve }) { - if (event.url.pathname.endsWith('/bypass')) { + const { untrack, url, parent } = event; + + if (untrack(() => url.pathname.endsWith('/bypass'))) { // Do not call load function at all return { from: 'handleLoad' }; - } else if (event.url.pathname.endsWith('/enrich')) { + } else if (untrack(() => url.pathname.endsWith('/enrich'))) { // Call load function with modified inputs and adjust result const result = await resolve({ ...event, parent: () => { console.log('called parent'); - return event.parent(); + return parent(); } }); return { @@ -199,6 +205,8 @@ export async function handleLoad({ event, resolve }) { } ``` +Not how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`. + ### handleError If an [unexpected error](/docs/errors#unexpected-errors) is thrown during loading or rendering, this function will be called with the `error`, `event`, `status` code and `message`. This allows for two things: diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 6692ea13eb46..17fa15986d67 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -41,7 +41,8 @@ if (DEV) { const stack = stack_array.slice(0, cutoff + 2).join('\n'); const in_load_heuristic = can_inspect_stack_trace - ? stack.includes('src/runtime/client/client.js') + ? // app.js if people have no handleLoad hook, else hooks.client.js + stack.includes('generated/client/app.js') || stack.includes('src/hooks.client.js') : loading; // This flag is set in initial_fetch and subsequent_fetch diff --git a/packages/kit/test/apps/basics/src/hooks.client.js b/packages/kit/test/apps/basics/src/hooks.client.js index a3b022884fda..ab45f3be7773 100644 --- a/packages/kit/test/apps/basics/src/hooks.client.js +++ b/packages/kit/test/apps/basics/src/hooks.client.js @@ -11,12 +11,14 @@ export function handleError({ error, event, status, message }) { /** @type {import("@sveltejs/kit").HandleLoad} */ export async function handleLoad({ event, resolve }) { - if (event.url.pathname.endsWith('/handle-load/bypass')) { + const { untrack, url } = event; + + if (untrack(() => url.pathname.endsWith('/handle-load/bypass'))) { return { from: 'handleLoad', foo: { bar: 'needed for root layout ' } }; - } else if (event.url.pathname.endsWith('/handle-load/enrich')) { + } else if (untrack(() => url.pathname.endsWith('/handle-load/enrich'))) { const result = await resolve(event); return { from: 'handleLoad and ' + /** @type {any} */ (result).from, diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index 7ac8a03fec8d..3f7aa86e70ae 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -176,12 +176,14 @@ export async function handleLoad({ event, resolve }) { /** @type {import("@sveltejs/kit").HandleServerLoad} */ export async function handleServerLoad({ event, resolve }) { - if (event.url.pathname.endsWith('/handle-server-load/bypass')) { + const { untrack, url } = event; + + if (untrack(() => url.pathname.endsWith('/handle-server-load/bypass'))) { return { from: 'handleServerLoad', foo: { bar: 'needed for root layout ' } }; - } else if (event.url.pathname.endsWith('/handle-server-load/enrich')) { + } else if (untrack(() => url.pathname.endsWith('/handle-server-load/enrich'))) { const result = await resolve(event); return { from: 'handleServerLoad and ' + /** @type {any} */ (result).from, From 15f508a4892bafbbdbd9190ad9bddd074071b918 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:55:08 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Lukas Stracke --- documentation/docs/30-advanced/20-hooks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 705f502ddebf..184e20bf2862 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -166,7 +166,7 @@ export async function handleServerLoad({ event, resolve }) { } ``` -Not how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`. +Note how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`. ## Shared hooks @@ -205,7 +205,7 @@ export async function handleLoad({ event, resolve }) { } ``` -Not how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`. +Note how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`. ### handleError