Skip to content

Commit 195e9ac

Browse files
elliott-with-the-longest-name-on-githubRich-HarrisRich Harris
authored
feat: add resolvePath export (#9949)
* feat: add `resolvePath` export for combining route IDs and params into a relative path * changeset * Update packages/kit/src/exports/index.js Co-authored-by: Rich Harris <[email protected]> * chore: Move things * add resolvePath to types/index.d.ts --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 35eafa9 commit 195e9ac

File tree

7 files changed

+123
-93
lines changed

7 files changed

+123
-93
lines changed
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: add `resolvePath` export for building relative paths from route IDs and parameters

packages/kit/src/core/postbuild/analyse.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { load_config } from '../config/index.js';
1212
import { forked } from '../../utils/fork.js';
1313
import { should_polyfill } from '../../utils/platform.js';
1414
import { installPolyfills } from '../../exports/node/polyfills.js';
15-
import { resolve_entry } from '../../utils/routing.js';
15+
import { resolvePath } from '../../exports/index.js';
1616

1717
export default forked(import.meta.url, analyse);
1818

@@ -145,7 +145,7 @@ async function analyse({ manifest_path, env }) {
145145
},
146146
prerender,
147147
entries:
148-
entries && (await entries()).map((entry_object) => resolve_entry(route.id, entry_object))
148+
entries && (await entries()).map((entry_object) => resolvePath(route.id, entry_object))
149149
});
150150
}
151151

packages/kit/src/exports/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { HttpError, Redirect, ActionFailure } from '../runtime/control.js';
22
import { BROWSER, DEV } from 'esm-env';
3+
import { get_route_segments } from '../utils/routing.js';
34

