Skip to content

Feat/resolve route imp #11406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sweet-apricots-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

feat: add types to `resolveRoute` (id & params)
2 changes: 1 addition & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@types/node": "^18.19.3",
"@types/sade": "^1.7.8",
"@types/set-cookie-parser": "^2.4.7",
"dts-buddy": "^0.4.3",
"dts-buddy": "^0.4.4",
"rollup": "^4.8.0",
"svelte": "^4.2.8",
"svelte-preprocess": "^5.1.2",
Expand Down
7 changes: 6 additions & 1 deletion packages/kit/scripts/generate-dts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createBundle } from 'dts-buddy';

createBundle({
compilerOptions: {
paths: {
$types: []
}
},
output: 'types/index.d.ts',
modules: {
'@sveltejs/kit': 'src/exports/public.d.ts',
Expand All @@ -11,7 +16,7 @@ createBundle({
'$app/environment': 'src/runtime/app/environment.js',
'$app/forms': 'src/runtime/app/forms.js',
'$app/navigation': 'src/runtime/app/navigation.js',
'$app/paths': 'src/runtime/app/paths.js',
'$app/paths': 'src/runtime/app/paths/types.d.ts',
'$app/stores': 'src/runtime/app/stores.js'
},
include: ['src']
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_tsconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function get_tsconfig(kit) {
const include = new Set([
'ambient.d.ts',
'non-ambient.d.ts',
'./types/route_ids.d.ts',
'./types/**/$types.d.ts',
config_relative('vite.config.js'),
config_relative('vite.config.ts')
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_tsconfig.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ test('Creates tsconfig include from kit.files', () => {
expect(include).toEqual([
'ambient.d.ts',
'non-ambient.d.ts',
'./types/route_ids.d.ts',
'./types/**/$types.d.ts',
'../vite.config.js',
'../vite.config.ts',
Expand Down
26 changes: 26 additions & 0 deletions packages/kit/src/core/sync/write_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ export async function write_all_types(config, manifest_data) {
}
}

/** @type {string[]} */
const route_ids = [];
routes_map.forEach((route_info) => {
// defaults to never if no params needed
let params = 'never';

// If we have some params, let's handle them
if (route_info.route.params.length > 0) {
params = `{ ${route_info.route.params
.map((param) => {
return `${param.name}${param.optional ? '?' : ''}: string${param.rest ? '[]' : ''}`;
})
.join(', ')} }`;
}

route_ids.push(`'${route_info.route.id}': ${params}`);
});

fs.writeFileSync(
`${types_dir}/route_ids.d.ts`,
`declare module '$types' {
export type RouteIds = {
${route_ids.join(',\n\t\t')}
};
}`
);
fs.writeFileSync(meta_data_file, JSON.stringify(meta_data, null, '\t'));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { base, assets } from '__sveltekit/paths';
import { base } from '__sveltekit/paths';
import { resolve_route } from '../../utils/routing.js';
import { resolve_route } from '../../../utils/routing.js';

/**
* Populate a route ID with params to resolve a pathname.
Expand All @@ -15,7 +15,7 @@ import { resolve_route } from '../../utils/routing.js';
* ); // `/blog/hello-world/something/else`
* ```
* @param {string} id
* @param {Record<string, string | undefined>} params
* @param {any} [params]
* @returns {string}
*/
export function resolveRoute(id, params) {
Expand Down
30 changes: 30 additions & 0 deletions packages/kit/src/runtime/app/paths/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// These types live here because I can't figure out how to express them with JSDoc

import { RouteIds } from '$types';

// Type utility to extract keys that correspond to routes
type RouteWithParams = {
[K in keyof RouteIds]: RouteIds[K] extends never ? never : K;
}[keyof RouteIds];

type RouteWithoutParams = {
[K in keyof RouteIds]: RouteIds[K] extends never ? K : never;
}[keyof RouteIds];

/**
* Populate a route ID with params to resolve a pathname.
* @example
* ```js
* resolveRoute(
* `/blog/[slug]/[...somethingElse]`,
* {
* slug: 'hello-world',
* somethingElse: 'something/else'
* }
* ); // `/blog/hello-world/something/else`
* ```
*/
export function resolveRoute<K extends RouteWithParams>(id: K, params: RouteIds[K]): string;
export function resolveRoute<K extends RouteWithoutParams>(id: K): string;

export { base, assets } from '__sveltekit/paths';
4 changes: 4 additions & 0 deletions packages/kit/src/types/generated.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// this file is a placeholder `$types` module that exists purely for typechecking the codebase.
// in actual use, the `$types` module will be declared in an ambient module generated by
// SvelteKit into `.svelte-kit/types`
export interface RouteIds {}
4 changes: 4 additions & 0 deletions packages/kit/test/apps/basics/src/params/numeric.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @param {string} param
* @returns {param is number}
*/
export function match(param) {
return !isNaN(parseInt(param));
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
<a href="/routing/matched/a">/routing/matched/a</a>
<a href="/routing/matched/B">/routing/matched/B</a>
<a href="/routing/matched/1">/routing/matched/1</a>
<a href="/routing/matched/everything-else">/routing/matched/everything-else</a>
<script lang="ts">
import { resolveRoute } from '$app/paths';
</script>

<!-- <a href="/routing/matched"> 🤞 matched </a> -->
<!-- <a href={resolveRoute('/rooting/matched')}> ❌ matched </a> -->
<a href={resolveRoute('/routing/matched')}> ✅ matched </a>

<!-- <a href="/routing/matched/a"> 🤞 lowercase </a> -->
<!-- <a href={resolveRoute('/routing/matched/[letter=lowercase]', { letter: true })}> ❌ lowercase </a> -->
<a href={resolveRoute('/routing/matched/[letter=lowercase]', { letter: 'a' })}> ✅ lowercase </a>

<!-- <a href="/routing/matched/B">/routing/matched/B</a> -->
<a href={resolveRoute('/routing/matched/[letter=uppercase]', { letter: 'B' })}>uppercase</a>
<!-- <a href="/routing/matched/1">/routing/matched/1</a> -->
<a href={resolveRoute('/routing/matched/[number=numeric]', { number: '1' })}> numeric </a>
<!-- <a href="/routing/matched/everything-else">/routing/matched/everything-else</a> -->
<a href={resolveRoute('/routing/matched/[fallback]', { fallback: 'everything-else' })}>
fallback
</a>
<a href={resolveRoute('/routing/matched/[[optional]]/withOption', { optional: 'sziaaa' })}>
optional
</a>

<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import { page } from '$app/stores';
</script>

<h1>with option: {$page.params.optional}</h1>
11 changes: 7 additions & 4 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -881,17 +881,20 @@ test.describe('Matchers', () => {
test('Matches parameters', async ({ page, clicknav }) => {
await page.goto('/routing/matched');

await clicknav('[href="/routing/matched/a"]');
await clicknav('[href*="/routing/matched/a"]');
expect(await page.textContent('h1')).toBe('lowercase: a');

await clicknav('[href="/routing/matched/B"]');
await clicknav('[href*="/routing/matched/B"]');
expect(await page.textContent('h1')).toBe('uppercase: B');

await clicknav('[href="/routing/matched/1"]');
await clicknav('[href*="/routing/matched/1"]');
expect(await page.textContent('h1')).toBe('number: 1');

await clicknav('[href="/routing/matched/everything-else"]');
await clicknav('[href*="/routing/matched/everything-else"]');
expect(await page.textContent('h1')).toBe('fallback: everything-else');

await clicknav('[href*="/routing/matched/sziaaa/withOption"]');
expect(await page.textContent('h1')).toBe('with option: sziaaa');
});
});

Expand Down
3 changes: 2 additions & 1 deletion packages/kit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"@sveltejs/kit": ["./src/exports/public.d.ts"],
"@sveltejs/kit/node": ["./src/exports/node/index.js"],
// internal use only
"types": ["./src/types/internal.d.ts"]
"types": ["./src/types/internal.d.ts"],
"$types": ["./src/types/generated.d.ts"]
},
"noUnusedLocals": true,
"noUnusedParameters": true
Expand Down
15 changes: 13 additions & 2 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2048,7 +2048,17 @@ declare module '$app/navigation' {
}

declare module '$app/paths' {
import type { RouteIds } from '$types';
export { base, assets } from '__sveltekit/paths';
// Type utility to extract keys that correspond to routes
type RouteWithParams = {
[K in keyof RouteIds]: RouteIds[K] extends never ? never : K;
}[keyof RouteIds];

type RouteWithoutParams = {
[K in keyof RouteIds]: RouteIds[K] extends never ? K : never;
}[keyof RouteIds];

/**
* Populate a route ID with params to resolve a pathname.
* @example
Expand All @@ -2061,8 +2071,9 @@ declare module '$app/paths' {
* }
* ); // `/blog/hello-world/something/else`
* ```
* */
export function resolveRoute(id: string, params: Record<string, string | undefined>): string;
*/
export function resolveRoute<K extends RouteWithParams>(id: K, params: RouteIds[K]): string;
export function resolveRoute<K extends RouteWithoutParams>(id: K): string;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint is not passing because I wrote the type here directly. It seems that we have to do it in JSDoc, but I don't manage to do it.
I would need some guidance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too am somewhat flummoxed by JSDoc in this case. For now I've put this stuff in a src .d.ts file, which sort of works, though there are some other problems that need solving separately.

I took the liberty of moving the RouteId type to $types rather than $app/paths — it's arguably more consistent with the various ./$types files, and it'll be convenient to have a single place to put all the generated types.

Right now it doesn't seem like that ambient declaration is getting picked up — working on it...


declare module '$app/stores' {
Expand Down
Empty file.
Loading