Skip to content

Commit

Permalink
feat: server and client init hook (#13103)
Browse files Browse the repository at this point in the history
* feat: server and client `init` hook

* fix: provide client_hooks fallback even if universal hooks are present

* chore: add tests

* chore: cache server instance in `vite dev`

* chore: remove only (duh)

* chore: revert single server in dev

* chore: revert weird did_it_run thing

* chore: add additional inited check to prevent multiple init in case of race conditions

* fix: remove `init` from client hooks

* docs: write some documentation, use proper exported types

* chore: regenerate types

* simplify

* chore: docs suggestions from review

Co-authored-by: Rich Harris <hello@rich-harris.dev>

* try this

* chore: fix lint

* chore: fix types

* save a couple bytes

* Apply suggestions from code review

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 10, 2024
1 parent 85b5716 commit 9fc5ff3
Show file tree
Hide file tree
Showing 15 changed files with 126 additions and 5 deletions.
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
19 changes: 19 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,25 @@ 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 runs once, when the server is created or the app starts in the browser, and is a useful place to do asynchronous work such as initializing a database connection.

> [!NOTE] If your environment supports top-level await, the `init` function is really no different from writing your initialisation logic at the top level of the module, but some environments — most notably, Safari — don't.
```js
/// file: src/hooks.server.js
import * as db from '$lib/server/database';

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

> [!NOTE]
> In the browser, asynchronous work in `init` will delay hydration, so be mindful of what you put in there.
## 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
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ 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 || ' : ''}(() => {})
};
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 before the server responds to its first request
*/
export type ServerInit = () => MaybePromise<void>;

/**
* The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once the app starts in the browser
*/
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;

await _app.hooks.init?.();

routes = parse(_app);
container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
target = _target;
Expand Down
13 changes: 11 additions & 2 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const prerender_env_handler = {
}
};

/** @type {Promise<any>} */
let init_promise;

export class Server {
/** @type {import('types').SSROptions} */
#options;
Expand Down Expand Up @@ -63,7 +66,9 @@ export class Server {
set_read_implementation(read);
}

if (!this.#options.hooks) {
// During DEV and for some adapters this function might be called in quick succession,
// so we need to make sure we're not invoking this logic (most notably the init hook) multiple times
await (init_promise ??= (async () => {
try {
const module = await get_hooks();

Expand All @@ -73,6 +78,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 All @@ -87,7 +96,7 @@ export class Server {
throw error;
}
}
}
})());
}

/**
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 before the server responds to its first request
*/
export type ServerInit = () => MaybePromise<void>;

/**
* The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once the app starts in the browser
*/
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

0 comments on commit 9fc5ff3

Please sign in to comment.