45
// For some reason we need to type the params as well here,
56
// JSdoc doesn't seem to like @type with function overloads
@@ -72,3 +73,48 @@ export function text(body, init) {
7273
export function fail(status, data) {
7374
return new ActionFailure(status, data);
7475
}
76+
77+
const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
78+
79+
/**
80+
* Populate a route ID with params to resolve a pathname.
81+
* @example
82+
* ```js
83+
* resolvePath(
84+
* `/blog/[slug]/[...somethingElse]`,
85+
* {
86+
* slug: 'hello-world',
87+
* somethingElse: 'something/else'
88+
* }
89+
* ); // `/blog/hello-world/something/else`
90+
* ```
91+
* @param {string} id
92+
* @param {Record<string, string | undefined>} params
93+
* @returns {string}
94+
*/
95+
export function resolvePath(id, params) {
96+
const segments = get_route_segments(id);
97+
return (
98+
'/' +
99+
segments
100+
.map((segment) =>
101+
segment.replace(basic_param_pattern, (_, optional, name) => {
102+
const param_value = params[name];
103+
104+
// This is nested so TS correctly narrows the type
105+
if (!param_value) {
106+
if (optional) return '';
107+
throw new Error(`Missing parameter '${name}' in route ${id}`);
108+
}
109+
110+
if (param_value.startsWith('/') || param_value.endsWith('/'))
111+
throw new Error(
112+
`Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar`
113+
);
114+
return param_value;
115+
})
116+
)
117+
.filter(Boolean)
118+
.join('/')
119+
);
120+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { assert, expect, test } from 'vitest';
2+
import { resolvePath } from './index.js';
3+
4+
const from_params_tests = [
5+
{
6+
route: '/blog/[one]/[two]',
7+
params: { one: 'one', two: 'two' },
8+
expected: '/blog/one/two'
9+
},
10+
{
11+
route: '/blog/[one=matcher]/[...two]',
12+
params: { one: 'one', two: 'two/three' },
13+
expected: '/blog/one/two/three'
14+
},
15+
{
16+
route: '/blog/[one=matcher]/[[two]]',
17+
params: { one: 'one' },
18+
expected: '/blog/one'
19+
},
20+
{
21+
route: '/blog/[one]/[two]-and-[three]',
22+
params: { one: 'one', two: '2', three: '3' },
23+
expected: '/blog/one/2-and-3'
24+
},
25+
{
26+
route: '/blog/[one]/[...two]-not-three',
27+
params: { one: 'one', two: 'two/2' },
28+
expected: '/blog/one/two/2-not-three'
29+
}
30+
];
31+
32+
for (const { route, params, expected } of from_params_tests) {
33+
test(`resolvePath generates correct path for ${route}`, () => {
34+
const result = resolvePath(route, params);
35+
assert.equal(result, expected);
36+
});
37+
}
38+
39+
test('resolvePath errors on missing params for required param', () => {
40+
expect(() => resolvePath('/blog/[one]/[two]', { one: 'one' })).toThrow(
41+
"Missing parameter 'two' in route /blog/[one]/[two]"
42+
);
43+
});
44+
45+
test('resolvePath errors on params values starting or ending with slashes', () => {
46+
assert.throws(
47+
() => resolvePath('/blog/[one]/[two]', { one: 'one', two: '/two' }),
48+
"Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar"
49+
);
50+
assert.throws(
51+
() => resolvePath('/blog/[one]/[two]', { one: 'one', two: 'two/' }),
52+
"Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar"
53+
);
54+
});

packages/kit/src/utils/routing.js

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -96,44 +96,6 @@ export function parse_route_id(id) {
9696
return { pattern, params };
9797
}
9898

99-
const basic_param_pattern = /\[(\[)?(?:\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
100-
101-
/**
102-
* Parses a route ID, then resolves it to a path by replacing parameters with actual values from `entry`.
103-
* @param {string} id The route id
104-
* @param {Record<string, string | undefined>} entry The entry meant to populate the route. For example, if the route is `/blog/[slug]`, the entry would be `{ slug: 'hello-world' }`
105-
* @example
106-
* ```js
107-
* resolve_entry(`/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' }); // `/blog/hello-world/something/else`
108-
* ```
109-
*/
110-
export function resolve_entry(id, entry) {
111-
const segments = get_route_segments(id);
112-
return (
113-
'/' +
114-
segments
115-
.map((segment) =>
116-
segment.replace(basic_param_pattern, (_, optional, name) => {
117-
const param_value = entry[name];
118-
119-
// This is nested so TS correctly narrows the type
120-
if (!param_value) {
121-
if (optional) return '';
122-
throw new Error(`Missing parameter '${name}' in route ${id}`);
123-
}
124-
125-
if (param_value.startsWith('/') || param_value.endsWith('/'))
126-
throw new Error(
127-
`Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar`
128-
);
129-
return param_value;
130-
})
131-
)
132-
.filter(Boolean)
133-
.join('/')
134-
);
135-
}
136-
13799
const optional_param_regex = /\/\[\[\w+?(?:=\w+)?\]\]/;
138100

139101
/**

packages/kit/src/utils/routing.spec.js

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert, expect, test } from 'vitest';
2-
import { exec, parse_route_id, resolve_entry } from './routing.js';
2+
import { exec, parse_route_id } from './routing.js';
33

44
const tests = {
55
'/': {
@@ -221,55 +221,3 @@ test('parse_route_id errors on bad param name', () => {
221221
assert.throws(() => parse_route_id('abc/[b-c]'), /Invalid param: b-c/);
222222
assert.throws(() => parse_route_id('abc/[bc=d-e]'), /Invalid param: bc=d-e/);
223223
});
224-
225-
const from_entry_tests = [
226-
{
227-
route: '/blog/[one]/[two]',
228-
entry: { one: 'one', two: 'two' },
229-
expected: '/blog/one/two'
230-
},
231-
{
232-
route: '/blog/[one=matcher]/[...two]',
233-
entry: { one: 'one', two: 'two/three' },
234-
expected: '/blog/one/two/three'
235-
},
236-
{
237-
route: '/blog/[one=matcher]/[[two]]',
238-
entry: { one: 'one' },
239-
expected: '/blog/one'
240-
},
241-
{
242-
route: '/blog/[one]/[two]-and-[three]',
243-
entry: { one: 'one', two: '2', three: '3' },
244-
expected: '/blog/one/2-and-3'
245-
},
246-
{
247-
route: '/blog/[one]/[...two]-not-three',
248-
entry: { one: 'one', two: 'two/2' },
249-
expected: '/blog/one/two/2-not-three'
250-
}
251-
];
252-
253-
for (const { route, entry, expected } of from_entry_tests) {
254-
test(`resolve_entry generates correct path for ${route}`, () => {
255-
const result = resolve_entry(route, entry);
256-
assert.equal(result, expected);
257-
});
258-
}
259-
260-
test('resolve_entry errors on missing entry for required param', () => {
261-
expect(() => resolve_entry('/blog/[one]/[two]', { one: 'one' })).toThrow(
262-
"Missing parameter 'two' in route /blog/[one]/[two]"
263-
);
264-
});
265-
266-
test('resolve_entry errors on entry values starting or ending with slashes', () => {
267-
assert.throws(
268-
() => resolve_entry('/blog/[one]/[two]', { one: 'one', two: '/two' }),
269-
"Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar"
270-
);
271-
assert.throws(
272-
() => resolve_entry('/blog/[one]/[two]', { one: 'one', two: 'two/' }),
273-
"Parameter 'two' in route /blog/[one]/[two] cannot start or end with a slash -- this would cause an invalid route like foo//bar"
274-
);
275-
});

packages/kit/types/index.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,3 +1310,18 @@ export interface Snapshot<T = any> {
13101310
capture: () => T;
13111311
restore: (snapshot: T) => void;
13121312
}
1313+
1314+
/**
1315+
* Populate a route ID with params to resolve a pathname.
1316+
* @example
1317+
* ```js
1318+
* resolvePath(
1319+
* `/blog/[slug]/[...somethingElse]`,
1320+
* {
1321+
* slug: 'hello-world',
1322+
* somethingElse: 'something/else'
1323+
* }
1324+
* ); // `/blog/hello-world/something/else`
1325+
* ```
1326+
*/
1327+
export function resolvePath(id: string, params: Record<string, string | undefined>): string;

0 commit comments

Comments
 (0)