Skip to content

Commit 737d119

Browse files
elliott-with-the-longest-name-on-githubRich-Harrisbenmccannteemingc
authored
feat: allow hyphenated matcher names (#16284)
closes #16277 I don't really see any reason not to do this -- it doesn't even really complicate the internal code. --------- Co-authored-by: Rich Harris <rich.harris@vercel.com> Co-authored-by: Rich Harris <richard.a.harris@gmail.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Tee Ming Chew <chewteeming01@gmail.com>
1 parent f29ea06 commit 737d119

9 files changed

Lines changed: 117 additions & 10 deletions

File tree

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: allow hyphens in param and matcher names

packages/kit/src/core/sync/create_manifest_data/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
142142
throw new Error(`Route ${id} should be renamed to ${id.replace(/#/g, '[x+23]')}`);
143143
}
144144

145-
if (/\[\.\.\.\w+\]\/\[\[/.test(id)) {
145+
if (/\[\.\.\.[\w-]+\]\/\[\[/.test(id)) {
146146
throw new Error(
147147
`Invalid route ${id} — an [[optional]] route segment cannot follow a [...rest] route segment`
148148
);

packages/kit/src/core/sync/write_non_ambient.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function generate_app_types(manifest_data, config) {
209209
if (route.params.length > 0) {
210210
const params = route.params.map((p) => {
211211
const type = get_matcher_type(p.matcher);
212-
return `${p.name}${p.optional ? '?:' : ':'} ${type}${p.optional ? ' | undefined' : ''}`;
212+
return `${/^\w+$/.test(p.name) ? p.name : `'${p.name}'`}${p.optional ? '?:' : ':'} ${type}${p.optional ? ' | undefined' : ''}`;
213213
});
214214
const route_type = `${s(route.id)}: { ${params.join('; ')} }`;
215215

@@ -230,7 +230,7 @@ function generate_app_types(manifest_data, config) {
230230
const params = Array.from(layout_params)
231231
.map(([name, { optional, matchers }]) => {
232232
const type = get_matchers_type(matchers);
233-
return `${name}${optional ? '?:' : ':'} ${type}${optional ? ' | undefined' : ''}`;
233+
return `${/^\w+$/.test(name) ? name : `'${name}'`}${optional ? '?:' : ':'} ${type}${optional ? ' | undefined' : ''}`;
234234
})
235235
.join('; ');
236236

packages/kit/src/core/sync/write_types/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ function generate_params_type(params, outdir, config) {
607607
return `{ ${params
608608
.map(
609609
(param) =>
610-
`${param.name}${param.optional ? '?' : ''}: ${
610+
`${/^\w+$/.test(param.name) ? param.name : `'${param.name}'`}${param.optional ? '?' : ''}: ${
611611
param.matcher
612612
? `import('@sveltejs/kit').MatcherParam<(typeof import('${params_import}').params)[${JSON.stringify(param.matcher)}]>`
613613
: 'string'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../../.svelte-kit/types/optional/[[optional-hyphen-param=hyphenated-matcher]]/$types').PageLoad} */
4+
export function load({ params }) {
5+
if (params['optional-hyphen-param']) {
6+
/** @type {"a" | "b"} */
7+
let a;
8+
a = params['optional-hyphen-param'];
9+
return { a };
10+
}
11+
}

packages/kit/src/core/sync/write_types/test/param-type-inference/params.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export const params = defineParams({
66
if (!['a', 'b'].includes(param)) return;
77
return /** @type {'a' | 'b'} */ (param);
88
},
9+
'hyphenated-matcher': (param) => {
10+
if (!['a', 'b'].includes(param)) return;
11+
return /** @type {'a' | 'b'} */ (param);
12+
},
913
boolean: () => /** @type {boolean} */ (true),
1014
number: v.pipe(v.string(), v.toNumber())
1115
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* eslint-disable */
2+
3+
/** @type {import('../../.svelte-kit/types/required/[hyphen-param=hyphenated-matcher]/$types').PageLoad} */
4+
export function load({ params }) {
5+
/** @type {"a" | "b"} */
6+
let a;
7+
a = params['hyphen-param'];
8+
}

packages/kit/src/utils/routing.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BROWSER } from 'esm-env';
22

3-
const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
3+
const param_pattern = /^(\[)?(\.\.\.)?([\w-]+)(?:=([\w-]+))?(\])?$/;
44

55
const root_group_pattern = /^\/\((?:[^)]+)\)$/;
66

@@ -19,7 +19,7 @@ export function parse_route_id(id) {
1919
`^${get_route_segments(id)
2020
.map((segment) => {
2121
// special case — /[...rest]/ could contain zero segments
22-
const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
22+
const rest_match = /^\[\.\.\.([\w-]+)(?:=([\w-]+))?\]$/.exec(segment);
2323
if (rest_match) {
2424
params.push({
2525
name: rest_match[1],
@@ -31,7 +31,7 @@ export function parse_route_id(id) {
3131
return '(?:/([^]*))?';
3232
}
3333
// special case — /[[optional]]/ could contain zero segments
34-
const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(segment);
34+
const optional_match = /^\[\[([\w-]+)(?:=([\w-]+))?\]\]$/.exec(segment);
3535
if (optional_match) {
3636
params.push({
3737
name: optional_match[1],
@@ -72,7 +72,7 @@ export function parse_route_id(id) {
7272
const match = /** @type {RegExpExecArray} */ (param_pattern.exec(content));
7373
if (!BROWSER && !match) {
7474
throw new Error(
75-
`Invalid param: ${content}. Params and matcher names can only have underscores and alphanumeric characters.`
75+
`Invalid param: ${content}. Params and matcher names can only have underscores, hyphens, and alphanumeric characters.`
7676
);
7777
}
7878
@@ -103,7 +103,7 @@ export function parse_route_id(id) {
103103
return { pattern, params };
104104
}
105105

106-
const optional_param_regex = /\/\[\[\w+?(?:=\w+)?\]\]/;
106+
const optional_param_regex = /\/\[\[[\w-]+?(?:=[\w-]+)?\]\]/;
107107

108108
/**
109109
* Removes optional params from a route ID.
@@ -260,7 +260,7 @@ function escape(str) {
260260
);
261261
}
262262

263-
const basic_param_pattern = /\[(\[)?(\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
263+
const basic_param_pattern = /\[(\[)?(\.\.\.)?([\w-]+?)(?:=([\w-]+))?\]\]?/g;
264264

265265
/**
266266
* Populate a route ID with params to resolve a pathname.

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,48 @@ describe('parse_route_id', () => {
5555
'/@-symbol/[id]': {
5656
pattern: /^\/@-symbol\/([^/]+?)\/?$/,
5757
params: [{ name: 'id', matcher: undefined, optional: false, rest: false, chained: false }]
58+
},
59+
'/blog/[page-slug]': {
60+
pattern: /^\/blog\/([^/]+?)\/?$/,
61+
params: [
62+
{ name: 'page-slug', matcher: undefined, optional: false, rest: false, chained: false }
63+
]
64+
},
65+
'/blog/[page-slug=positive-integer]': {
66+
pattern: /^\/blog\/([^/]+?)\/?$/,
67+
params: [
68+
{
69+
name: 'page-slug',
70+
matcher: 'positive-integer',
71+
optional: false,
72+
rest: false,
73+
chained: false
74+
}
75+
]
76+
},
77+
'/blog/[[page-slug=positive-integer]]/sub': {
78+
pattern: /^\/blog(?:\/([^/]+))?\/sub\/?$/,
79+
params: [
80+
{
81+
name: 'page-slug',
82+
matcher: 'positive-integer',
83+
optional: true,
84+
rest: false,
85+
chained: true
86+
}
87+
]
88+
},
89+
'/[...catch-all]': {
90+
pattern: /^(?:\/([^]*))?\/?$/,
91+
params: [
92+
{ name: 'catch-all', matcher: undefined, optional: false, rest: true, chained: true }
93+
]
94+
},
95+
'/[...catch-all=some-matcher]': {
96+
pattern: /^(?:\/([^]*))?\/?$/,
97+
params: [
98+
{ name: 'catch-all', matcher: 'some-matcher', optional: false, rest: true, chained: true }
99+
]
58100
}
59101
};
60102

@@ -404,6 +446,21 @@ describe('resolve_route', () => {
404446
route: '/blog/[one]/[...two]-not-three/',
405447
params: { one: 'one', two: 'two/2' },
406448
expected: '/blog/one/two/2-not-three/'
449+
},
450+
{
451+
route: '/blog/[page-slug]',
452+
params: { 'page-slug': 'hello' },
453+
expected: '/blog/hello'
454+
},
455+
{
456+
route: '/blog/[page-slug=positive-integer]',
457+
params: { 'page-slug': '42' },
458+
expected: '/blog/42'
459+
},
460+
{
461+
route: '/[...catch-all=some-matcher]',
462+
params: { 'catch-all': 'a/b' },
463+
expected: '/a/b'
407464
}
408465
];
409466

@@ -420,6 +477,12 @@ describe('resolve_route', () => {
420477
);
421478
});
422479

480+
test('resolvePath errors on missing params for required param with hyphenated name', () => {
481+
expect(() => resolve_route('/blog/[page-slug]', {})).toThrow(
482+
"Missing parameter 'page-slug' in route /blog/[page-slug]"
483+
);
484+
});
485+
423486
test('resolvePath errors on params values starting or ending with slashes', () => {
424487
assert.throws(
425488
() => resolve_route('/blog/[one]/[two]', { one: 'one', two: '/two' }),
@@ -532,6 +595,22 @@ describe('find_route', () => {
532595
);
533596
});
534597

598+
test('respects matchers with hyphenated names', () => {
599+
const routes = [create_route('/blog/[slug=positive-integer]'), create_route('/blog/[slug]')];
600+
/** @type {import('@sveltejs/kit').ParamDefinition} */
601+
const positive_integer = (param) => (/^\d+$/.test(param) ? param : undefined);
602+
const matchers = defineParams({ 'positive-integer': positive_integer });
603+
604+
// "42" matches the positive-integer matcher
605+
const result1 = find_route('/blog/42', routes, matchers);
606+
assert.equal(result1?.route.id, '/blog/[slug=positive-integer]');
607+
assert.deepEqual(result1?.params, { slug: '42' });
608+
609+
// "hello" doesn't match, falls through to [slug]
610+
const result2 = find_route('/blog/hello', routes, matchers);
611+
assert.equal(result2?.route.id, '/blog/[slug]');
612+
});
613+
535614
test('decodes params', () => {
536615
const routes = [create_route('/blog/[slug]')];
537616

0 commit comments

Comments
 (0)