diff --git a/.changeset/eight-poems-learn.md b/.changeset/eight-poems-learn.md new file mode 100644 index 000000000000..62d9e4224d54 --- /dev/null +++ b/.changeset/eight-poems-learn.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: server and client `init` hook diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 6decc1cbf463..9663d724fc18 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -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). diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 1b97ed02e4c1..fe6127de934c 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -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 || ' : ''}(() => {}) }; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 84a3f041c322..be184575530f 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -724,6 +724,16 @@ export type HandleFetch = (input: { fetch: typeof fetch; }) => MaybePromise; +/** + * 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; + +/** + * 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; + /** * 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 diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index bec38c9122fa..820e0567c0fa 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -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; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 36cbd04be16f..5aa10ac560f5 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -15,6 +15,9 @@ const prerender_env_handler = { } }; +/** @type {Promise} */ +let init_promise; + export class Server { /** @type {import('types').SSROptions} */ #options; @@ -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(); @@ -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 = { @@ -87,7 +96,7 @@ export class Server { throw error; } } - } + })()); } /** diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index c9dbb51ce007..cd7b1518b86a 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -17,7 +17,9 @@ import { RequestEvent, SSRManifest, Emulator, - Adapter + Adapter, + ServerInit, + ClientInit } from '@sveltejs/kit'; import { HttpMethod, @@ -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 { diff --git a/packages/kit/test/apps/basics/src/hooks.client.js b/packages/kit/test/apps/basics/src/hooks.client.js index 8e18bf084c06..3749d91b5aa3 100644 --- a/packages/kit/test/apps/basics/src/hooks.client.js +++ b/packages/kit/test/apps/basics/src/hooks.client.js @@ -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'); +} diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index b7cbbd9004eb..e485d038d995 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -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` @@ -154,3 +155,7 @@ export async function handleFetch({ request, fetch }) { return fetch(request); } + +export function init() { + _set_from_init(); +} diff --git a/packages/kit/test/apps/basics/src/routes/init-hooks/+page.server.ts b/packages/kit/test/apps/basics/src/routes/init-hooks/+page.server.ts new file mode 100644 index 000000000000..bccb323eaa3b --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/init-hooks/+page.server.ts @@ -0,0 +1,11 @@ +let did_init_run = 0; + +export function _set_from_init() { + did_init_run++; +} + +export function load() { + return { + did_init_run + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/init-hooks/+page.svelte b/packages/kit/test/apps/basics/src/routes/init-hooks/+page.svelte new file mode 100644 index 000000000000..f421cfb32e9f --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/init-hooks/+page.svelte @@ -0,0 +1,7 @@ + + +

{data.did_init_run}

+ +navigate diff --git a/packages/kit/test/apps/basics/src/routes/init-hooks/navigate/+page.svelte b/packages/kit/test/apps/basics/src/routes/init-hooks/navigate/+page.svelte new file mode 100644 index 000000000000..3cb4eaa0ec04 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/init-hooks/navigate/+page.svelte @@ -0,0 +1 @@ +navigated diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 56b2c96bc744..2286a6e6a234 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -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 diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index ff77c55f0204..3c57fc9904c2 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -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'); + }); +}); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 63fdcb004bf3..748a6a864b3e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -706,6 +706,16 @@ declare module '@sveltejs/kit' { fetch: typeof fetch; }) => MaybePromise; + /** + * 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; + + /** + * 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; + /** * 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