From b5560b1d6061bb6d46980fbc55fa2759b5c3d3f8 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 29 Jul 2026 17:19:16 -0600 Subject: [PATCH 1/7] breaking: change `preloadCode(pathname)` to `preloadCode(routeId)`, make `match` pre-resolve loaders --- .changeset/tidy-owls-preload.md | 5 + packages/kit/src/runtime/client/client.js | 152 ++++++++++++++---- packages/kit/src/runtime/pathname.js | 37 +++++ .../kit/src/runtime/server/page/render.js | 21 ++- .../src/runtime/server/page/server_routing.js | 19 +++ packages/kit/src/runtime/server/respond.js | 19 ++- packages/kit/test/ambient.d.ts | 3 +- .../basics/test/cross-platform/client.test.js | 75 ++++++++- packages/kit/test/apps/options/test/test.js | 3 +- packages/kit/test/setup.js | 2 + packages/kit/test/types.d.ts | 3 +- packages/kit/test/utils.js | 6 +- packages/kit/types/index.d.ts | 16 +- 13 files changed, 319 insertions(+), 42 deletions(-) create mode 100644 .changeset/tidy-owls-preload.md diff --git a/.changeset/tidy-owls-preload.md b/.changeset/tidy-owls-preload.md new file mode 100644 index 000000000000..c3a8fc8848e1 --- /dev/null +++ b/.changeset/tidy-owls-preload.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: `preloadCode` now takes a route ID (e.g. `/blog/[slug]`) instead of a pathname. Route IDs are not prefixed with `paths.base` diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 5143ad962d2d..152efa483da2 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -21,7 +21,7 @@ import { scroll_state, load_css } from './utils.js'; -import { base, set_match_implementation } from '$app/paths/internal/client'; +import { base, app_dir, set_match_implementation } from '$app/paths/internal/client'; import * as devalue from 'devalue'; import { HISTORY_INFO_KEY, @@ -41,7 +41,11 @@ import { import { get_message, get_status } from '../../utils/error.js'; import { page, navigating, updated, notify_version } from './state.svelte.js'; import { payload } from './payload.js'; -import { add_data_suffix, add_resolution_suffix } from '../pathname.js'; +import { + add_data_suffix, + add_resolution_suffix, + route_id_resolution_pathname +} from '../pathname.js'; import { noop_span } from '../telemetry/noop.js'; import { read_ndjson } from './ndjson.js'; import Root from '../components/root.svelte'; @@ -296,6 +300,28 @@ function discard_load_cache() { */ const reroute_cache = new Map(); +/** + * Cache of route ID -> parsed route for server-side route resolution. + * Populated whenever a server route resolution occurs (`match`, link preloading, + * hydration), so that `preloadCode(id)` doesn't need an extra server round trip. + * Lives until full page reload. + * @type {Map} + */ +const route_id_cache = new Map(); + +/** + * Parse a server-provided route and record it in `route_id_cache`, so that a subsequent + * `preloadCode(id)` for the same route doesn't need another server round trip. Always use + * this rather than calling `parse_server_route` directly. + * @param {import('types').CSRRouteServer} server_route + * @returns {import('types').CSRRoute} + */ +function parse_and_cache_server_route(server_route) { + const route = parse_server_route(server_route, app.nodes); + route_id_cache.set(route.id, route); + return route; +} + /** * Note on before_navigate_callbacks, on_navigate_callbacks and after_navigate_callbacks: * do not re-assign as some closures keep references to these Sets @@ -792,6 +818,47 @@ async function _preload_data(intent) { return load_cache.promise; } +/** + * Fetch and parse the route with the given ID from the server-side + * route resolution endpoint. Returns `undefined` if the route doesn't exist. + * Only used when `router.resolution === 'server'`. + * @param {string} id + * @returns {Promise} + */ +async function load_route_by_id(id) { + /** @type {{ route?: import('types').CSRRouteServer }} */ + let module; + + try { + module = await import( + /* @vite-ignore */ + base + route_id_resolution_pathname(app_dir, id) + ); + } catch { + // if there's no module at that path the response is a 404 (or, on a static + // host, fallback HTML with the wrong MIME type) and the import rejects — + // treat it the same as an unknown route rather than surfacing a cryptic error + return; + } + + if (!module.route) return; + + return parse_and_cache_server_route(module.route); +} + +/** + * Import the modules for a route's layout and leaf nodes, without running `load` functions. + * @param {import('types').CSRRoute} route + * @returns {Promise} + */ +async function load_route_nodes(route) { + await Promise.all( + /** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ ( + [...route.layouts, route.leaf].filter(Boolean) + ).map(([, node_loader]) => node_loader()) + ); +} + /** * @param {URL} url * @returns {Promise} @@ -800,11 +867,7 @@ async function _preload_code(url) { const route = (await get_navigation_intent(url, false))?.route; if (route) { - await Promise.all( - /** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ ( - [...route.layouts, route.leaf].filter(Boolean) - ).map((load) => load[1]()) - ); + await load_route_nodes(route); } } @@ -1735,7 +1798,7 @@ export async function get_navigation_intent(url, invalidating) { return { id: get_page_key(url), invalidating, - route: parse_server_route(route, app.nodes), + route: parse_and_cache_server_route(route), params, url }; @@ -2660,45 +2723,72 @@ export async function preloadData(href) { * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. * - * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about/+page.svelte`) or `/blog/*` (to match `src/routes/blog/[slug]/+page.svelte`). + * Takes a route ID such as `/about` or `/blog/[slug]`. Unlike pathnames, route IDs + * are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths). + * If you have a pathname rather than a route ID, you can convert it with + * [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`: + * + * ```js + * import { match } from '$app/paths'; + * import { preloadCode } from '$app/navigation'; + * + * const matched = await match('/blog/hello-world'); + * if (matched) await preloadCode(matched.id); + * ``` * * Unlike `preloadData`, this won't call `load` functions. * Returns a Promise that resolves when the modules have been imported. * - * @param {string} pathname + * @param {RouteId} id * @returns {Promise} */ -export async function preloadCode(pathname) { +export async function preloadCode(id) { if (!BROWSER) { throw new Error('Cannot call preloadCode(...) on the server'); } - // `current.url` is null until the first navigation/hydration completes, so fall back - // to `location` to support calling `preloadCode` during initial page load (#13297) - const url = new URL(pathname, current.url ?? location.href); + if (DEV && id[0] !== '/') { + throw new Error( + `argument passed to preloadCode must be a route ID (i.e. "/blog/[slug]" rather than "blog/[slug]")` + ); + } - if (DEV) { - if (!pathname.startsWith('/')) { - throw new Error( - 'argument passed to preloadCode must be a pathname (i.e. "/about" rather than "http://example.com/about"' - ); - } + /** @type {import('types').CSRRoute | undefined} */ + let route; - if (!pathname.startsWith(base)) { - throw new Error( - `pathname passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")` - ); - } + if (__SVELTEKIT_CLIENT_ROUTING__) { + route = routes.find((r) => r.id === id); + } else { + route = route_id_cache.get(id) ?? (await load_route_by_id(id)); + } - if (__SVELTEKIT_CLIENT_ROUTING__) { - const rerouted = await get_rerouted_url(url); - if (!rerouted || !routes.find((route) => route.exec(get_url_path(rerouted)))) { - throw new Error(`'${pathname}' did not match any routes`); + if (!route) { + if (DEV) { + // TODO: this is a warning rather than an error because `$app/manifest`'s + // `routes` export includes endpoint-only (+server.js) routes, which have no + // code to preload and are indistinguishable from nonexistent ids on the + // client. Revisit if/when `$app/manifest.routes` is filtered or annotated + // (see https://github.com/sveltejs/kit/issues/16511) — at that point unknown + // ids could error again. + let message = `'${id}' did not match any page routes (note that endpoint-only routes have no code to preload)`; + + if (__SVELTEKIT_CLIENT_ROUTING__) { + // the most common migration mistake is passing a pathname, which used to work + const candidates = [id]; + if (base && id.startsWith(base)) candidates.push(id.slice(base.length) || '/'); + + if (candidates.some((path) => routes.some((r) => r.exec(path)))) { + message += `. It does match as a pathname — use \`match(...)\` from \`$app/paths\` to convert a pathname into a route ID`; + } } + + console.warn(message); } + + return; } - return _preload_code(url); + await load_route_nodes(route); } /** @@ -3364,7 +3454,7 @@ async function _hydrate( } else { // undefined in case of 404 if (server_route) { - parsed_route = route = parse_server_route(server_route, app.nodes); + parsed_route = route = parse_and_cache_server_route(server_route); } else { route = { id: null }; params = {}; diff --git a/packages/kit/src/runtime/pathname.js b/packages/kit/src/runtime/pathname.js index 7c2f80ca4642..b4aa07f88e34 100644 --- a/packages/kit/src/runtime/pathname.js +++ b/packages/kit/src/runtime/pathname.js @@ -47,3 +47,40 @@ export function add_resolution_suffix(pathname) { export function strip_resolution_suffix(pathname) { return pathname.slice(0, -ROUTE_SUFFIX.length); } + +const ROUTES_PREFIX = '/routes'; + +/** + * The pathname of the route-ID-keyed resolution module for a given route ID, + * e.g. `/_app/routes/blog/[slug]/__route.js` (before prefixing with `base`). + * @param {string} app_dir + * @param {string} route_id + * @returns {string} + */ +export function route_id_resolution_pathname(app_dir, route_id) { + return add_resolution_suffix(`/${app_dir}${ROUTES_PREFIX}${route_id === '/' ? '' : route_id}`); +} + +/** + * Whether a pathname (with the `/__route.js` suffix already stripped, and `base` NOT yet stripped) + * is a route-ID resolution request rather than a pathname resolution request. + * @param {string} pathname + * @param {string} base + * @param {string} app_dir + * @returns {boolean} + */ +export function is_route_id_resolution_path(pathname, base, app_dir) { + const prefix = `${base}/${app_dir}${ROUTES_PREFIX}`; + return pathname === prefix || pathname.startsWith(prefix + '/'); +} + +/** + * Extract the route ID from a decoded, base-stripped, suffix-stripped pathname, + * e.g. `/_app/routes/blog/[slug]` -> `/blog/[slug]`, `/_app/routes` -> `/`. + * @param {string} pathname + * @param {string} app_dir + * @returns {string} + */ +export function extract_route_id(pathname, app_dir) { + return pathname.slice(`/${app_dir}${ROUTES_PREFIX}`.length) || '/'; +} diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 24598422ec32..2ebffb668ced 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -11,7 +11,11 @@ import { uneval_action_response } from './actions.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import { SCHEME } from '../../../utils/url.js'; import { create_server_routing_response, generate_route_object } from './server_routing.js'; -import { add_data_suffix, add_resolution_suffix } from '../../pathname.js'; +import { + add_data_suffix, + add_resolution_suffix, + route_id_resolution_pathname +} from '../../pathname.js'; import { try_get_request_store, with_request_store } from '@sveltejs/kit/internal/server'; import { text_encoder } from '../../utils.js'; import { count_non_ssi_comments, create_replacer, get_global_name } from '../utils.js'; @@ -391,6 +395,21 @@ export async function render_response({ pathname, create_server_routing_response(route, event.params, new URL(pathname, event.url), client) ); + + // Prerender a route-ID-keyed `/_app/routes//__route.js` module alongside the + // pathname-keyed one above, so that `preloadCode(id)` can resolve a route ID without + // hitting the server. This is required, not merely an optimisation: a fully + // prerendered route is filtered out of `manifest._.routes` (see `generateManifest` + // in core/adapt/builder.js), so on a host with no SvelteKit server there is nothing + // left to answer a resolution request for it at runtime. + if (route) { + const id_pathname = paths.base + route_id_resolution_pathname(paths.app_dir, route.id); + + state.prerendering.dependencies.set( + id_pathname, + create_server_routing_response(route, {}, new URL(id_pathname, event.url), client) + ); + } } const blocks = []; diff --git a/packages/kit/src/runtime/server/page/server_routing.js b/packages/kit/src/runtime/server/page/server_routing.js index 9a064bc4f7dc..da14dd698b3d 100644 --- a/packages/kit/src/runtime/server/page/server_routing.js +++ b/packages/kit/src/runtime/server/page/server_routing.js @@ -84,6 +84,25 @@ export async function resolve_route(resolved_path, url, manifest) { } } +/** + * Resolve a route-ID resolution request (`/_app/routes//__route.js`) to a + * JS module containing the route's node loaders. Params are always `{}` since + * this endpoint exists to support `preloadCode(routeId)`, which doesn't need them. + * @param {string} route_id + * @param {URL} url + * @param {SSRManifest} manifest + * @returns {Response} + */ +export function resolve_route_by_id(route_id, url, manifest) { + if (!manifest._.client?.routes) { + return text('Server-side route resolution disabled', { status: 400 }); + } + + const route = manifest._.client.routes.find((r) => r.id === route_id) ?? null; + + return create_server_routing_response(route, {}, url, manifest._.client).response; +} + /** * @param {import('types').SSRClientRoute | null} route * @param {Partial>} params diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index a575adae62ac..d6c0a033d63c 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -26,13 +26,15 @@ import { validate_server_exports } from '../../utils/exports.js'; import { action_json_redirect, is_action_json_request } from './page/actions.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; import { get_public_env } from './env_module.js'; -import { resolve_route } from './page/server_routing.js'; +import { resolve_route, resolve_route_by_id } from './page/server_routing.js'; import { validateHeaders } from './validate-headers.js'; import { add_data_suffix, add_resolution_suffix, + extract_route_id, has_data_suffix, has_resolution_suffix, + is_route_id_resolution_path, strip_data_suffix, strip_resolution_suffix } from '../pathname.js'; @@ -138,12 +140,16 @@ export async function internal_respond(request, options, manifest, state) { let skip_route_resolution = false; + /** Whether this is a `/${app_dir}/routes//__route.js` request, used by `preloadCode` */ + let is_route_id_resolution_request = false; + if (is_route_resolution_request) { /** * If the request is for a route resolution, first modify the URL, then continue as normal * for path resolution, then return the route object as a JS file. */ url.pathname = strip_resolution_suffix(url.pathname); + is_route_id_resolution_request = is_route_id_resolution_path(url.pathname, base, app_dir); } else if (is_data_request) { url.pathname = strip_data_suffix(url.pathname) + @@ -270,7 +276,8 @@ export async function internal_respond(request, options, manifest, state) { /** @type {string | null} */ let resolved_path = url.pathname; - if (!remote_id) { + // `reroute` hooks receive pathnames, so they must not run for route-ID resolution requests + if (!remote_id && !is_route_id_resolution_request) { const prerendering_reroute_state = state.prerendering?.inside_reroute; try { // For the duration or a reroute, disable the prerendering state as reroute could call API endpoints @@ -354,6 +361,14 @@ export async function internal_respond(request, options, manifest, state) { } if (is_route_resolution_request) { + if (is_route_id_resolution_request) { + return resolve_route_by_id( + extract_route_id(resolved_path, app_dir), + new URL(request.url), + manifest + ); + } + return resolve_route(resolved_path, new URL(request.url), manifest); } diff --git a/packages/kit/test/ambient.d.ts b/packages/kit/test/ambient.d.ts index 8e27e47df4df..ac905fd86f07 100644 --- a/packages/kit/test/ambient.d.ts +++ b/packages/kit/test/ambient.d.ts @@ -12,7 +12,8 @@ declare global { const preloadData: (url: string) => Promise; const beforeNavigate: (fn: (navigation: BeforeNavigate) => void | boolean) => void; const afterNavigate: (fn: (navigation: AfterNavigate) => void) => void; - const preloadCode: (pathname: string) => Promise; + const preloadCode: (id: string) => Promise; + const match: (url: string) => Promise<{ id: string; params: Record } | null>; } export {}; diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index dd6c740c0306..9711112bf256 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -838,12 +838,85 @@ test.describe('Prefetching', () => { throw new Error('Error was not thrown'); } catch (/** @type {any} */ e) { expect(e.message).toMatch( - 'argument passed to preloadCode must be a pathname (i.e. "/about" rather than "http://example.com/about"' + 'argument passed to preloadCode must be a route ID (i.e. "/blog/[slug]" rather than "blog/[slug]")' ); } } }); + test('prefetches code programmatically with a dynamic route id', async ({ page, app }) => { + await page.goto('/routing/a'); + + await app.preloadCode('/routing/[slug]'); + + /** @type {string[]} */ + const requests = []; + page.on('request', (r) => { + requests.push(r.url()); + }); + + // the reliable, mode-independent assertion: navigating to a matching page + // afterwards must not fetch any additional JS modules for the route + await app.goto('/routing/preloaded-by-id'); + expect(await page.textContent('h1')).toBe('preloaded-by-id'); + + expect(requests.filter((r) => r.endsWith('.js') && !r.includes('__route.js'))).toEqual([]); + }); + + test('preloadCode after match does not re-request route resolution', async ({ page, app }) => { + await page.goto('/routing/a'); + + const matched = await app.match('/routing/matched-by-id'); + expect(matched?.id).toBe('/routing/[slug]'); + + /** @type {string[]} */ + const requests = []; + page.on('request', (r) => { + requests.push(r.url()); + }); + + await app.preloadCode(/** @type {string} */ (matched?.id)); + + // with server-side route resolution the loaders were cached during `match`, + // so no additional `__route.js` request may occur. In client-resolution mode + // this is trivially true (no `__route.js` requests exist at all) — the test is + // meaningful under the `test:server-side-route-resolution:*` suites, so don't + // remove it from the matrix + expect(requests.filter((r) => r.includes('__route.js'))).toEqual([]); + }); + + if (process.env.DEV) { + test('warns when preloadCode is called with an unknown route id', async ({ page, app }) => { + await page.goto('/routing/a'); + + /** @type {string[]} */ + const warnings = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') warnings.push(msg.text()); + }); + + await app.preloadCode('/does-not-exist-[at]-all'); + + expect(warnings.join('\n')).toMatch('did not match any page routes'); + }); + + if (!process.env.ROUTER_RESOLUTION) { + test('hints at `match` when preloadCode is called with a pathname', async ({ page, app }) => { + await page.goto('/routing/a'); + + /** @type {string[]} */ + const warnings = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') warnings.push(msg.text()); + }); + + await app.preloadCode('/routing/some-slug'); + + expect(warnings.join('\n')).toMatch('match('); + }); + } + } + test('prefetches data programmatically', async ({ baseURL, page, app }) => { await page.goto('/routing/a'); diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js index 895cb9ef6212..98c66b40c593 100644 --- a/packages/kit/test/apps/options/test/test.js +++ b/packages/kit/test/apps/options/test/test.js @@ -174,7 +174,8 @@ test.describe('trailingSlash', () => { // also wait for network processing to complete, see // https://playwright.dev/docs/network#network-events - await app.preloadCode('/path-base/preloading/preloaded'); + // route IDs are never prefixed with `paths.base` + await app.preloadCode('/preloading/preloaded'); // svelte request made is environment dependent if (process.env.DEV) { diff --git a/packages/kit/test/setup.js b/packages/kit/test/setup.js index d6d34f54a815..36bdf9fe1d79 100644 --- a/packages/kit/test/setup.js +++ b/packages/kit/test/setup.js @@ -6,6 +6,7 @@ import { beforeNavigate, afterNavigate } from '$app/navigation'; +import { match } from '$app/paths'; import { onMount, tick } from 'svelte'; export function setup() { @@ -18,6 +19,7 @@ export function setup() { preloadData, beforeNavigate, afterNavigate, + match, svelte_tick: tick }); diff --git a/packages/kit/test/types.d.ts b/packages/kit/test/types.d.ts index 4ae47f7d3b65..90ffc94cf8c1 100644 --- a/packages/kit/test/types.d.ts +++ b/packages/kit/test/types.d.ts @@ -18,8 +18,9 @@ export const test: TestType< invalidate(url: string): Promise; beforeNavigate(fn: (navigation: BeforeNavigate) => void | boolean): void; afterNavigate(fn: (navigation: AfterNavigate) => void): void; - preloadCode(pathname: string): Promise; + preloadCode(id: string): Promise; preloadData(url: string): Promise; + match(url: string): Promise<{ id: string; params: Record } | null>; }; clicknav( selector: string, diff --git a/packages/kit/test/utils.js b/packages/kit/test/utils.js index c41412520f35..cf4f1172817e 100644 --- a/packages/kit/test/utils.js +++ b/packages/kit/test/utils.js @@ -24,9 +24,11 @@ export const test = base.extend({ afterNavigate: () => page.evaluate(() => afterNavigate(() => {})), - preloadCode: (pathname) => page.evaluate((pathname) => preloadCode(pathname), pathname), + preloadCode: (id) => page.evaluate((id) => preloadCode(id), id), - preloadData: (url) => page.evaluate((url) => preloadData(url), url) + preloadData: (url) => page.evaluate((url) => preloadData(url), url), + + match: (url) => page.evaluate((url) => match(url), url) }); }, diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index dd97c8dd8fa6..6d640f2c73e0 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3133,6 +3133,7 @@ declare module '$app/forms' { } declare module '$app/navigation' { + import type { RouteId } from '$app/types'; /** * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a URL. * @@ -3238,13 +3239,24 @@ declare module '$app/navigation' { * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. * - * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about/+page.svelte`) or `/blog/*` (to match `src/routes/blog/[slug]/+page.svelte`). + * Takes a route ID such as `/about` or `/blog/[slug]`. Unlike pathnames, route IDs + * are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths). + * If you have a pathname rather than a route ID, you can convert it with + * [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`: + * + * ```js + * import { match } from '$app/paths'; + * import { preloadCode } from '$app/navigation'; + * + * const matched = await match('/blog/hello-world'); + * if (matched) await preloadCode(matched.id); + * ``` * * Unlike `preloadData`, this won't call `load` functions. * Returns a Promise that resolves when the modules have been imported. * * */ - export function preloadCode(pathname: string): Promise; + export function preloadCode(id: RouteId): Promise; /** * Programmatically create a new history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). * From aff95bc1d739eb8c5e0cdfee8604a0fa4e75a510 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 30 Jul 2026 10:03:30 -0400 Subject: [PATCH 2/7] chore: only generate each route resolution module once when prerendering (#16578) Stacked on #16576, per https://github.com/sveltejs/kit/pull/16576#discussion_r3679015025. `dependencies` is per-`visit()`, so the route-ID module was generated once per prerendered page. On the `prerendering/basics` fixture with `resolution: 'server'`, 31 generations produced 28 files; with this, 28 produce 28, and the output is byte-identical apart from the build version stamp. --- .changeset/lucky-moons-resolve.md | 5 +++++ packages/kit/src/core/postbuild/fallback.js | 3 ++- packages/kit/src/core/postbuild/prerender.js | 6 +++++- packages/kit/src/runtime/server/page/render.js | 5 ++++- packages/kit/src/types/internal.d.ts | 2 ++ 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 .changeset/lucky-moons-resolve.md diff --git a/.changeset/lucky-moons-resolve.md b/.changeset/lucky-moons-resolve.md new file mode 100644 index 000000000000..83155bcaa073 --- /dev/null +++ b/.changeset/lucky-moons-resolve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: only generate each route's resolution module once when prerendering diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js index 493be5283e1a..1023d7890aec 100644 --- a/packages/kit/src/core/postbuild/fallback.js +++ b/packages/kit/src/core/postbuild/fallback.js @@ -39,7 +39,8 @@ async function generate_fallback({ manifest_path, env, out_dir, origin, assets } prerendering: { fallback: true, dependencies: new Map(), - remote_responses: new Map() + remote_responses: new Map(), + resolved_route_ids: new Set() }, read: (file) => readFileSync(join(assets, file)) }); diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index f496a9f72f8d..0332aff3bfda 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -262,6 +262,9 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit /** @type {Map>} */ const remote_responses = new Map(); + /** @type {Set} */ + const resolved_route_ids = new Set(); + /** @type {Map>} */ const expected_hashlinks = new Map(); @@ -307,7 +310,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env, vit }, prerendering: { dependencies, - remote_responses + remote_responses, + resolved_route_ids }, read: (file) => { // stuff we just wrote diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 2ebffb668ced..8c9813f5be81 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -402,7 +402,10 @@ export async function render_response({ // prerendered route is filtered out of `manifest._.routes` (see `generateManifest` // in core/adapt/builder.js), so on a host with no SvelteKit server there is nothing // left to answer a resolution request for it at runtime. - if (route) { + // `dependencies` is per-page, so without this we'd regenerate once per prerendered page + if (route && !state.prerendering.resolved_route_ids.has(route.id)) { + state.prerendering.resolved_route_ids.add(route.id); + const id_pathname = paths.base + route_id_resolution_pathname(paths.app_dir, route.id); state.prerendering.dependencies.set( diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 3d5c723af32b..75cd1c433b85 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -239,6 +239,8 @@ export interface PrerenderOptions { dependencies: Map; /** Results of remote `prerender` functions, shared across the whole prerender run so that each only executes once */ remote_responses: Map>; + /** Route IDs whose resolution module has been emitted, shared across the whole prerender run so that each only generates once */ + resolved_route_ids: Set; /** True for the duration of a call to the `reroute` hook */ inside_reroute?: boolean; } From 6f4b78114661250cdd6b7c5cf6e15afa3de93915 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 30 Jul 2026 15:33:48 -0600 Subject: [PATCH 3/7] chore: cache endpoint-only route responses --- packages/kit/src/runtime/client/client.js | 53 ++++++++++++++----- .../src/runtime/server/page/server_routing.js | 33 ++++++++++-- .../basics/test/cross-platform/client.test.js | 45 +++++++++++++++- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 152efa483da2..5e146d4a9045 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -300,12 +300,19 @@ function discard_load_cache() { */ const reroute_cache = new Map(); +/** + * Sentinel for a route that exists but has no code to preload, i.e. a `+server.js` with no + * `+page`. Cached like a parsed route so that repeated `preloadCode(id)` calls for the same + * id don't re-request it. + */ +const ENDPOINT_ONLY = Symbol('endpoint only'); + /** * Cache of route ID -> parsed route for server-side route resolution. * Populated whenever a server route resolution occurs (`match`, link preloading, * hydration), so that `preloadCode(id)` doesn't need an extra server round trip. * Lives until full page reload. - * @type {Map} + * @type {Map} */ const route_id_cache = new Map(); @@ -819,14 +826,14 @@ async function _preload_data(intent) { } /** - * Fetch and parse the route with the given ID from the server-side - * route resolution endpoint. Returns `undefined` if the route doesn't exist. - * Only used when `router.resolution === 'server'`. + * Fetch and parse the route with the given ID from the server-side route resolution endpoint. + * Returns `ENDPOINT_ONLY` if the route exists but has no code to preload, or `undefined` if + * there is no such route. Only used when `router.resolution === 'server'`. * @param {string} id - * @returns {Promise} + * @returns {Promise} */ async function load_route_by_id(id) { - /** @type {{ route?: import('types').CSRRouteServer }} */ + /** @type {{ route?: import('types').CSRRouteServer, endpoint_only?: boolean }} */ let module; try { @@ -841,6 +848,12 @@ async function load_route_by_id(id) { return; } + if (module.endpoint_only) { + // the route exists, it just has no code — cache that so we don't ask again + route_id_cache.set(id, ENDPOINT_ONLY); + return ENDPOINT_ONLY; + } + if (!module.route) return; return parse_and_cache_server_route(module.route); @@ -2753,7 +2766,7 @@ export async function preloadCode(id) { ); } - /** @type {import('types').CSRRoute | undefined} */ + /** @type {import('types').CSRRoute | typeof ENDPOINT_ONLY | undefined} */ let route; if (__SVELTEKIT_CLIENT_ROUTING__) { @@ -2762,17 +2775,29 @@ export async function preloadCode(id) { route = route_id_cache.get(id) ?? (await load_route_by_id(id)); } + if (route === ENDPOINT_ONLY) { + if (DEV) { + console.warn( + `'${id}' has no \`+page\`, so there is no code to preload. If you meant to warm up an ` + + `endpoint, request it with \`fetch\` instead.` + ); + } + + return; + } + if (!route) { if (DEV) { - // TODO: this is a warning rather than an error because `$app/manifest`'s - // `routes` export includes endpoint-only (+server.js) routes, which have no - // code to preload and are indistinguishable from nonexistent ids on the - // client. Revisit if/when `$app/manifest.routes` is filtered or annotated - // (see https://github.com/sveltejs/kit/issues/16511) — at that point unknown - // ids could error again. - let message = `'${id}' did not match any page routes (note that endpoint-only routes have no code to preload)`; + // NOTE: still a warning rather than an error. Under server resolution we now know for + // certain that the id is unknown (endpoint-only routes are reported separately, above), + // but under client routing we can't tell the two apart — endpoint-only routes are + // deliberately absent from the client manifest. Throwing in one mode and warning in the + // other would make behaviour depend on a config option, so both warn. + let message = `'${id}' did not match any route`; if (__SVELTEKIT_CLIENT_ROUTING__) { + message += ` (note that routes without a \`+page\` have no code to preload)`; + // the most common migration mistake is passing a pathname, which used to work const candidates = [id]; if (base && id.startsWith(base)) candidates.push(id.slice(base.length) || '/'); diff --git a/packages/kit/src/runtime/server/page/server_routing.js b/packages/kit/src/runtime/server/page/server_routing.js index da14dd698b3d..13bfcb7c2521 100644 --- a/packages/kit/src/runtime/server/page/server_routing.js +++ b/packages/kit/src/runtime/server/page/server_routing.js @@ -88,6 +88,14 @@ export async function resolve_route(resolved_path, url, manifest) { * Resolve a route-ID resolution request (`/_app/routes//__route.js`) to a * JS module containing the route's node loaders. Params are always `{}` since * this endpoint exists to support `preloadCode(routeId)`, which doesn't need them. + * + * The module has one of three shapes, which the client uses to tell three cases apart: + * + * - `export const route = {...}` — a page route, with loaders to import + * - `export const endpoint_only = true` — a real route with no `+page`, so there is + * nothing to preload, but the client can cache that fact and stop asking + * - an empty module — no such route + * * @param {string} route_id * @param {URL} url * @param {SSRManifest} manifest @@ -98,9 +106,26 @@ export function resolve_route_by_id(route_id, url, manifest) { return text('Server-side route resolution disabled', { status: 400 }); } - const route = manifest._.client.routes.find((r) => r.id === route_id) ?? null; + const route = manifest._.client.routes.find((r) => r.id === route_id); + + if (route) { + return create_server_routing_response(route, {}, url, manifest._.client).response; + } + + // `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the + // route doesn't exist — it might be a `+server.js`-only route. `_.routes` includes those + // (with `page: null`), so we can distinguish "exists but has no code" from "unknown". + if (manifest._.routes.some((r) => r.id === route_id && !r.page)) { + return text('export const endpoint_only = true;', { headers: js_headers() }); + } - return create_server_routing_response(route, {}, url, manifest._.client).response; + return create_server_routing_response(null, {}, url, manifest._.client).response; +} + +function js_headers() { + return new Headers({ + 'content-type': 'application/javascript; charset=utf-8' + }); } /** @@ -111,9 +136,7 @@ export function resolve_route_by_id(route_id, url, manifest) { * @returns {{response: Response, body: string}} */ export function create_server_routing_response(route, params, url, client) { - const headers = new Headers({ - 'content-type': 'application/javascript; charset=utf-8' - }); + const headers = js_headers(); if (route) { const csr_route = generate_route_object(route, url, client); diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 9711112bf256..448adf2b4104 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -885,6 +885,24 @@ test.describe('Prefetching', () => { expect(requests.filter((r) => r.includes('__route.js'))).toEqual([]); }); + test('preloadCode caches endpoint-only route ids', async ({ page, app }) => { + await page.goto('/routing/a'); + + // first call discovers that `/set-cookie` exists but has no `+page` + await app.preloadCode('/set-cookie'); + + /** @type {string[]} */ + const requests = []; + page.on('request', (r) => { + requests.push(r.url()); + }); + + // the second call must be answered from the cache, without asking the server again + await app.preloadCode('/set-cookie'); + + expect(requests.filter((r) => r.includes('__route.js'))).toEqual([]); + }); + if (process.env.DEV) { test('warns when preloadCode is called with an unknown route id', async ({ page, app }) => { await page.goto('/routing/a'); @@ -897,7 +915,32 @@ test.describe('Prefetching', () => { await app.preloadCode('/does-not-exist-[at]-all'); - expect(warnings.join('\n')).toMatch('did not match any page routes'); + expect(warnings.join('\n')).toMatch('did not match any route'); + }); + + test('warns when preloadCode is called with an endpoint-only route id', async ({ + page, + app + }) => { + await page.goto('/routing/a'); + + /** @type {string[]} */ + const warnings = []; + page.on('console', (msg) => { + if (msg.type() === 'warning') warnings.push(msg.text()); + }); + + // `/set-cookie` is a `+server.js` with no `+page`, so there is no code to preload + await app.preloadCode('/set-cookie'); + + if (process.env.ROUTER_RESOLUTION) { + // under server resolution the endpoint tells us the route exists but has no page + expect(warnings.join('\n')).toMatch('has no `+page`'); + } else { + // under client routing, endpoint-only routes aren't in the client manifest at all, + // so they're indistinguishable from an unknown id + expect(warnings.join('\n')).toMatch('did not match any route'); + } }); if (!process.env.ROUTER_RESOLUTION) { From 705091e94ecc43bdfe35be3accc542c8fb6acae1 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 30 Jul 2026 16:09:15 -0600 Subject: [PATCH 4/7] misc --- packages/kit/src/runtime/client/client.js | 9 +++---- .../kit/src/runtime/server/page/render.js | 6 +---- .../src/runtime/server/page/server_routing.js | 26 +++++++++++-------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 5e146d4a9045..c25f7181b09b 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -849,7 +849,7 @@ async function load_route_by_id(id) { } if (module.endpoint_only) { - // the route exists, it just has no code — cache that so we don't ask again + // The route exists, it just has no code to preload. route_id_cache.set(id, ENDPOINT_ONLY); return ENDPOINT_ONLY; } @@ -2788,11 +2788,8 @@ export async function preloadCode(id) { if (!route) { if (DEV) { - // NOTE: still a warning rather than an error. Under server resolution we now know for - // certain that the id is unknown (endpoint-only routes are reported separately, above), - // but under client routing we can't tell the two apart — endpoint-only routes are - // deliberately absent from the client manifest. Throwing in one mode and warning in the - // other would make behaviour depend on a config option, so both warn. + // warn rather than throw, since under client routing an endpoint-only route id is + // indistinguishable from a typo — the client manifest only contains routes with a `+page` let message = `'${id}' did not match any route`; if (__SVELTEKIT_CLIENT_ROUTING__) { diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 8c9813f5be81..941ca42210a8 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -398,11 +398,7 @@ export async function render_response({ // Prerender a route-ID-keyed `/_app/routes//__route.js` module alongside the // pathname-keyed one above, so that `preloadCode(id)` can resolve a route ID without - // hitting the server. This is required, not merely an optimisation: a fully - // prerendered route is filtered out of `manifest._.routes` (see `generateManifest` - // in core/adapt/builder.js), so on a host with no SvelteKit server there is nothing - // left to answer a resolution request for it at runtime. - // `dependencies` is per-page, so without this we'd regenerate once per prerendered page + // hitting the server. if (route && !state.prerendering.resolved_route_ids.has(route.id)) { state.prerendering.resolved_route_ids.add(route.id); diff --git a/packages/kit/src/runtime/server/page/server_routing.js b/packages/kit/src/runtime/server/page/server_routing.js index 13bfcb7c2521..6a950ec194bb 100644 --- a/packages/kit/src/runtime/server/page/server_routing.js +++ b/packages/kit/src/runtime/server/page/server_routing.js @@ -106,20 +106,24 @@ export function resolve_route_by_id(route_id, url, manifest) { return text('Server-side route resolution disabled', { status: 400 }); } - const route = manifest._.client.routes.find((r) => r.id === route_id); + try { + const route = manifest._.client.routes.find((r) => r.id === route_id); - if (route) { - return create_server_routing_response(route, {}, url, manifest._.client).response; - } + if (route) { + return create_server_routing_response(route, {}, url, manifest._.client).response; + } - // `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the - // route doesn't exist — it might be a `+server.js`-only route. `_.routes` includes those - // (with `page: null`), so we can distinguish "exists but has no code" from "unknown". - if (manifest._.routes.some((r) => r.id === route_id && !r.page)) { - return text('export const endpoint_only = true;', { headers: js_headers() }); - } + // `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the + // route doesn't exist — it might be a `+server.js`-only route. `_.routes` includes those + // (with `page: null`), so we can distinguish "exists but has no code" from "unknown". + if (manifest._.routes.some((r) => r.id === route_id && !r.page)) { + return text('export const endpoint_only = true;', { headers: js_headers() }); + } - return create_server_routing_response(null, {}, url, manifest._.client).response; + return create_server_routing_response(null, {}, url, manifest._.client).response; + } catch { + return text('Error resolving route', { status: 500 }); + } } function js_headers() { From 511964a29441118b2dc57ed306ccdf9a0e018fa2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Aug 2026 12:51:44 -0400 Subject: [PATCH 5/7] separate treatment for route resolution and route id resolution --- packages/kit/src/runtime/pathname.js | 2 +- .../kit/src/runtime/server/page/render.js | 4 ++-- .../src/runtime/server/page/server_routing.js | 19 +++++++++------- packages/kit/src/runtime/server/respond.js | 22 +++++++++---------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/kit/src/runtime/pathname.js b/packages/kit/src/runtime/pathname.js index b4aa07f88e34..baa7270a266c 100644 --- a/packages/kit/src/runtime/pathname.js +++ b/packages/kit/src/runtime/pathname.js @@ -58,7 +58,7 @@ const ROUTES_PREFIX = '/routes'; * @returns {string} */ export function route_id_resolution_pathname(app_dir, route_id) { - return add_resolution_suffix(`/${app_dir}${ROUTES_PREFIX}${route_id === '/' ? '' : route_id}`); + return `/${app_dir}${ROUTES_PREFIX}${route_id === '/' ? '' : route_id}`; } /** diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 2a7c9c8547f8..19ecbc369594 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -395,7 +395,7 @@ export async function render_response({ create_server_routing_response(route, event.params, new URL(pathname, event.url), client) ); - // Prerender a route-ID-keyed `/_app/routes//__route.js` module alongside the + // Prerender a route-ID-keyed `/_app/routes/` module alongside the // pathname-keyed one above, so that `preloadCode(id)` can resolve a route ID without // hitting the server. if (route && !state.prerendering.resolved_route_ids.has(route.id)) { @@ -405,7 +405,7 @@ export async function render_response({ state.prerendering.dependencies.set( id_pathname, - create_server_routing_response(route, {}, new URL(id_pathname, event.url), client) + create_server_routing_response(route, null, new URL(id_pathname, event.url), client) ); } } diff --git a/packages/kit/src/runtime/server/page/server_routing.js b/packages/kit/src/runtime/server/page/server_routing.js index 6a950ec194bb..0cc9a027faba 100644 --- a/packages/kit/src/runtime/server/page/server_routing.js +++ b/packages/kit/src/runtime/server/page/server_routing.js @@ -110,7 +110,7 @@ export function resolve_route_by_id(route_id, url, manifest) { const route = manifest._.client.routes.find((r) => r.id === route_id); if (route) { - return create_server_routing_response(route, {}, url, manifest._.client).response; + return create_server_routing_response(route, null, url, manifest._.client).response; } // `client.routes` only contains routes with a `+page`, so a miss above doesn't mean the @@ -120,7 +120,7 @@ export function resolve_route_by_id(route_id, url, manifest) { return text('export const endpoint_only = true;', { headers: js_headers() }); } - return create_server_routing_response(null, {}, url, manifest._.client).response; + return create_server_routing_response(null, null, url, manifest._.client).response; } catch { return text('Error resolving route', { status: 500 }); } @@ -134,22 +134,25 @@ function js_headers() { /** * @param {import('types').SSRClientRoute | null} route - * @param {Partial>} params + * @param {Partial> | null} params * @param {URL} url * @param {NonNullable} client * @returns {{response: Response, body: string}} */ export function create_server_routing_response(route, params, url, client) { const headers = js_headers(); + let body = ''; if (route) { const csr_route = generate_route_object(route, url, client); - const body = `${create_css_import(route, url, client)}\nexport const route = ${csr_route}; export const params = ${JSON.stringify(params)};`; + body = `${create_css_import(route, url, client)}export const route = ${csr_route};`; - return { response: text(body, { headers }), body }; - } else { - return { response: text('', { headers }), body: '' }; + if (params !== null) { + body += `\nexport const params = ${JSON.stringify(params)}`; + } } + + return { response: text(body, { headers }), body }; } /** @@ -178,5 +181,5 @@ function create_css_import(route, url, client) { if (!css) return ''; - return `${create_client_import(client.start, url)}.then(x => x.load_css([${css}]));`; + return `${create_client_import(client.start, url)}.then(x => x.load_css([${css}]));\n`; } diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index d6c0a033d63c..22ea8ba63b6a 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -91,6 +91,8 @@ export async function internal_respond(request, options, manifest, state) { /** URL but stripped from the potential `/__data.json` suffix and its search param */ const url = new URL(request.url); + /** Whether this is a `/${app_dir}/routes//__route.js` request, used by `preloadCode` */ + const is_route_id_resolution_request = is_route_id_resolution_path(url.pathname, base, app_dir); const is_route_resolution_request = has_resolution_suffix(url.pathname); const is_data_request = has_data_suffix(url.pathname); const remote_id = get_remote_id(url); @@ -140,16 +142,12 @@ export async function internal_respond(request, options, manifest, state) { let skip_route_resolution = false; - /** Whether this is a `/${app_dir}/routes//__route.js` request, used by `preloadCode` */ - let is_route_id_resolution_request = false; - if (is_route_resolution_request) { /** * If the request is for a route resolution, first modify the URL, then continue as normal * for path resolution, then return the route object as a JS file. */ url.pathname = strip_resolution_suffix(url.pathname); - is_route_id_resolution_request = is_route_id_resolution_path(url.pathname, base, app_dir); } else if (is_data_request) { url.pathname = strip_data_suffix(url.pathname) + @@ -360,15 +358,15 @@ export async function internal_respond(request, options, manifest, state) { resolved_path = resolved_path.slice(base.length) || '/'; } - if (is_route_resolution_request) { - if (is_route_id_resolution_request) { - return resolve_route_by_id( - extract_route_id(resolved_path, app_dir), - new URL(request.url), - manifest - ); - } + if (is_route_id_resolution_request) { + return resolve_route_by_id( + extract_route_id(resolved_path, app_dir), + new URL(request.url), + manifest + ); + } + if (is_route_resolution_request) { return resolve_route(resolved_path, new URL(request.url), manifest); } From 46a7f92e455f3c4b9b71cb165ac4e42c548b494b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Aug 2026 13:15:29 -0400 Subject: [PATCH 6/7] simplify --- packages/kit/src/runtime/client/client.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 60530736de22..d10b53952e6e 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2763,14 +2763,9 @@ export async function preloadCode(id) { ); } - /** @type {import('types').CSRRoute | typeof ENDPOINT_ONLY | undefined} */ - let route; - - if (__SVELTEKIT_CLIENT_ROUTING__) { - route = routes.find((r) => r.id === id); - } else { - route = route_id_cache.get(id) ?? (await load_route_by_id(id)); - } + const route = __SVELTEKIT_CLIENT_ROUTING__ + ? routes.find((r) => r.id === id) + : (route_id_cache.get(id) ?? (await load_route_by_id(id))); if (route === ENDPOINT_ONLY) { if (DEV) { From 81c34f3bfb92ae4e89732032a04e60f4f7eb2cc2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Aug 2026 15:55:40 -0400 Subject: [PATCH 7/7] revert the bit vercel-bot was mad about --- packages/kit/src/runtime/pathname.js | 2 +- .../kit/src/runtime/server/page/render.js | 2 +- packages/kit/src/runtime/server/respond.js | 22 ++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/kit/src/runtime/pathname.js b/packages/kit/src/runtime/pathname.js index baa7270a266c..b4aa07f88e34 100644 --- a/packages/kit/src/runtime/pathname.js +++ b/packages/kit/src/runtime/pathname.js @@ -58,7 +58,7 @@ const ROUTES_PREFIX = '/routes'; * @returns {string} */ export function route_id_resolution_pathname(app_dir, route_id) { - return `/${app_dir}${ROUTES_PREFIX}${route_id === '/' ? '' : route_id}`; + return add_resolution_suffix(`/${app_dir}${ROUTES_PREFIX}${route_id === '/' ? '' : route_id}`); } /** diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 19ecbc369594..de1eaf54ecb6 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -395,7 +395,7 @@ export async function render_response({ create_server_routing_response(route, event.params, new URL(pathname, event.url), client) ); - // Prerender a route-ID-keyed `/_app/routes/` module alongside the + // Prerender a route-ID-keyed `/_app/routes//__route.js` module alongside the // pathname-keyed one above, so that `preloadCode(id)` can resolve a route ID without // hitting the server. if (route && !state.prerendering.resolved_route_ids.has(route.id)) { diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 22ea8ba63b6a..d6c0a033d63c 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -91,8 +91,6 @@ export async function internal_respond(request, options, manifest, state) { /** URL but stripped from the potential `/__data.json` suffix and its search param */ const url = new URL(request.url); - /** Whether this is a `/${app_dir}/routes//__route.js` request, used by `preloadCode` */ - const is_route_id_resolution_request = is_route_id_resolution_path(url.pathname, base, app_dir); const is_route_resolution_request = has_resolution_suffix(url.pathname); const is_data_request = has_data_suffix(url.pathname); const remote_id = get_remote_id(url); @@ -142,12 +140,16 @@ export async function internal_respond(request, options, manifest, state) { let skip_route_resolution = false; + /** Whether this is a `/${app_dir}/routes//__route.js` request, used by `preloadCode` */ + let is_route_id_resolution_request = false; + if (is_route_resolution_request) { /** * If the request is for a route resolution, first modify the URL, then continue as normal * for path resolution, then return the route object as a JS file. */ url.pathname = strip_resolution_suffix(url.pathname); + is_route_id_resolution_request = is_route_id_resolution_path(url.pathname, base, app_dir); } else if (is_data_request) { url.pathname = strip_data_suffix(url.pathname) + @@ -358,15 +360,15 @@ export async function internal_respond(request, options, manifest, state) { resolved_path = resolved_path.slice(base.length) || '/'; } - if (is_route_id_resolution_request) { - return resolve_route_by_id( - extract_route_id(resolved_path, app_dir), - new URL(request.url), - manifest - ); - } - if (is_route_resolution_request) { + if (is_route_id_resolution_request) { + return resolve_route_by_id( + extract_route_id(resolved_path, app_dir), + new URL(request.url), + manifest + ); + } + return resolve_route(resolved_path, new URL(request.url), manifest); }