From bd66c78036903371062fe9d3036ebc1c9c4b984b Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 11:36:59 +0200 Subject: [PATCH 1/8] feat: make handleFetch a shared hook --- .../src/core/sync/write_client_manifest.js | 3 + packages/kit/src/exports/public.d.ts | 10 +- packages/kit/src/runtime/client/client.js | 3 +- packages/kit/src/runtime/client/fetcher.js | 137 +++++++++++------- packages/kit/src/types/internal.d.ts | 2 + packages/kit/types/index.d.ts | 10 +- 6 files changed, 110 insertions(+), 55 deletions(-) diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index c27c61add43c..6c7f6ead2d13 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -162,6 +162,9 @@ export function write_client_manifest(kit, manifest_data, output, metadata) { export const dictionary = ${dictionary}; export const hooks = { + handleFetch: ${ + client_hooks_file ? 'client_hooks.handleFetch || ' : '' + }(({ request, fetch }) => fetch(request)), handleError: ${ client_hooks_file ? 'client_hooks.handleError || ' : '' }(({ error }) => { console.error(error) }), diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 66374dd5fdd1..49d4b60d985d 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -790,7 +790,7 @@ export type HandleClientError = (input: { }) => MaybePromise; /** - * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Server-hooks-handleFetch) hook allows you to modify (or replace) the result of an [`event.fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`. + * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleFetch) hook allows you to modify (or replace) the result of an [`event.fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`. */ export type HandleFetch = (input: { event: RequestEvent; @@ -798,6 +798,14 @@ export type HandleFetch = (input: { fetch: typeof fetch; }) => MaybePromise; +/** + * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleFetch) hook allows you to modify (or replace) a `fetch` request that happens inside a `load` function that runs on the client + */ +export type HandleClientFetch = (input: { + request: Request; + 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 * @since 2.10.0 diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index f831afa5db34..ba65882a7ac4 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -7,7 +7,7 @@ import { make_trackable, normalize_path } from '../../utils/url.js'; -import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js'; +import { create_fetch, dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js'; import { parse, parse_server_route } from './parse.js'; import * as storage from './session-storage.js'; import { @@ -276,6 +276,7 @@ export async function start(_app, _target, hydrate) { } app = _app; + create_fetch(_app); await _app.hooks.init?.(); diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 6a84221f54ee..407a49c6a72d 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -15,68 +15,87 @@ export function unlock_fetch() { loading -= 1; } -if (DEV && BROWSER) { - let can_inspect_stack_trace = false; - - // detect whether async stack traces work - // eslint-disable-next-line @typescript-eslint/require-await - const check_stack_trace = async () => { - const stack = /** @type {string} */ (new Error().stack); - can_inspect_stack_trace = stack.includes('check_stack_trace'); - }; - - void check_stack_trace(); - +/** + * @param {import('./types.js').SvelteKitApp} app + */ +export function create_fetch(app) { /** - * @param {RequestInfo | URL} input - * @param {RequestInit & Record | undefined} init + * @type {typeof fetch} */ - window.fetch = (input, init) => { - // Check if fetch was called via load_node. the lock method only checks if it was called at the - // same time, but not necessarily if it was called from `load`. - // We use just the filename as the method name sometimes does not appear on the CI. - const url = input instanceof Request ? input.url : input.toString(); - const stack_array = /** @type {string} */ (new Error().stack).split('\n'); - // We need to do a cutoff because Safari and Firefox maintain the stack - // across events and for example traces a `fetch` call triggered from a button - // back to the creation of the event listener and the element creation itself, - // where at some point client.js will show up, leading to false positives. - const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load')); - 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') - : loading; - - // This flag is set in initial_fetch and subsequent_fetch - const used_kit_fetch = init?.__sveltekit_fetch__; - - if (in_load_heuristic && !used_kit_fetch) { - console.warn( - `Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://svelte.dev/docs/kit/load#making-fetch-requests` - ); - } + let runtime_fetch; + if (DEV && BROWSER) { + let can_inspect_stack_trace = false; + + // detect whether async stack traces work + // eslint-disable-next-line @typescript-eslint/require-await + const check_stack_trace = async () => { + const stack = /** @type {string} */ (new Error().stack); + can_inspect_stack_trace = stack.includes('check_stack_trace'); + }; + + void check_stack_trace(); + + /** + * @param {RequestInfo | URL} input + * @param {RequestInit & Record | undefined} init + */ + runtime_fetch = (input, init) => { + // Check if fetch was called via load_node. the lock method only checks if it was called at the + // same time, but not necessarily if it was called from `load`. + // We use just the filename as the method name sometimes does not appear on the CI. + const url = input instanceof Request ? input.url : input.toString(); + const stack_array = /** @type {string} */ (new Error().stack).split('\n'); + // We need to do a cutoff because Safari and Firefox maintain the stack + // across events and for example traces a `fetch` call triggered from a button + // back to the creation of the event listener and the element creation itself, + // where at some point client.js will show up, leading to false positives. + const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load')); + 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') + : loading; + + // This flag is set in initial_fetch and subsequent_fetch + const used_kit_fetch = init?.__sveltekit_fetch__; + + if (in_load_heuristic && !used_kit_fetch) { + console.warn( + `Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://svelte.dev/docs/kit/load#making-fetch-requests` + ); + } - const method = input instanceof Request ? input.method : init?.method || 'GET'; + const method = input instanceof Request ? input.method : init?.method || 'GET'; - if (method !== 'GET') { - cache.delete(build_selector(input)); - } + if (method !== 'GET') { + cache.delete(build_selector(input)); + } - return native_fetch(input, init); - }; -} else if (BROWSER) { - window.fetch = (input, init) => { - const method = input instanceof Request ? input.method : init?.method || 'GET'; + return native_fetch(input, init); + }; + } else if (BROWSER) { + runtime_fetch = (input, init) => { + const method = input instanceof Request ? input.method : init?.method || 'GET'; - if (method !== 'GET') { - cache.delete(build_selector(input)); - } + if (method !== 'GET') { + cache.delete(build_selector(input)); + } - return native_fetch(input, init); + return native_fetch(input, init); + }; + } + + window.fetch = async (input, init) => { + const original_request = normalize_fetch_input(input, init); + + return app.hooks.handleFetch({ + request: original_request, + fetch: runtime_fetch, + }); }; } + const cache = new Map(); /** @@ -175,3 +194,17 @@ function build_selector(resource, opts) { return selector; } + + +/** + * @param {RequestInfo | URL} info + * @param {RequestInit | undefined} init + * @returns {Request} + */ +function normalize_fetch_input(info, init) { + if (info instanceof Request) { + return info; + } + + return new Request(typeof info === 'string' ? new URL(info) : info, init); +} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 17e2425e3c17..bf6a2f523baf 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -12,6 +12,7 @@ import { ServerInitOptions, HandleFetch, Actions, + HandleClientFetch, HandleClientError, Reroute, RequestEvent, @@ -155,6 +156,7 @@ export interface ServerHooks { } export interface ClientHooks { + handleFetch: HandleClientFetch; handleError: HandleClientError; reroute: Reroute; transport: Record; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 300a6f726ebb..807b948591e7 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -772,7 +772,7 @@ declare module '@sveltejs/kit' { }) => MaybePromise; /** - * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Server-hooks-handleFetch) hook allows you to modify (or replace) the result of an [`event.fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`. + * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleFetch) hook allows you to modify (or replace) the result of an [`event.fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`. */ export type HandleFetch = (input: { event: RequestEvent; @@ -780,6 +780,14 @@ declare module '@sveltejs/kit' { fetch: typeof fetch; }) => MaybePromise; + /** + * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleFetch) hook allows you to modify (or replace) a `fetch` request that happens on the client + */ + export type HandleClientFetch = (input: { + request: Request; + 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 * @since 2.10.0 From 98d5e16a1ed45a87039f0a4e36c505d1a68ee243 Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 11:37:11 +0200 Subject: [PATCH 2/8] docs: add doc for client-side handleFetch --- documentation/docs/30-advanced/20-hooks.md | 34 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index fffd0f1aea2e..e0a95cf848d0 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -104,8 +104,14 @@ export async function handle({ event, resolve }) { Note that `resolve(...)` will never throw an error, it will always return a `Promise` with the appropriate status code. If an error is thrown elsewhere during `handle`, it is treated as fatal, and SvelteKit will respond with a JSON representation of the error or a fallback error page — which can be customised via `src/error.html` — depending on the `Accept` header. You can read more about error handling [here](errors). +## Shared hooks + +The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`: + ### handleFetch +#### on the server + This function allows you to modify (or replace) the result of an [`event.fetch`](load#Making-fetch-requests) call that runs on the server (or during prerendering) inside an endpoint, `load`, `action`, `handle`, `handleError` or `reroute`. For example, your `load` function might make a request to a public URL like `https://api.yourapp.com` when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet). @@ -143,9 +149,33 @@ export async function handleFetch({ event, request, fetch }) { } ``` -## Shared hooks +#### on the client + +This function allows you to modify (or replace) `fetch` requests that happens on the client. + +This allows, for example, to pass custom headers to server when running `load` or `action` function on the server *(inside a `+page.server.ts` or `+layout.server.ts`)*, to automatically includes credentials to requests to your API or to collect logs or metrics. + +*Note: on the client, the `event` argument is not passed to the hook.* + + +```js +/// file: src/hooks.client.js +/** @type {import('@sveltejs/kit').HandleClientFetch} */ +export async function handleFetch({ request, fetch }) { + if (request.url.startsWith(location.origin)) { + request.headers.set('X-Auth-Token', 'my-custom-auth-token'); + } else if (request.url.startsWith('https://api.my-domain.com/')) { + request.headers.set('Authorization', 'Bearer my-api-token'); + } + + console.time(`request: ${request.url}`); + + return fetch(request).finally(() => { + console.timeEnd(`request: ${request.url}`); + }); +} +``` -The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`: ### handleError From a630a3d9f12b73d325bbf637b598c16158631930 Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 11:37:32 +0200 Subject: [PATCH 3/8] chore: add a test on the basic playground --- playgrounds/basic/src/hooks.client.ts | 10 ++++++++++ playgrounds/basic/src/routes/+layout.svelte | 1 + .../basic/src/routes/client-fetch/+page.server.ts | 10 ++++++++++ playgrounds/basic/src/routes/client-fetch/+page.svelte | 6 ++++++ 4 files changed, 27 insertions(+) create mode 100644 playgrounds/basic/src/hooks.client.ts create mode 100644 playgrounds/basic/src/routes/client-fetch/+page.server.ts create mode 100644 playgrounds/basic/src/routes/client-fetch/+page.svelte diff --git a/playgrounds/basic/src/hooks.client.ts b/playgrounds/basic/src/hooks.client.ts new file mode 100644 index 000000000000..8509de00bb8b --- /dev/null +++ b/playgrounds/basic/src/hooks.client.ts @@ -0,0 +1,10 @@ +import type { HandleClientFetch } from '@sveltejs/kit'; + +export const handleFetch: HandleClientFetch = async ({ request, fetch }) => { + // You can modify the request here if needed + if (request.url.startsWith(location.origin)) { + request.headers.set('X-Client-Header', 'imtheclient'); + } + + return await fetch(request); +}; diff --git a/playgrounds/basic/src/routes/+layout.svelte b/playgrounds/basic/src/routes/+layout.svelte index 707b3afe5a1e..6888710c6468 100644 --- a/playgrounds/basic/src/routes/+layout.svelte +++ b/playgrounds/basic/src/routes/+layout.svelte @@ -13,6 +13,7 @@ /c /d /e + /client-fetch {@render children()} diff --git a/playgrounds/basic/src/routes/client-fetch/+page.server.ts b/playgrounds/basic/src/routes/client-fetch/+page.server.ts new file mode 100644 index 000000000000..8695a3a89d22 --- /dev/null +++ b/playgrounds/basic/src/routes/client-fetch/+page.server.ts @@ -0,0 +1,10 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = ({ request }) => { + const client_header = request.headers.get('x-client-header'); + console.log('client header:', client_header); + + return { + header: client_header + }; +}; diff --git a/playgrounds/basic/src/routes/client-fetch/+page.svelte b/playgrounds/basic/src/routes/client-fetch/+page.svelte new file mode 100644 index 000000000000..35bc297797c8 --- /dev/null +++ b/playgrounds/basic/src/routes/client-fetch/+page.svelte @@ -0,0 +1,6 @@ + + +

If you navigate from client-side, you should have an header set by handleFetch:

+
header: {data.header}
From d8833c24e53330b4b86c13380d8a1fbd8453520a Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 12:01:08 +0200 Subject: [PATCH 4/8] chore: add changeset --- .changeset/two-dolphins-fry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-dolphins-fry.md diff --git a/.changeset/two-dolphins-fry.md b/.changeset/two-dolphins-fry.md new file mode 100644 index 000000000000..b609ed5e2bda --- /dev/null +++ b/.changeset/two-dolphins-fry.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: make handleFetch a shared hook From 7bf367b4c6911379ca8fd1f0f1e0f46edf85c1ff Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 12:05:53 +0200 Subject: [PATCH 5/8] style: apply prettier --- packages/kit/src/runtime/client/client.js | 9 ++++++++- packages/kit/src/runtime/client/fetcher.js | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index ba65882a7ac4..101d47c6df07 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -7,7 +7,14 @@ import { make_trackable, normalize_path } from '../../utils/url.js'; -import { create_fetch, dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js'; +import { + create_fetch, + dev_fetch, + initial_fetch, + lock_fetch, + subsequent_fetch, + unlock_fetch +} from './fetcher.js'; import { parse, parse_server_route } from './parse.js'; import * as storage from './session-storage.js'; import { diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 407a49c6a72d..127da1ee51d2 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -90,12 +90,11 @@ export function create_fetch(app) { return app.hooks.handleFetch({ request: original_request, - fetch: runtime_fetch, + fetch: runtime_fetch }); }; } - const cache = new Map(); /** @@ -195,7 +194,6 @@ function build_selector(resource, opts) { return selector; } - /** * @param {RequestInfo | URL} info * @param {RequestInit | undefined} init From b26d7937c5b964a9f40eab7d91c39e3ccb8790d0 Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 14:19:09 +0200 Subject: [PATCH 6/8] fix: ensure url of Request is correctly handled --- packages/kit/src/runtime/client/fetcher.js | 36 ++++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 127da1ee51d2..c46324eeee31 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -85,14 +85,16 @@ export function create_fetch(app) { }; } - window.fetch = async (input, init) => { - const original_request = normalize_fetch_input(input, init); - - return app.hooks.handleFetch({ - request: original_request, - fetch: runtime_fetch - }); - }; + if (BROWSER) { + window.fetch = async (input, init) => { + const original_request = normalize_fetch_input(input, init); + + return app.hooks.handleFetch({ + request: original_request, + fetch: runtime_fetch + }); + }; + } } const cache = new Map(); @@ -172,7 +174,7 @@ export function dev_fetch(resource, opts) { * @param {RequestInit} [opts] */ function build_selector(resource, opts) { - const url = JSON.stringify(resource instanceof Request ? resource.url : resource); + const url = get_selector_url(resource); let selector = `script[data-sveltekit-fetched][data-url=${url}]`; @@ -194,6 +196,20 @@ function build_selector(resource, opts) { return selector; } +/** + * Build the cache url for a given request + * @param {URL | RequestInfo} resource + */ +function get_selector_url(resource) { + if (resource instanceof Request) { + resource = resource.url.startsWith(location.origin) + ? resource.url.slice(location.origin.length) + : resource.url; + } + + return JSON.stringify(resource); +} + /** * @param {RequestInfo | URL} info * @param {RequestInit | undefined} init @@ -204,5 +220,5 @@ function normalize_fetch_input(info, init) { return info; } - return new Request(typeof info === 'string' ? new URL(info) : info, init); + return new Request(typeof info === 'string' ? new URL(info, location.href) : info, init); } From 7370c2b96e522716ca639fcccf4c65b362833c74 Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 30 Apr 2025 14:44:58 +0200 Subject: [PATCH 7/8] test: add tests for client-side handleFetch --- .../kit/test/apps/client-fetch/.gitignore | 2 + .../kit/test/apps/client-fetch/package.json | 25 +++++++++++++ .../apps/client-fetch/playwright.config.js | 1 + .../kit/test/apps/client-fetch/src/app.html | 12 ++++++ .../apps/client-fetch/src/hooks.client.js | 7 ++++ .../client-fetch/src/routes/+layout.svelte | 7 ++++ .../apps/client-fetch/src/routes/+page.svelte | 1 + .../client-fetch/src/routes/api/+server.js | 7 ++++ .../src/routes/fetch/+page.svelte | 13 +++++++ .../src/routes/load/+page.server.js | 4 ++ .../client-fetch/src/routes/load/+page.svelte | 5 +++ .../test/apps/client-fetch/svelte.config.js | 4 ++ .../kit/test/apps/client-fetch/test/test.js | 37 +++++++++++++++++++ .../kit/test/apps/client-fetch/tsconfig.json | 10 +++++ .../kit/test/apps/client-fetch/vite.config.js | 18 +++++++++ pnpm-lock.yaml | 24 ++++++++++++ 16 files changed, 177 insertions(+) create mode 100644 packages/kit/test/apps/client-fetch/.gitignore create mode 100644 packages/kit/test/apps/client-fetch/package.json create mode 100644 packages/kit/test/apps/client-fetch/playwright.config.js create mode 100644 packages/kit/test/apps/client-fetch/src/app.html create mode 100644 packages/kit/test/apps/client-fetch/src/hooks.client.js create mode 100644 packages/kit/test/apps/client-fetch/src/routes/+layout.svelte create mode 100644 packages/kit/test/apps/client-fetch/src/routes/+page.svelte create mode 100644 packages/kit/test/apps/client-fetch/src/routes/api/+server.js create mode 100644 packages/kit/test/apps/client-fetch/src/routes/fetch/+page.svelte create mode 100644 packages/kit/test/apps/client-fetch/src/routes/load/+page.server.js create mode 100644 packages/kit/test/apps/client-fetch/src/routes/load/+page.svelte create mode 100644 packages/kit/test/apps/client-fetch/svelte.config.js create mode 100644 packages/kit/test/apps/client-fetch/test/test.js create mode 100644 packages/kit/test/apps/client-fetch/tsconfig.json create mode 100644 packages/kit/test/apps/client-fetch/vite.config.js diff --git a/packages/kit/test/apps/client-fetch/.gitignore b/packages/kit/test/apps/client-fetch/.gitignore new file mode 100644 index 000000000000..216b07aa0731 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/.gitignore @@ -0,0 +1,2 @@ +.custom-out-dir +!.env \ No newline at end of file diff --git a/packages/kit/test/apps/client-fetch/package.json b/packages/kit/test/apps/client-fetch/package.json new file mode 100644 index 000000000000..63792c626533 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/package.json @@ -0,0 +1,25 @@ +{ + "name": "test-embed", + "private": true, + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync", + "check": "svelte-kit sync && tsc && svelte-check", + "test": "pnpm test:dev && pnpm test:build", + "test:dev": "cross-env DEV=true playwright test", + "test:build": "playwright test" + }, + "devDependencies": { + "@sveltejs/kit": "workspace:^", + "@sveltejs/vite-plugin-svelte": "^5.0.1", + "cross-env": "^7.0.3", + "svelte": "^5.23.1", + "svelte-check": "^4.1.1", + "typescript": "^5.5.4", + "vite": "^6.2.6" + }, + "type": "module" +} diff --git a/packages/kit/test/apps/client-fetch/playwright.config.js b/packages/kit/test/apps/client-fetch/playwright.config.js new file mode 100644 index 000000000000..33d36b651014 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/playwright.config.js @@ -0,0 +1 @@ +export { config as default } from '../../utils.js'; diff --git a/packages/kit/test/apps/client-fetch/src/app.html b/packages/kit/test/apps/client-fetch/src/app.html new file mode 100644 index 000000000000..79d946ed86a3 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/kit/test/apps/client-fetch/src/hooks.client.js b/packages/kit/test/apps/client-fetch/src/hooks.client.js new file mode 100644 index 000000000000..5d11f9b89706 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/hooks.client.js @@ -0,0 +1,7 @@ +export const handleFetch = async ({ request, fetch }) => { + if (request.url.startsWith(location.origin)) { + request.headers.set('X-Client-Header', 'imtheclient'); + } + + return await fetch(request); +}; diff --git a/packages/kit/test/apps/client-fetch/src/routes/+layout.svelte b/packages/kit/test/apps/client-fetch/src/routes/+layout.svelte new file mode 100644 index 000000000000..5e1f1fed86c2 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/routes/+layout.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/kit/test/apps/client-fetch/src/routes/+page.svelte b/packages/kit/test/apps/client-fetch/src/routes/+page.svelte new file mode 100644 index 000000000000..5aea2b8b1259 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/routes/+page.svelte @@ -0,0 +1 @@ +load diff --git a/packages/kit/test/apps/client-fetch/src/routes/api/+server.js b/packages/kit/test/apps/client-fetch/src/routes/api/+server.js new file mode 100644 index 000000000000..b52a5569ede1 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/routes/api/+server.js @@ -0,0 +1,7 @@ +import { json } from '@sveltejs/kit'; + +export function GET({ request }) { + const header = request.headers.get('x-client-header') ?? 'empty'; + + return json({ header }); +} diff --git a/packages/kit/test/apps/client-fetch/src/routes/fetch/+page.svelte b/packages/kit/test/apps/client-fetch/src/routes/fetch/+page.svelte new file mode 100644 index 000000000000..ea9e869d9b56 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/routes/fetch/+page.svelte @@ -0,0 +1,13 @@ + + +
{header}
diff --git a/packages/kit/test/apps/client-fetch/src/routes/load/+page.server.js b/packages/kit/test/apps/client-fetch/src/routes/load/+page.server.js new file mode 100644 index 000000000000..0d6a3f274a13 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/routes/load/+page.server.js @@ -0,0 +1,4 @@ +export const load = ({ request }) => { + const header = request.headers.get('x-client-header') ?? 'empty'; + return { header }; +}; diff --git a/packages/kit/test/apps/client-fetch/src/routes/load/+page.svelte b/packages/kit/test/apps/client-fetch/src/routes/load/+page.svelte new file mode 100644 index 000000000000..03ad366c5325 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/src/routes/load/+page.svelte @@ -0,0 +1,5 @@ + + +
{data.header}
diff --git a/packages/kit/test/apps/client-fetch/svelte.config.js b/packages/kit/test/apps/client-fetch/svelte.config.js new file mode 100644 index 000000000000..bded48544036 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/svelte.config.js @@ -0,0 +1,4 @@ +/** @type {import('@sveltejs/kit').Config} */ +const config = {}; + +export default config; diff --git a/packages/kit/test/apps/client-fetch/test/test.js b/packages/kit/test/apps/client-fetch/test/test.js new file mode 100644 index 000000000000..43becb35d84c --- /dev/null +++ b/packages/kit/test/apps/client-fetch/test/test.js @@ -0,0 +1,37 @@ +import { expect } from '@playwright/test'; +import { test } from '../../../utils.js'; + +/** @typedef {import('@playwright/test').Response} Response */ + +test.describe.configure({ mode: 'parallel' }); + +test.describe('client-fetch', () => { + test('should use client handleFetch for client-side load requests', async ({ + page, + javaScriptEnabled + }) => { + await page.goto('/'); + await page.click('.navigate-to-load'); + + if (javaScriptEnabled) { + await expect(page.getByTestId('header')).toHaveText('imtheclient'); + } else { + await expect(page.getByTestId('header')).toHaveText('empty'); + } + }); + + test('should not use client handleFetch for server-side load requests', async ({ page }) => { + await page.goto('/load'); + await expect(page.getByTestId('header')).toHaveText('empty'); + }); + + test('should use client handleFetch for fetch requests', async ({ page, javaScriptEnabled }) => { + await page.goto('/fetch'); + + if (javaScriptEnabled) { + await expect(page.getByTestId('header')).toHaveText('imtheclient'); + } else { + await expect(page.getByTestId('header')).toHaveText('loading'); + } + }); +}); diff --git a/packages/kit/test/apps/client-fetch/tsconfig.json b/packages/kit/test/apps/client-fetch/tsconfig.json new file mode 100644 index 000000000000..1d665886266b --- /dev/null +++ b/packages/kit/test/apps/client-fetch/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "noEmit": true, + "resolveJsonModule": true + }, + "extends": "./.svelte-kit/tsconfig.json" +} diff --git a/packages/kit/test/apps/client-fetch/vite.config.js b/packages/kit/test/apps/client-fetch/vite.config.js new file mode 100644 index 000000000000..69200cdb7cd8 --- /dev/null +++ b/packages/kit/test/apps/client-fetch/vite.config.js @@ -0,0 +1,18 @@ +import * as path from 'node:path'; +import { sveltekit } from '@sveltejs/kit/vite'; + +/** @type {import('vite').UserConfig} */ +const config = { + build: { + minify: false + }, + clearScreen: false, + plugins: [sveltekit()], + server: { + fs: { + allow: [path.resolve('../../../src')] + } + } +}; + +export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cfbfe6d8d90..b87f1e9200e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -489,6 +489,30 @@ importers: specifier: ^6.2.7 version: 6.2.7(@types/node@18.19.50)(lightningcss@1.24.1) + packages/kit/test/apps/client-fetch: + devDependencies: + '@sveltejs/kit': + specifier: workspace:^ + version: link:../../.. + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.1 + version: 5.0.1(svelte@5.23.1)(vite@6.2.6(@types/node@18.19.50)(lightningcss@1.24.1)) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + svelte: + specifier: ^5.23.1 + version: 5.23.1 + svelte-check: + specifier: ^4.1.1 + version: 4.1.1(picomatch@4.0.2)(svelte@5.23.1)(typescript@5.6.3) + typescript: + specifier: ^5.5.4 + version: 5.6.3 + vite: + specifier: ^6.2.6 + version: 6.2.6(@types/node@18.19.50)(lightningcss@1.24.1) + packages/kit/test/apps/dev-only: devDependencies: '@sveltejs/kit': From 767c555842ac14f45d2305d3c6a19cc04c9a0f3c Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Wed, 7 May 2025 12:18:56 +0200 Subject: [PATCH 8/8] chore: update pnpm-lock after rebase --- packages/kit/types/index.d.ts | 2 +- pnpm-lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 807b948591e7..aeeeb0724fed 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -781,7 +781,7 @@ declare module '@sveltejs/kit' { }) => MaybePromise; /** - * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleFetch) hook allows you to modify (or replace) a `fetch` request that happens on the client + * The [`handleFetch`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleFetch) hook allows you to modify (or replace) a `fetch` request that happens inside a `load` function that runs on the client */ export type HandleClientFetch = (input: { request: Request; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b87f1e9200e8..b9ad9f80ff4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -496,7 +496,7 @@ importers: version: link:../../.. '@sveltejs/vite-plugin-svelte': specifier: ^5.0.1 - version: 5.0.1(svelte@5.23.1)(vite@6.2.6(@types/node@18.19.50)(lightningcss@1.24.1)) + version: 5.0.3(svelte@5.23.1)(vite@6.2.7(@types/node@18.19.50)(lightningcss@1.24.1)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -511,7 +511,7 @@ importers: version: 5.6.3 vite: specifier: ^6.2.6 - version: 6.2.6(@types/node@18.19.50)(lightningcss@1.24.1) + version: 6.2.7(@types/node@18.19.50)(lightningcss@1.24.1) packages/kit/test/apps/dev-only: devDependencies: