From 0096db2c01a35607c828ed7624e550b992a40bc2 Mon Sep 17 00:00:00 2001 From: yuki0418 Date: Sun, 27 Jul 2025 22:43:25 +0900 Subject: [PATCH 1/2] fix: add support for trailing slashes in typed routes --- .changeset/curvy-bugs-build.md | 5 +++++ packages/kit/src/core/sync/write_types/index.js | 13 ++++++++++++- .../core/sync/write_types/test/app-types/+page.ts | 3 +++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/curvy-bugs-build.md diff --git a/.changeset/curvy-bugs-build.md b/.changeset/curvy-bugs-build.md new file mode 100644 index 000000000000..4fb178ce47cc --- /dev/null +++ b/.changeset/curvy-bugs-build.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: add trailing slash pathname when generating typed routes diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index d51c60a94442..ba3f88d34056 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -82,10 +82,21 @@ export function write_all_types(config, manifest_data) { dynamic_routes.push(route_type); const pathname = remove_group_segments(route.id); - pathnames.add(`\`${replace_required_params(replace_optional_params(pathname))}\` & {}`); + const replaced_pathname = replace_required_params(replace_optional_params(pathname)); + pathnames.add(`\`${replaced_pathname}\` & {}`); + + if (pathname !== '/') { + // Support trailing slash + pathnames.add(`\`${replaced_pathname + '/'}\` & {}`); + } } else { const pathname = remove_group_segments(route.id); pathnames.add(s(pathname)); + + if (pathname !== '/') { + // Support trailing slash + pathnames.add(s(pathname + '/')); + } } /** @type {Map} */ diff --git a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts b/packages/kit/src/core/sync/write_types/test/app-types/+page.ts index 09dea2ca9d96..7da8f0ee9b6a 100644 --- a/packages/kit/src/core/sync/write_types/test/app-types/+page.ts +++ b/packages/kit/src/core/sync/write_types/test/app-types/+page.ts @@ -26,9 +26,12 @@ declare let pathname: Pathname; pathname = '/nope'; pathname = '/foo'; pathname = '/foo/1/2'; +pathname = '/foo/'; +pathname = '/foo/1/2/'; // Test layout groups pathname = '/path-a'; +pathname = '/path-a/'; // @ts-expect-error layout group names are NOT part of the pathname type pathname = '/(group)/path-a'; From dc85fde060cfb21154d79ac12f851c2e6445af50 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 20 Aug 2025 14:40:47 -0400 Subject: [PATCH 2/2] fix merge --- .../kit/src/core/sync/write_non_ambient.js | 13 +++- .../kit/src/core/sync/write_types/index.js | 72 ------------------- 2 files changed, 12 insertions(+), 73 deletions(-) diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index df55a3cbee0d..c4161720879a 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -66,10 +66,21 @@ function generate_app_types(manifest_data) { dynamic_routes.push(route_type); const pathname = remove_group_segments(route.id); - pathnames.add(`\`${replace_required_params(replace_optional_params(pathname))}\` & {}`); + const replaced_pathname = replace_required_params(replace_optional_params(pathname)); + pathnames.add(`\`${replaced_pathname}\` & {}`); + + if (pathname !== '/') { + // Support trailing slash + pathnames.add(`\`${replaced_pathname + '/'}\` & {}`); + } } else { const pathname = remove_group_segments(route.id); pathnames.add(s(pathname)); + + if (pathname !== '/') { + // Support trailing slash + pathnames.add(s(pathname + '/')); + } } /** @type {Map} */ diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js index ea794a8542b9..a7f48109548d 100644 --- a/packages/kit/src/core/sync/write_types/index.js +++ b/packages/kit/src/core/sync/write_types/index.js @@ -54,78 +54,6 @@ export function write_all_types(config, manifest_data) { } } - /** @type {Set} */ - const pathnames = new Set(); - - /** @type {string[]} */ - const dynamic_routes = []; - - /** @type {string[]} */ - const layouts = []; - - for (const route of manifest_data.routes) { - if (route.params.length > 0) { - const params = route.params.map((p) => `${p.name}${p.optional ? '?:' : ':'} string`); - const route_type = `${s(route.id)}: { ${params.join('; ')} }`; - - dynamic_routes.push(route_type); - - const pathname = remove_group_segments(route.id); - const replaced_pathname = replace_required_params(replace_optional_params(pathname)); - pathnames.add(`\`${replaced_pathname}\` & {}`); - - if (pathname !== '/') { - // Support trailing slash - pathnames.add(`\`${replaced_pathname + '/'}\` & {}`); - } - } else { - const pathname = remove_group_segments(route.id); - pathnames.add(s(pathname)); - - if (pathname !== '/') { - // Support trailing slash - pathnames.add(s(pathname + '/')); - } - } - - /** @type {Map} */ - const child_params = new Map(route.params.map((p) => [p.name, p.optional])); - - for (const child of manifest_data.routes.filter((r) => r.id.startsWith(route.id))) { - for (const p of child.params) { - if (!child_params.has(p.name)) { - child_params.set(p.name, true); // always optional - } - } - } - - const layout_params = Array.from(child_params) - .map(([name, optional]) => `${name}${optional ? '?:' : ':'} string`) - .join('; '); - - const layout_type = `${s(route.id)}: ${layout_params.length > 0 ? `{ ${layout_params} }` : 'undefined'}`; - layouts.push(layout_type); - } - - try { - fs.mkdirSync(types_dir, { recursive: true }); - } catch {} - - fs.writeFileSync( - `${types_dir}/index.d.ts`, - [ - `type DynamicRoutes = {\n\t${dynamic_routes.join(';\n\t')}\n};`, - `type Layouts = {\n\t${layouts.join(';\n\t')}\n};`, - // we enumerate these rather than doing `keyof Routes` so that the list is visible on hover - `export type RouteId = ${manifest_data.routes.map((r) => s(r.id)).join(' | ')};`, - 'export type RouteParams = T extends keyof DynamicRoutes ? DynamicRoutes[T] : Record;', - 'export type LayoutParams = Layouts[T] | Record;', - `export type Pathname = ${Array.from(pathnames).join(' | ')};`, - 'export type ResolvedPathname = `${"" | `/${string}`}${Pathname}`;', - `export type Asset = ${manifest_data.assets.map((asset) => s('/' + asset.file)).join(' | ') || 'never'};` - ].join('\n\n') - ); - // Read/write meta data on each invocation, not once per node process, // it could be invoked by another process in the meantime. const meta_data_file = `${types_dir}/route_meta_data.json`;