Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/eight-poems-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: server and client `init` hook
30 changes: 30 additions & 0 deletions documentation/docs/30-advanced/20-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,36 @@ During development, if an error occurs because of a syntax error in your Svelte

> [!NOTE] Make sure that `handleError` _never_ throws an error

### init

This function is the very first function that will be invoked in the SvelteKit context both on the server (when the server starts) and on the client (when the client side app starts). It can be asynchronous and it will be awaited by the SvelteKit runtime. It's guaranteed to only run once.

On the server it can be used to initialize your database connection, setup your mocks or prepare whatever state your application will need.

> [!NOTE] If your environment supports top level await the `init` function is really not different from writing your initialization in the module itself but it can be useful if you don't have that luxury.

```js
/// file: src/hooks.server.js

let db;

/** @type {import('@sveltejs/kit').ServerInit} */
export async function init() {
db = await client.connect();
}
```

On the client this is the only way to actually stop svelte from hydrating your code until your initialization run. Pay attention to what you put in your init because the app will be unresponsive until the `init` hook completes (it will not execute again on navigation tho).

```js
/// file: src/hooks.client.js

/** @type {import('@sveltejs/kit').ServerInit} */
export async function init() {
await loadSomeDataNeededForHydration();
}
```

## Universal hooks

The following can be added to `src/hooks.js`. Universal hooks run on both server and client (not to be confused with shared hooks, which are environment-specific).
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
handleError: ${
client_hooks_file ? 'client_hooks.handleError || ' : ''
}(({ error }) => { console.error(error) }),
${client_hooks_file ? 'init: client_hooks.init,' : ''}

reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {})
reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}),
};

export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,16 @@ export type HandleFetch = (input: {
fetch: typeof fetch;
}) => MaybePromise<Response>;

/**
* The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once as soon as the server is executed.
*/
export type ServerInit = () => MaybePromise<void>;

/**
* The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once as the client side app is started.
*/
export type ClientInit = () => MaybePromise<void>;

/**
* The [`reroute`](https://svelte.dev/docs/kit/hooks#Universal-hooks-reroute) hook allows you to modify the URL before it is used to determine which route to render.
* @since 2.3.0
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ export async function start(_app, _target, hydrate) {
}

app = _app;
if (app.hooks.init != null) {
await app.hooks.init();
}
routes = parse(_app);
container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
target = _target;
Expand Down
1 change: 0 additions & 1 deletion packages/kit/src/runtime/server/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
declare module '__SERVER__/internal.js' {
export const options: import('types').SSROptions;
export const get_hooks: () => Promise<Partial<import('types').ServerHooks>>;
}
4 changes: 4 additions & 0 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export class Server {
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)),
reroute: module.reroute || (() => {})
};

if (module.init) {
await module.init();
}
} catch (error) {
if (DEV) {
this.#options.hooks = {
Expand Down
6 changes: 5 additions & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
RequestEvent,
SSRManifest,
Emulator,
Adapter
Adapter,
ServerInit,
ClientInit
} from '@sveltejs/kit';
import {
HttpMethod,
Expand Down Expand Up @@ -109,11 +111,13 @@ export interface ServerHooks {
handle: Handle;
handleError: HandleServerError;
reroute: Reroute;
init?: ServerInit;
}

export interface ClientHooks {
handleError: HandleClientError;
reroute: Reroute;
init?: ClientInit;
}

export interface Env {
Expand Down
4 changes: 4 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,7 @@ export function handleError({ error, event, status, message }) {
? undefined
: { message: `${/** @type {Error} */ (error).message} (${status} ${message})` };
}

export function init() {
console.log('init hooks.client.js');
}
9 changes: 7 additions & 2 deletions packages/kit/test/apps/basics/src/hooks.server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from 'node:fs';
import { sequence } from '@sveltejs/kit/hooks';
import { error, isHttpError, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import fs from 'node:fs';
import { COOKIE_NAME } from './routes/cookies/shared';
import { _set_from_init } from './routes/init-hooks/+page.server';

/**
* Transform an error into a POJO, by copying its `name`, `message`
Expand Down Expand Up @@ -154,3 +155,7 @@ export async function handleFetch({ request, fetch }) {

return fetch(request);
}

export function init() {
_set_from_init();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
let did_init_run = 0;

export function _set_from_init() {
did_init_run++;
}

export function load() {
return {
did_init_run
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
const { data } = $props();
</script>

<p>{data.did_init_run}</p>

<a href="/init-hooks/navigate">navigate</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
navigated
23 changes: 23 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,29 @@ test.describe('reroute', () => {
});
});

test.describe('init', () => {
test('init client hook is called once when the application start on the client', async ({
page
}) => {
/**
* @type string[]
*/
const logs = [];
page.addListener('console', (message) => {
if (message.type() === 'log') {
logs.push(message.text());
}
});
const log_event = page.waitForEvent('console');
await page.goto('/init-hooks');
await log_event;
expect(logs).toStrictEqual(['init hooks.client.js']);
await page.getByRole('link').first().click();
await page.waitForLoadState('load');
expect(logs).toStrictEqual(['init hooks.client.js']);
});
});

test.describe('INP', () => {
test('does not block next paint', async ({ page }) => {
// Thanks to https://publishing-project.rivendellweb.net/measuring-performance-tasks-with-playwright/#interaction-to-next-paint-inp
Expand Down
9 changes: 9 additions & 0 deletions packages/kit/test/apps/basics/test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,3 +648,12 @@ test.describe('reroute', () => {
expect(response?.status()).toBe(500);
});
});

test.describe('init', () => {
test('init server hook is called once before the load function', async ({ page }) => {
await page.goto('/init-hooks');
await expect(page.locator('p')).toHaveText('1');
await page.reload();
await expect(page.locator('p')).toHaveText('1');
});
});
10 changes: 10 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,16 @@ declare module '@sveltejs/kit' {
fetch: typeof fetch;
}) => MaybePromise<Response>;

/**
* The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once as soon as the server is executed.
*/
export type ServerInit = () => MaybePromise<void>;

/**
* The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once as the client side app is started.
*/
export type ClientInit = () => MaybePromise<void>;

/**
* The [`reroute`](https://svelte.dev/docs/kit/hooks#Universal-hooks-reroute) hook allows you to modify the URL before it is used to determine which route to render.
* @since 2.3.0
Expand Down