Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handleLoad hooks #11313

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions documentation/docs/30-advanced/20-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,80 @@ 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 }) {
const { untrack, url, parent } = event;

if (untrack(() => url.pathname.endsWith('/bypass'))) {
// Do not call load function at all
return {
from: 'handleServerLoad'
};
} 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 parent();
}
});
return {
from: 'handleServerLoad and ' + /** @type {any} */ (result).from
};
} else {
// Call load function directly
return resolve(event);
}
}
```

Not how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

## 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 }) {
const { untrack, url, parent } = event;

if (untrack(() => url.pathname.endsWith('/bypass'))) {
// Do not call load function at all
return {
from: 'handleLoad'
};
} 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 parent();
}
});
return {
from: 'handleLoad and ' + /** @type {any} */ (result).from
};
} else {
// Call load function directly
return resolve(event);
}
}
```

Not how we're using `untrack` to avoid rerunning all load functions on any URL change because of accessing `url.pathname`.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

### 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:
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'}';
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,22 @@ export type HandleFetch = (input: {
fetch: typeof fetch;
}) => MaybePromise<Response>;

/**
* 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<Load>;

/**
* 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<ServerLoad>;

/**
* 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.
Expand Down
7 changes: 5 additions & 2 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,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 ${
Expand All @@ -650,7 +652,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;
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/client/fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/server/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}>;
}
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export async function render_data(
}
}
return data;
}
},
handle_load: options.hooks.handleServerLoad
});
} catch (e) {
aborted = true;
Expand Down
8 changes: 6 additions & 2 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 24 additions & 13 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { validate_depends } from '../../shared.js';
* state: import('types').SSRState;
* node: import('types').SSRNode | undefined;
* parent: () => Promise<Record<string, any>>;
* handle_load: import('@sveltejs/kit').HandleServerLoad
* }} opts
* @returns {Promise<import('types').ServerDataNode | null>}
*/
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;
Expand Down Expand Up @@ -57,7 +58,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);
Expand Down Expand Up @@ -142,7 +144,11 @@ export async function load_server_data({ event, state, node, parent }) {
is_tracking = true;
}
}
});
};

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);
Expand All @@ -168,6 +174,7 @@ export async function load_server_data({ event, state, node, parent }) {
* resolve_opts: import('types').RequiredResolveOptions;
* server_data_promise: Promise<import('types').ServerDataNode | null>;
* state: import('types').SSRState;
* handle_load: import('@sveltejs/kit').HandleLoad
* csr: boolean;
* }} opts
* @returns {Promise<Record<string, any | Promise<any>> | null>}
Expand All @@ -180,6 +187,7 @@ export async function load_data({
server_data_promise,
state,
resolve_opts,
handle_load,
csr
}) {
const server_data_node = await server_data_promise;
Expand All @@ -188,16 +196,19 @@ 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,
untrack: (fn) => fn()
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,
untrack: (fn) => fn()
},
resolve: node.universal.load
});

if (__SVELTEKIT_DEV__) {
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/runtime/server/page/respond_with_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -63,6 +64,7 @@ export async function respond_with_error({
resolve_opts,
server_data_promise,
state,
handle_load: options.hooks.handleLoad,
csr
});

Expand Down
7 changes: 6 additions & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
ServerInitOptions,
HandleFetch,
Actions,
HandleClientError
HandleClientError,
HandleLoad,
HandleServerLoad
} from '@sveltejs/kit';
import {
HttpMethod,
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions packages/kit/test/apps/basics/src/hooks.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,23 @@ 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 }) {
const { untrack, url } = event;

if (untrack(() => url.pathname.endsWith('/handle-load/bypass'))) {
return {
from: 'handleLoad',
foo: { bar: 'needed for root layout ' }
};
} else if (untrack(() => 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);
}
}
38 changes: 38 additions & 0 deletions packages/kit/test/apps/basics/src/hooks.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,41 @@ 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 }) {
const { untrack, url } = event;

if (untrack(() => url.pathname.endsWith('/handle-server-load/bypass'))) {
return {
from: 'handleServerLoad',
foo: { bar: 'needed for root layout ' }
};
} else if (untrack(() => 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function load() {
return {
from: 'load'
};
}
Loading
Loading