From 4a5addaadc613245d140c467619c1f15267aaba3 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:56:45 -0400 Subject: [PATCH 1/5] breaking: exclude directories without a `+page` or `+server` from `RouteId` --- .changeset/routeid-only-real-routes.md | 5 +++++ .../docs/98-reference/22-$app-types.md | 4 +++- .../core/sync/create_manifest_data/index.js | 18 ++++++++++++++++++ packages/kit/src/core/sync/write_app_types.js | 13 +++++++++---- .../kit/src/core/sync/write_types/index.js | 7 +++++-- .../sync/write_types/test/app-types/+page.js | 5 ++++- packages/kit/src/exports/vite/index.js | 15 +++++++++++---- packages/kit/src/types/ambient.d.ts | 8 +++++--- packages/kit/test/apps/basics/test/test.js | 2 ++ packages/kit/types/index.d.ts | 8 +++++--- 10 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 .changeset/routeid-only-real-routes.md diff --git a/.changeset/routeid-only-real-routes.md b/.changeset/routeid-only-real-routes.md new file mode 100644 index 000000000000..6104c72f0735 --- /dev/null +++ b/.changeset/routeid-only-real-routes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: only include routes with a `+page` or `+server` in `RouteId` and `$app/manifest` diff --git a/documentation/docs/98-reference/22-$app-types.md b/documentation/docs/98-reference/22-$app-types.md index 0663a89ea385..d2a9934fed7f 100644 --- a/documentation/docs/98-reference/22-$app-types.md +++ b/documentation/docs/98-reference/22-$app-types.md @@ -82,10 +82,12 @@ type RouteParams = { /* generated */ } | Record ```dts -type RouteParams = { /* generated */ } | Record; +type LayoutParams = { /* generated */ }; ``` diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 5cdf83a03cb8..c4b76d5e5d85 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -41,6 +41,24 @@ export default function create_manifest_data({ }; } +/** + * Whether the router can match this route. `manifest_data.routes` also contains an entry for + * every other directory in `src/routes`, so that layouts and params can be resolved, and those + * ids never reach `event.route.id`. + * @param {import('types').RouteData} route + */ +export function is_app_route(route) { + return !!(route.page || route.endpoint); +} + +/** + * Returns the routes that `$app/manifest` exposes and that `RouteId` is generated from. + * @param {import('types').ManifestData} manifest_data + */ +export function get_app_routes(manifest_data) { + return manifest_data.routes.filter(is_app_route).map((route) => ({ id: route.id })); +} + /** * Returns a list of files in the `static` directory. * @param {import('types').ValidatedConfig} config diff --git a/packages/kit/src/core/sync/write_app_types.js b/packages/kit/src/core/sync/write_app_types.js index e8a76fdc25d6..1ee63c19d923 100644 --- a/packages/kit/src/core/sync/write_app_types.js +++ b/packages/kit/src/core/sync/write_app_types.js @@ -5,6 +5,7 @@ import { posixify } from '../../utils/os.js'; import { write_if_changed } from './utils.js'; import { s } from '../../utils/misc.js'; import { get_route_segments } from '../../utils/routing.js'; +import { get_app_routes, is_app_route } from './create_manifest_data/index.js'; const replace_optional_params = (/** @type {string} */ id) => id.replace(/\/\[\[[^\]]+\]\]/g, '${string}'); @@ -203,9 +204,9 @@ function generate_app_types(manifest_data, config, dir) { const type = get_matcher_type(p.matcher); return `${/^\w+$/.test(p.name) ? p.name : `'${p.name}'`}${p.optional ? '?:' : ':'} ${type}${p.optional ? ' | undefined' : ''}`; }); - const route_type = `${s(route.id)}: { ${params.join('; ')} }`; - - dynamic_routes.push(route_type); + if (is_app_route(route)) { + dynamic_routes.push(`${s(route.id)}: { ${params.join('; ')} }`); + } normalized_pathname = replace_required_params(replace_optional_params(pathname)).slice(1); serialise = (p) => `\`${p}\` & {}`; @@ -237,7 +238,11 @@ function generate_app_types(manifest_data, config, dir) { return [ 'declare module "$app/types" {', '\texport interface AppTypes {', - `\t\tRouteId(): ${manifest_data.routes.map((r) => s(r.id)).join(' | ')};`, + `\t\tRouteId(): ${ + get_app_routes(manifest_data) + .map((route) => s(route.id)) + .join(' | ') || 'never' + };`, `\t\tRouteParams(): {\n\t\t\t${dynamic_routes.join(';\n\t\t\t')}\n\t\t};`, `\t\tLayoutParams(): {\n\t\t\t${layouts.join(';\n\t\t\t')}\n\t\t};`, `\t\tPath(): ${Array.from(pathnames).join(' | ')};`, diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index 184766fbf85c..09d02365e13a 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -5,6 +5,7 @@ import { rimraf, walk, resolve_entry } from '../../../utils/filesystem.js'; import { compact } from '../../../utils/array.js'; import { posixify } from '../../../utils/os.js'; import { ts } from '../ts.js'; +import { is_app_route } from '../create_manifest_data/index.js'; const remove_relative_parent_traversals = (/** @type {string} */ path) => path.replace(/\.\.\//g, ''); const is_whitespace = (/** @type {string} */ char) => /\s/.test(char); @@ -278,7 +279,9 @@ function update_types(config, routes, route, root, to_delete = new Set()) { let all_pages_have_load = true; /** @type {import('types').RouteParam[]} */ const layout_params = []; - const ids = ['RouteId']; + // a layout can live in a directory that has no `+page` or `+server`, in which case its + // own id is never the matched route + const ids = is_app_route(route) ? ['RouteId'] : []; route.layout.child_pages?.forEach((page) => { const leaf = routes.get(page); @@ -312,7 +315,7 @@ function update_types(config, routes, route, root, to_delete = new Set()) { ids.push('null'); } - declarations.push(`type LayoutRouteId = ${ids.join(' | ')}`); + declarations.push(`type LayoutRouteId = ${ids.join(' | ') || 'never'}`); declarations.push( 'type LayoutParams = RouteParams & ' + generate_params_type(layout_params, outdir, config) diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.js b/packages/kit/src/core/sync/write_types/test/app-types/+page.js index ee5b3a316821..94e128fafbde 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.js @@ -7,9 +7,12 @@ id = '/foo/[bar]/[baz]'; id = '/(group)/path-a'; // @ts-expect-error -// eslint-disable-next-line @typescript-eslint/no-unused-vars id = '/nope'; +// @ts-expect-error `/foo` is a directory with no `+page` or `+server`, so it is not a route +// eslint-disable-next-line @typescript-eslint/no-unused-vars +id = '/foo'; + /** @type {import('$app/types').RouteParams<'/foo/[bar]/[baz]'>} */ const params = { bar: 'A', diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index f5a8f5633d50..e88ebd0aaaa6 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -46,6 +46,7 @@ import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; import { hash } from '../../utils/hash.js'; import { dedent } from '../../core/sync/utils.js'; +import { get_app_routes } from '../../core/sync/create_manifest_data/index.js'; import { get_import_aliases, get_hash_import_keys } from '../../utils/imports.js'; import { app_env_private, @@ -1129,7 +1130,9 @@ function kit({ svelte_config }) { ]; export const routes = [ - ${manifest_data.routes.map((route) => s({ id: route.id })).join(',\n')} + ${get_app_routes(manifest_data) + .map((route) => s(route)) + .join(',\n')} ]; `; } @@ -1571,7 +1574,7 @@ function kit({ svelte_config }) { // the client build and after prerendering respectively. replace_manifest_placeholder_variables(server_chunks, `${out}/server`, { assets: manifest_data.assets.map((asset) => ({ path: asset.file })), - routes: manifest_data.routes.map((route) => ({ id: route.id })) + routes: get_app_routes(manifest_data) }); const verbose = builder.config.logLevel === 'info'; @@ -1733,7 +1736,7 @@ function kit({ svelte_config }) { replace_manifest_placeholder_variables(client_chunks, `${out}/client`, { immutable, assets: manifest_data.assets.map((asset) => ({ path: asset.file })), - routes: manifest_data.routes.map((route) => ({ id: route.id })) + routes: get_app_routes(manifest_data) }); // Now that the client build is done, replace the `build` sentinel @@ -2145,7 +2148,11 @@ const create_manifest_data_module = (is_build, manifest_data) => { // In dev, `manifest_data` may not be set yet on the very first load, // but `configureServer` (which calls `sync.create`) runs before any // module is served, so it will be set by the time this is called. - const routes = manifest_data?.routes.map((route) => s({ id: route.id })).join(',\n') ?? ''; + const routes = manifest_data + ? get_app_routes(manifest_data) + .map((route) => s(route)) + .join(',\n') + : ''; const assets = manifest_data?.assets.map((asset) => s({ path: asset.file })).join(',\n') ?? ''; return dedent` diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index 288495ddc974..ea7a35f957b1 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -121,10 +121,12 @@ declare module '$app/types' { /** * A utility for getting the parameters associated with a given layout, which is similar to `RouteParams` but also includes optional parameters for any child route. + * + * Unlike `RouteId`, this accepts any directory in `src/routes`, since a layout can live in a directory that has no `+page` or `+server` of its own. */ - export type LayoutParams = T extends keyof ReturnType - ? ReturnType[T] - : Record; + export type LayoutParams> = ReturnType< + AppTypes['LayoutParams'] + >[T]; /** * A union of all valid paths in your app, relative to the `base` path. diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 908e760ca214..2202bbcb77c1 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -782,6 +782,8 @@ test.describe('$app/manifest', () => { expect(ids).toContain('/'); expect(ids).toContain('/app-manifest'); expect(ids).toContain('/routing'); + // `/accessibility` only holds a layout and child routes, so it isn't a route itself + expect(ids).not.toContain('/accessibility'); }); test('exposes static assets', async ({ page }) => { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 2e3440aab488..e8ed2e0e6987 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3809,10 +3809,12 @@ declare module '$app/types' { /** * A utility for getting the parameters associated with a given layout, which is similar to `RouteParams` but also includes optional parameters for any child route. + * + * Unlike `RouteId`, this accepts any directory in `src/routes`, since a layout can live in a directory that has no `+page` or `+server` of its own. */ - export type LayoutParams = T extends keyof ReturnType - ? ReturnType[T] - : Record; + export type LayoutParams> = ReturnType< + AppTypes['LayoutParams'] + >[T]; /** * A union of all valid paths in your app, relative to the `base` path. From 37b819d309c6538bd7948253115b748ca281989e Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:16:39 -0400 Subject: [PATCH 2/5] defer to #16588 for `$app/manifest` --- .changeset/routeid-only-real-routes.md | 2 +- packages/kit/src/exports/vite/index.js | 15 ++++----------- packages/kit/test/apps/basics/test/test.js | 2 -- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.changeset/routeid-only-real-routes.md b/.changeset/routeid-only-real-routes.md index 6104c72f0735..014f1c6e160e 100644 --- a/.changeset/routeid-only-real-routes.md +++ b/.changeset/routeid-only-real-routes.md @@ -2,4 +2,4 @@ '@sveltejs/kit': major --- -breaking: only include routes with a `+page` or `+server` in `RouteId` and `$app/manifest` +breaking: only include routes with a `+page` or `+server` in `RouteId` diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index e88ebd0aaaa6..f5a8f5633d50 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -46,7 +46,6 @@ import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; import { hash } from '../../utils/hash.js'; import { dedent } from '../../core/sync/utils.js'; -import { get_app_routes } from '../../core/sync/create_manifest_data/index.js'; import { get_import_aliases, get_hash_import_keys } from '../../utils/imports.js'; import { app_env_private, @@ -1130,9 +1129,7 @@ function kit({ svelte_config }) { ]; export const routes = [ - ${get_app_routes(manifest_data) - .map((route) => s(route)) - .join(',\n')} + ${manifest_data.routes.map((route) => s({ id: route.id })).join(',\n')} ]; `; } @@ -1574,7 +1571,7 @@ function kit({ svelte_config }) { // the client build and after prerendering respectively. replace_manifest_placeholder_variables(server_chunks, `${out}/server`, { assets: manifest_data.assets.map((asset) => ({ path: asset.file })), - routes: get_app_routes(manifest_data) + routes: manifest_data.routes.map((route) => ({ id: route.id })) }); const verbose = builder.config.logLevel === 'info'; @@ -1736,7 +1733,7 @@ function kit({ svelte_config }) { replace_manifest_placeholder_variables(client_chunks, `${out}/client`, { immutable, assets: manifest_data.assets.map((asset) => ({ path: asset.file })), - routes: get_app_routes(manifest_data) + routes: manifest_data.routes.map((route) => ({ id: route.id })) }); // Now that the client build is done, replace the `build` sentinel @@ -2148,11 +2145,7 @@ const create_manifest_data_module = (is_build, manifest_data) => { // In dev, `manifest_data` may not be set yet on the very first load, // but `configureServer` (which calls `sync.create`) runs before any // module is served, so it will be set by the time this is called. - const routes = manifest_data - ? get_app_routes(manifest_data) - .map((route) => s(route)) - .join(',\n') - : ''; + const routes = manifest_data?.routes.map((route) => s({ id: route.id })).join(',\n') ?? ''; const assets = manifest_data?.assets.map((asset) => s({ path: asset.file })).join(',\n') ?? ''; return dedent` diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 2202bbcb77c1..908e760ca214 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -782,8 +782,6 @@ test.describe('$app/manifest', () => { expect(ids).toContain('/'); expect(ids).toContain('/app-manifest'); expect(ids).toContain('/routing'); - // `/accessibility` only holds a layout and child routes, so it isn't a route itself - expect(ids).not.toContain('/accessibility'); }); test('exposes static assets', async ({ page }) => { From 32d0ecef4d413acad0799757dd9aca89f2854517 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:16:39 -0400 Subject: [PATCH 3/5] generate RouteId in a single pass --- .../src/core/sync/create_manifest_data/index.js | 8 -------- packages/kit/src/core/sync/write_app_types.js | 15 +++++++++------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index c4b76d5e5d85..93f4a26c5138 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -51,14 +51,6 @@ export function is_app_route(route) { return !!(route.page || route.endpoint); } -/** - * Returns the routes that `$app/manifest` exposes and that `RouteId` is generated from. - * @param {import('types').ManifestData} manifest_data - */ -export function get_app_routes(manifest_data) { - return manifest_data.routes.filter(is_app_route).map((route) => ({ id: route.id })); -} - /** * Returns a list of files in the `static` directory. * @param {import('types').ValidatedConfig} config diff --git a/packages/kit/src/core/sync/write_app_types.js b/packages/kit/src/core/sync/write_app_types.js index 1ee63c19d923..81a844220d2a 100644 --- a/packages/kit/src/core/sync/write_app_types.js +++ b/packages/kit/src/core/sync/write_app_types.js @@ -5,7 +5,7 @@ import { posixify } from '../../utils/os.js'; import { write_if_changed } from './utils.js'; import { s } from '../../utils/misc.js'; import { get_route_segments } from '../../utils/routing.js'; -import { get_app_routes, is_app_route } from './create_manifest_data/index.js'; +import { is_app_route } from './create_manifest_data/index.js'; const replace_optional_params = (/** @type {string} */ id) => id.replace(/\/\[\[[^\]]+\]\]/g, '${string}'); @@ -143,6 +143,9 @@ function generate_app_types(manifest_data, config, dir) { /** @type {Set} */ const pathnames = new Set(); + /** @type {string[]} */ + const app_route_ids = []; + /** @type {string[]} */ const dynamic_routes = []; @@ -193,6 +196,10 @@ function generate_app_types(manifest_data, config, dir) { } for (const route of manifest_data.routes) { + if (is_app_route(route)) { + app_route_ids.push(s(route.id)); + } + const pathname = remove_group_segments(route.id); let normalized_pathname = pathname.slice(1); @@ -238,11 +245,7 @@ function generate_app_types(manifest_data, config, dir) { return [ 'declare module "$app/types" {', '\texport interface AppTypes {', - `\t\tRouteId(): ${ - get_app_routes(manifest_data) - .map((route) => s(route.id)) - .join(' | ') || 'never' - };`, + `\t\tRouteId(): ${app_route_ids.join(' | ') || 'never'};`, `\t\tRouteParams(): {\n\t\t\t${dynamic_routes.join(';\n\t\t\t')}\n\t\t};`, `\t\tLayoutParams(): {\n\t\t\t${layouts.join(';\n\t\t\t')}\n\t\t};`, `\t\tPath(): ${Array.from(pathnames).join(' | ')};`, From f6d160944668f5b7cd52e666eb86bc1ff7d822da Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:45:44 -0400 Subject: [PATCH 4/5] test that a directory with only a layout is excluded from RouteId --- .../kit/src/core/sync/write_types/test/app-types/+page.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.js b/packages/kit/src/core/sync/write_types/test/app-types/+page.js index 94e128fafbde..ec71ebf72393 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.js +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.js @@ -10,9 +10,12 @@ id = '/(group)/path-a'; id = '/nope'; // @ts-expect-error `/foo` is a directory with no `+page` or `+server`, so it is not a route -// eslint-disable-next-line @typescript-eslint/no-unused-vars id = '/foo'; +// @ts-expect-error a directory with only a `+layout` is not a route +// eslint-disable-next-line @typescript-eslint/no-unused-vars +id = '/(group)/path-a/trailing-slash/always/layout'; + /** @type {import('$app/types').RouteParams<'/foo/[bar]/[baz]'>} */ const params = { bar: 'A', From 6c7935d43945939c9862b7d21b7aaccfe00df4e5 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:54:04 -0400 Subject: [PATCH 5/5] use is_app_route for the $app/manifest routes filter --- .changeset/famous-pandas-hammer.md | 2 +- packages/kit/src/core/sync/create_manifest_data/index.js | 4 +--- packages/kit/src/exports/vite/index.js | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.changeset/famous-pandas-hammer.md b/.changeset/famous-pandas-hammer.md index 6959de0d4be9..0bcfccf2635c 100644 --- a/.changeset/famous-pandas-hammer.md +++ b/.changeset/famous-pandas-hammer.md @@ -2,4 +2,4 @@ '@sveltejs/kit': patch --- -fix: remove paths without layout files from `LayoutParams` +fix: exclude routes without a page or endpoint from `routes` in `$app/manifest`, and remove directories with no route files from `LayoutParams` diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 42d46bd35245..2a5782928487 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -451,9 +451,7 @@ function create_routes_and_nodes(cwd, config, fallback) { } // remove route objects with no route file - routes = routes.filter( - (route) => route.page || route.endpoint || route.leaf || route.layout || route.error - ); + routes = routes.filter((route) => route.endpoint || route.leaf || route.layout || route.error); // add parents to error nodes so that we can compute which page options apply to them for (const route of routes) { diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 2e9d2fc874f7..655843db6996 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -41,6 +41,7 @@ import { } from './utils.js'; import { stackless } from '../../utils/error.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; +import { is_app_route } from '../../core/sync/create_manifest_data/index.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; import { s } from '../../utils/misc.js'; @@ -2127,7 +2128,7 @@ function stringify_assets(assets) { function stringify_routes(routes) { return ( routes - ?.filter((route) => route.page || route.endpoint || route.leaf) + ?.filter(is_app_route) .map((route) => s({ id: route.id })) .join(',\n') ?? '' );