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 diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index f924b83ad075..276df8eca64c 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -104,9 +104,15 @@ 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 -This function allows you to modify (or replace) a `fetch` request that happens inside a `load`, `action` or `handle` function that runs on the server (or during prerendering). +#### on the server + +This function allows you to modify (or replace) a `fetch` request that happens inside a `load`, `action` or `handle` function that runs on the server (or during pre-rendering). 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). @@ -147,9 +153,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 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 684f18981610..9a0630818c1a 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) a `fetch` request that happens inside a `load` function that runs on the server (or during prerendering). + * 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 server (or during pre-rendering). */ 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..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 { 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 +283,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..c46324eeee31 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -15,66 +15,86 @@ 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); + }; + } + + if (BROWSER) { + window.fetch = async (input, init) => { + const original_request = normalize_fetch_input(input, init); - return native_fetch(input, init); - }; + return app.hooks.handleFetch({ + request: original_request, + fetch: runtime_fetch + }); + }; + } } const cache = new Map(); @@ -154,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}]`; @@ -175,3 +195,30 @@ 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 + * @returns {Request} + */ +function normalize_fetch_input(info, init) { + if (info instanceof Request) { + return info; + } + + return new Request(typeof info === 'string' ? new URL(info, location.href) : info, init); +} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 2d54b37ac145..ebcd785f0180 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/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/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 4dfb901cda11..f39b82b08732 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) a `fetch` request that happens inside a `load` function that runs on the server (or during prerendering). + * 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 server (or during pre-rendering). */ 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 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/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}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22586d107e08..868f06e2bbb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -490,6 +490,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.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 + 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.7(@types/node@18.19.50)(lightningcss@1.24.1) + packages/kit/test/apps/dev-only: devDependencies: '@sveltejs/kit':