From a86cffd88c71c26fbe2fadd3d5f5ac3de23061c9 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 3 Dec 2024 18:41:53 +0100 Subject: [PATCH 01/18] feat: server and client `init` hook --- .changeset/eight-poems-learn.md | 5 +++++ packages/kit/src/core/sync/write_client_manifest.js | 3 ++- packages/kit/src/runtime/client/client.js | 3 +++ packages/kit/src/runtime/server/index.js | 4 ++++ packages/kit/src/types/internal.d.ts | 2 ++ 5 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/eight-poems-learn.md 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/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index 1b97ed02e4c1..cd2d19f0b29c 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -152,7 +152,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { client_hooks_file ? 'client_hooks.handleError || ' : '' }(({ error }) => { console.error(error) }), - reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}) + reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}), + init: ${universal_hooks_file ? 'universal_hooks.init || ' : client_hooks_file ? 'client_hooks.init || ' : ''}(() => {}), }; export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index bec38c9122fa..e287cf1cb7c3 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; + if (app.hooks.init != null) { + 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..5a2f11447c70 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -67,6 +67,10 @@ export class Server { try { const module = await get_hooks(); + if (module.init != null) { + await module.init(); + } + this.#options.hooks = { handle: module.handle || (({ event, resolve }) => resolve(event)), handleError: module.handleError || (({ error }) => console.error(error)), diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index c9dbb51ce007..9e24539c93dc 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -109,11 +109,13 @@ export interface ServerHooks { handle: Handle; handleError: HandleServerError; reroute: Reroute; + init?: () => void | Promise; } export interface ClientHooks { handleError: HandleClientError; reroute: Reroute; + init?: () => void | Promise; } export interface Env { From 7950b9ac58b8ada631ae85c3bd3120fa58841f6a Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 01:53:00 +0100 Subject: [PATCH 02/18] fix: provide client_hooks fallback even if universal hooks are present --- packages/kit/src/core/sync/write_client_manifest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index cd2d19f0b29c..bb4c65cc57ba 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -153,7 +153,7 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { }(({ error }) => { console.error(error) }), reroute: ${universal_hooks_file ? 'universal_hooks.reroute || ' : ''}(() => {}), - init: ${universal_hooks_file ? 'universal_hooks.init || ' : client_hooks_file ? 'client_hooks.init || ' : ''}(() => {}), + init: ${universal_hooks_file ? `(universal_hooks.init${client_hooks_file ? ' ?? client_hooks.init' : ''}) || ` : client_hooks_file ? 'client_hooks.init || ' : ''}(() => {}), }; export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; From 10f4b840226350261dbd3970c131b4791a5158ce Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 01:54:08 +0100 Subject: [PATCH 03/18] chore: add tests --- .../kit/test/apps/basics/src/hooks.client.js | 4 ++++ .../kit/test/apps/basics/src/hooks.server.js | 9 ++++++-- .../src/routes/init-hooks/+page.server.ts | 12 ++++++++++ .../basics/src/routes/init-hooks/+page.svelte | 7 ++++++ .../routes/init-hooks/navigate/+page.svelte | 1 + .../kit/test/apps/basics/test/client.test.js | 23 +++++++++++++++++++ .../kit/test/apps/basics/test/server.test.js | 12 ++++++++++ 7 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/init-hooks/+page.server.ts create mode 100644 packages/kit/test/apps/basics/src/routes/init-hooks/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/init-hooks/navigate/+page.svelte 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..99bcf44403a2 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/init-hooks/+page.server.ts @@ -0,0 +1,12 @@ +const did_init_run = {}; + +export function _set_from_init() { + did_init_run[process.env.TEST_WORKER_INDEX] ??= 0; + did_init_run[process.env.TEST_WORKER_INDEX]++; +} + +export function load() { + return { + did_init_run: did_init_run[process.env.TEST_WORKER_INDEX] + }; +} 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..b074c01d49c6 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -648,3 +648,15 @@ test.describe('reroute', () => { expect(response?.status()).toBe(500); }); }); + +test.describe('init', () => { + // we need to skip in dev because the vite dev middleware create + // multiple servers and it would be flaky + test.skip(() => process.env.DEV); + 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'); + }); +}); From 2a051d3747a79f8236d556cf6c8f16d9f16feff2 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 02:02:11 +0100 Subject: [PATCH 04/18] chore: cache server instance in `vite dev` --- packages/kit/src/exports/vite/dev/index.js | 7 ++++++- packages/kit/test/apps/basics/test/server.test.js | 5 +---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 3935daf28565..198c7f8b64d2 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -428,6 +428,9 @@ export async function dev(vite, vite_config, svelte_config) { // serving routes with those names. See https://github.com/vitejs/vite/issues/7363 remove_static_middlewares(vite.middlewares); + /** @type {InstanceType} */ + let server; + vite.middlewares.use(async (req, res) => { // Vite's base middleware strips out the base path. Restore it const original_url = req.url; @@ -484,7 +487,9 @@ export async function dev(vite, vite_config, svelte_config) { const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths'); set_assets(assets); - const server = new Server(manifest); + if (!server) { + server = new Server(manifest); + } await server.init({ env, diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index b074c01d49c6..79c1a22ce355 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -649,10 +649,7 @@ test.describe('reroute', () => { }); }); -test.describe('init', () => { - // we need to skip in dev because the vite dev middleware create - // multiple servers and it would be flaky - test.skip(() => process.env.DEV); +test.describe.only('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'); From 306d37f4c8717ff99f33763c99a00012c9e189a5 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 02:06:10 +0100 Subject: [PATCH 05/18] chore: remove only (duh) --- packages/kit/test/apps/basics/test/server.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 79c1a22ce355..3c57fc9904c2 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -649,7 +649,7 @@ test.describe('reroute', () => { }); }); -test.describe.only('init', () => { +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'); From 61aaf240c418bf7264cb53cab3bceb310e6d325c Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 09:23:49 +0100 Subject: [PATCH 06/18] chore: revert single server in dev --- packages/kit/src/exports/vite/dev/index.js | 7 +------ packages/kit/test/apps/basics/test/server.test.js | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index 198c7f8b64d2..3935daf28565 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -428,9 +428,6 @@ export async function dev(vite, vite_config, svelte_config) { // serving routes with those names. See https://github.com/vitejs/vite/issues/7363 remove_static_middlewares(vite.middlewares); - /** @type {InstanceType} */ - let server; - vite.middlewares.use(async (req, res) => { // Vite's base middleware strips out the base path. Restore it const original_url = req.url; @@ -487,9 +484,7 @@ export async function dev(vite, vite_config, svelte_config) { const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths'); set_assets(assets); - if (!server) { - server = new Server(manifest); - } + const server = new Server(manifest); await server.init({ env, diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 3c57fc9904c2..b074c01d49c6 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -650,6 +650,9 @@ test.describe('reroute', () => { }); test.describe('init', () => { + // we need to skip in dev because the vite dev middleware create + // multiple servers and it would be flaky + test.skip(() => process.env.DEV); 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'); From 7ba6cd33f996a71301c66cb7d6acbc04ffd9faf9 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 09:27:04 +0100 Subject: [PATCH 07/18] chore: revert weird did_it_run thing --- .../test/apps/basics/src/routes/init-hooks/+page.server.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 99bcf44403a2..bccb323eaa3b 100644 --- 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 @@ -1,12 +1,11 @@ -const did_init_run = {}; +let did_init_run = 0; export function _set_from_init() { - did_init_run[process.env.TEST_WORKER_INDEX] ??= 0; - did_init_run[process.env.TEST_WORKER_INDEX]++; + did_init_run++; } export function load() { return { - did_init_run: did_init_run[process.env.TEST_WORKER_INDEX] + did_init_run }; } From 5cace243550a3cd93dee44446e1da5769b278ac0 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 12:44:31 +0100 Subject: [PATCH 08/18] chore: add additional inited check to prevent multiple init in case of race conditions --- packages/kit/src/core/sync/write_server.js | 6 ++++++ packages/kit/src/runtime/server/ambient.d.ts | 2 ++ packages/kit/src/runtime/server/index.js | 5 +++-- packages/kit/test/apps/basics/test/server.test.js | 3 --- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index eb50bfd4735b..5d17b2809319 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -70,6 +70,12 @@ export async function get_hooks() { }; } +export let inited = false; + +export function set_inited(){ + inited = true; +} + export { set_assets, set_building, set_manifest, set_prerendering, set_private_env, set_public_env, set_read_implementation, set_safe_public_env }; `; diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index 2f38261123dc..d9cf9f46e59a 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -1,4 +1,6 @@ declare module '__SERVER__/internal.js' { export const options: import('types').SSROptions; export const get_hooks: () => Promise>; + export let inited: boolean; + export let set_inited: () => void; } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 5a2f11447c70..cb3db2e17917 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,6 +1,6 @@ import { respond } from './respond.js'; import { set_private_env, set_public_env, set_safe_public_env } from '../shared-server.js'; -import { options, get_hooks } from '__SERVER__/internal.js'; +import { options, get_hooks, inited, set_inited } from '__SERVER__/internal.js'; import { DEV } from 'esm-env'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; import { prerendering } from '__sveltekit/environment'; @@ -67,7 +67,8 @@ export class Server { try { const module = await get_hooks(); - if (module.init != null) { + if (module.init != null && !inited) { + set_inited(); await module.init(); } diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index b074c01d49c6..3c57fc9904c2 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -650,9 +650,6 @@ test.describe('reroute', () => { }); test.describe('init', () => { - // we need to skip in dev because the vite dev middleware create - // multiple servers and it would be flaky - test.skip(() => process.env.DEV); 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'); From a7a7bb0f1da23d01ac1ee27a1babe5a44de4fa8c Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 15:05:05 +0100 Subject: [PATCH 09/18] fix: remove `init` from client hooks --- packages/kit/src/core/sync/write_client_manifest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index bb4c65cc57ba..e2925c3ec649 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -151,9 +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 || ' : ''}(() => {}), - init: ${universal_hooks_file ? `(universal_hooks.init${client_hooks_file ? ' ?? client_hooks.init' : ''}) || ` : client_hooks_file ? 'client_hooks.init || ' : ''}(() => {}), }; export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}'; From c5f46c633579e1c0c7079953f45ddcc960ff3c4f Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 15:30:44 +0100 Subject: [PATCH 10/18] docs: write some documentation, use proper exported types --- documentation/docs/30-advanced/20-hooks.md | 30 ++++++++++++++++++++++ packages/kit/src/exports/public.d.ts | 10 ++++++++ packages/kit/src/types/internal.d.ts | 8 +++--- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 6decc1cbf463..6cb200bdff41 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -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). diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 84a3f041c322..24d37e9147e9 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 once as soon as the server is executed. + */ +export type ServerInit = () => MaybePromise; + +/** + * 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; + /** * 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/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 9e24539c93dc..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,13 +111,13 @@ export interface ServerHooks { handle: Handle; handleError: HandleServerError; reroute: Reroute; - init?: () => void | Promise; + init?: ServerInit; } export interface ClientHooks { handleError: HandleClientError; reroute: Reroute; - init?: () => void | Promise; + init?: ClientInit; } export interface Env { From 7e5e75e97449f2d52ea43c04070265196e25d222 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Wed, 4 Dec 2024 16:48:16 +0100 Subject: [PATCH 11/18] chore: regenerate types --- packages/kit/types/index.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 63fdcb004bf3..2991f44c8692 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 once as soon as the server is executed. + */ + export type ServerInit = () => MaybePromise; + + /** + * 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; + /** * 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 From feb3b7c082c35b9d52c14d3ec7d58dcce4882c15 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 9 Dec 2024 16:29:03 -0500 Subject: [PATCH 12/18] simplify --- packages/kit/src/core/sync/write_server.js | 6 ------ packages/kit/src/runtime/server/ambient.d.ts | 3 --- packages/kit/src/runtime/server/index.js | 11 +++++------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 5d17b2809319..eb50bfd4735b 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -70,12 +70,6 @@ export async function get_hooks() { }; } -export let inited = false; - -export function set_inited(){ - inited = true; -} - export { set_assets, set_building, set_manifest, set_prerendering, set_private_env, set_public_env, set_read_implementation, set_safe_public_env }; `; diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index d9cf9f46e59a..a1f2d2c65d29 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -1,6 +1,3 @@ declare module '__SERVER__/internal.js' { export const options: import('types').SSROptions; export const get_hooks: () => Promise>; - export let inited: boolean; - export let set_inited: () => void; -} diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index cb3db2e17917..44324b881b25 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,6 +1,6 @@ import { respond } from './respond.js'; import { set_private_env, set_public_env, set_safe_public_env } from '../shared-server.js'; -import { options, get_hooks, inited, set_inited } from '__SERVER__/internal.js'; +import { options, get_hooks } from '__SERVER__/internal.js'; import { DEV } from 'esm-env'; import { filter_private_env, filter_public_env } from '../../utils/env.js'; import { prerendering } from '__sveltekit/environment'; @@ -67,17 +67,16 @@ export class Server { try { const module = await get_hooks(); - if (module.init != null && !inited) { - set_inited(); - await module.init(); - } - this.#options.hooks = { handle: module.handle || (({ event, resolve }) => resolve(event)), handleError: module.handleError || (({ error }) => console.error(error)), handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)), reroute: module.reroute || (() => {}) }; + + if (module.init) { + await module.init(); + } } catch (error) { if (DEV) { this.#options.hooks = { From c2adc541efb92dd386834b959150f8ddbe3a2870 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 23:08:53 +0100 Subject: [PATCH 13/18] chore: docs suggestions from review Co-authored-by: Rich Harris --- documentation/docs/30-advanced/20-hooks.md | 23 ++++++---------------- packages/kit/src/exports/public.d.ts | 4 ++-- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index 6cb200bdff41..9663d724fc18 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -239,33 +239,22 @@ During development, if an error occurs because of a syntax error in your Svelte ### 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. +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. -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. +> [!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 - -let db; +import * as db from '$lib/server/database'; /** @type {import('@sveltejs/kit').ServerInit} */ export async function init() { - db = await client.connect(); + await db.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(); -} -``` +> [!NOTE] +> In the browser, asynchronous work in `init` will delay hydration, so be mindful of what you put in there. ## Universal hooks diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 24d37e9147e9..be184575530f 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -725,12 +725,12 @@ export type HandleFetch = (input: { }) => MaybePromise; /** - * The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once as soon as the server is executed. + * 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 as the client side app is started. + * 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; From 4cd7488214d1a3dd79b79f8955b54fa3083a7633 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 9 Dec 2024 18:07:45 -0500 Subject: [PATCH 14/18] try this --- packages/kit/src/runtime/server/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 44324b881b25..1d1b148dd736 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,7 @@ export class Server { set_read_implementation(read); } - if (!this.#options.hooks) { + await (init_promise ??= (async () => { try { const module = await get_hooks(); @@ -91,7 +94,7 @@ export class Server { throw error; } } - } + })()); } /** From 917d71b7018c6a5ab78a396edefa8a1c3bb481b4 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 10 Dec 2024 01:45:53 +0100 Subject: [PATCH 15/18] chore: fix lint --- packages/kit/src/runtime/server/ambient.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kit/src/runtime/server/ambient.d.ts b/packages/kit/src/runtime/server/ambient.d.ts index a1f2d2c65d29..2f38261123dc 100644 --- a/packages/kit/src/runtime/server/ambient.d.ts +++ b/packages/kit/src/runtime/server/ambient.d.ts @@ -1,3 +1,4 @@ declare module '__SERVER__/internal.js' { export const options: import('types').SSROptions; export const get_hooks: () => Promise>; +} From 43e26234b213dd844740381b9269ab0861d35f20 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 10 Dec 2024 01:56:47 +0100 Subject: [PATCH 16/18] chore: fix types --- packages/kit/types/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 2991f44c8692..748a6a864b3e 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -707,12 +707,12 @@ declare module '@sveltejs/kit' { }) => MaybePromise; /** - * The [`init`](https://svelte.dev/docs/kit/hooks#Shared-hooks-init) will be invoked once as soon as the server is executed. + * 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 as the client side app is started. + * 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; From bacfbe7f45aa77f668c74e9c841eb3ab3166dcb5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 9 Dec 2024 21:48:29 -0500 Subject: [PATCH 17/18] save a couple bytes --- packages/kit/src/runtime/client/client.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index e287cf1cb7c3..820e0567c0fa 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -266,9 +266,9 @@ export async function start(_app, _target, hydrate) { } app = _app; - if (app.hooks.init != null) { - await app.hooks.init(); - } + + await _app.hooks.init?.(); + routes = parse(_app); container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement; target = _target; From a9c85b0ee6d46ada1d8e52c2b2003793fd6492c0 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:51:52 +0100 Subject: [PATCH 18/18] Apply suggestions from code review --- packages/kit/src/core/sync/write_client_manifest.js | 2 +- packages/kit/src/runtime/server/index.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index e2925c3ec649..fe6127de934c 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -153,7 +153,7 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { }(({ 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'}'; diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 1d1b148dd736..5aa10ac560f5 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -66,6 +66,8 @@ export class Server { set_read_implementation(read); } + // 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();