Skip to content

Commit 9e9ab44

Browse files
committed
feat: provide match function to allow the opposite of resolve
1 parent 9fda2fc commit 9e9ab44

File tree

10 files changed

+156
-1
lines changed

10 files changed

+156
-1
lines changed

.changeset/new-bugs-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: `match` function to map a path back to a route id and params

packages/kit/src/runtime/app/paths/client.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
/** @import { ResolveArgs } from './types.js' */
33
import { base, assets, hash_routing } from './internal/client.js';
44
import { resolve_route } from '../../../utils/routing.js';
5+
import { decode_params } from '../../../utils/url.js';
6+
import { get_routes } from '../../client/client.js';
57

68
/**
79
* Resolve the URL of an asset in your `static` directory, by prefixing it with [`config.kit.paths.assets`](https://svelte.dev/docs/kit/configuration#paths) if configured, or otherwise by prefixing it with the base path.
@@ -58,4 +60,40 @@ export function resolve(...args) {
5860
);
5961
}
6062

63+
/**
64+
* Match a pathname to a route ID and extracts any parameters.
65+
*
66+
* @example
67+
* ```js
68+
* import { match } from '$app/paths';
69+
*
70+
* const result = await match('/blog/hello-world');
71+
* // → { id: '/blog/[slug]', params: { slug: 'hello-world' } }
72+
*
73+
*
74+
* @param {Pathname} pathname
75+
* @returns {Promise<{ id: RouteId, params: Record<string, string> } | null>}
76+
*/
77+
export async function match(pathname) {
78+
let path = pathname;
79+
80+
if (base && path.startsWith(base)) {
81+
path = path.slice(base.length) || '/';
82+
}
83+
84+
if (hash_routing && path.startsWith('#')) {
85+
path = path.slice(1) || '/';
86+
}
87+
88+
const routes = get_routes();
89+
for (const route of routes) {
90+
const params = route.exec(path);
91+
if (params) {
92+
return { id: /** @type {RouteId} */ (route.id), params: decode_params(params) };
93+
}
94+
}
95+
96+
return null;
97+
}
98+
6199
export { base, assets, resolve as resolveRoute };

packages/kit/src/runtime/app/paths/server.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { base, assets, relative, initial_base } from './internal/server.js';
2-
import { resolve_route } from '../../../utils/routing.js';
2+
import { resolve_route, exec } from '../../../utils/routing.js';
3+
import { decode_params } from '../../../utils/url.js';
34
import { try_get_request_store } from '@sveltejs/kit/internal/server';
5+
import { manifest } from '__sveltekit/server';
46

57
/** @type {import('./client.js').asset} */
68
export function asset(file) {
@@ -27,4 +29,30 @@ export function resolve(id, params) {
2729
return base + resolved;
2830
}
2931

32+
/** @type {import('./client.js').match} */
33+
export async function match(pathname) {
34+
let path = pathname;
35+
36+
if (base && path.startsWith(base)) {
37+
path = path.slice(base.length) || '/';
38+
}
39+
40+
const matchers = await manifest._.matchers();
41+
42+
for (const route of manifest._.routes) {
43+
const match = route.pattern.exec(path);
44+
if (!match) continue;
45+
46+
const matched = exec(match, route.params, matchers);
47+
if (matched) {
48+
return {
49+
id: /** @type {import('$app/types').RouteId} */ (route.id),
50+
params: decode_params(matched)
51+
};
52+
}
53+
}
54+
55+
return null;
56+
}
57+
3058
export { base, assets, resolve as resolveRoute };

packages/kit/src/runtime/client/client.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ let target;
189189
/** @type {import('./types.js').SvelteKitApp} */
190190
export let app;
191191

192+
/**
193+
* Returns the client-side routes array. Used by `$app/paths` for route matching.
194+
* @returns {import('types').CSRRoute[]}
195+
*/
196+
export function get_routes() {
197+
return routes;
198+
}
199+
192200
/**
193201
* Data that was serialized during SSR. This is cleared when the user first navigates
194202
* @type {Record<string, any>}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { match } from '$app/paths';
2+
import { testPaths } from './const';
3+
4+
export async function load() {
5+
const serverResults = await Promise.all(
6+
testPaths.map(async (path) => ({ path, result: await match(path) }))
7+
);
8+
9+
return {
10+
serverResults
11+
};
12+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script>
2+
import { match } from '$app/paths';
3+
import { onMount } from 'svelte';
4+
import { testPaths } from './const';
5+
6+
let { data } = $props();
7+
8+
const clientResults = $state([]);
9+
10+
onMount(async () => {
11+
for (const path of testPaths) {
12+
const result = await match(path);
13+
clientResults.push({ path, result });
14+
}
15+
});
16+
</script>
17+
18+
<h1>Match Test</h1>
19+
20+
<div id="server-results">
21+
{#each data.serverResults as { path, result }}
22+
<div class="result" data-path={path}>
23+
{JSON.stringify(result)}
24+
</div>
25+
{/each}
26+
</div>
27+
28+
<div id="client-results">
29+
{#each clientResults as { path, result }}
30+
<div class="result" data-path={path}>
31+
{JSON.stringify(result)}
32+
</div>
33+
{/each}
34+
</div>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const testPaths = [
2+
'/match/load/foo',
3+
'/match/slug/test-slug',
4+
'/match/not-a-real-route-that-exists'
5+
];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bar
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
slug

packages/kit/test/apps/basics/test/test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,29 @@ test.describe('$app/paths', () => {
766766
javaScriptEnabled ? absolute : '../../../../favicon.png'
767767
);
768768
});
769+
770+
test('match() returns route id and params for matching routes', async ({
771+
page,
772+
javaScriptEnabled
773+
}) => {
774+
await page.goto('/match');
775+
776+
const samples = [
777+
{ path: '/match/load/foo', expected: { id: '/match/load/foo', params: {} } },
778+
{
779+
path: '/match/slug/test-slug',
780+
expected: { id: '/match/slug/[slug]', params: { slug: 'test-slug' } }
781+
},
782+
{ path: '/match/not-a-real-route-that-exists', expected: null }
783+
];
784+
785+
for (const { path, expected } of samples) {
786+
const results = javaScriptEnabled
787+
? page.locator('#client-results')
788+
: page.locator('#server-results');
789+
await expect(results.locator(`[data-path="${path}"]`)).toHaveText(JSON.stringify(expected));
790+
}
791+
});
769792
});
770793

771794
// TODO SvelteKit 3: remove these tests

0 commit comments

Comments
 (0)