Skip to content

Commit 846640e

Browse files
committed
fix(experimental): repeatable params in subsegments
1 parent be37b79 commit 846640e

2 files changed

Lines changed: 51 additions & 11 deletions

File tree

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { MatcherPatternPathStar } from './matcher-pattern-path-star'
77
import { miss } from './errors'
88
import { definePathParamParser } from './param-parsers'
9+
import { mockWarn } from '../../../../__tests__/vitest-mock-warn'
910

1011
describe('MatcherPatternPathStatic', () => {
1112
describe('match()', () => {
@@ -129,6 +130,8 @@ describe('MatcherPatternPathStar', () => {
129130
})
130131

131132
describe('MatcherPatternPathDynamic', () => {
133+
mockWarn()
134+
132135
it('single param', () => {
133136
const pattern = new MatcherPatternPathDynamic(
134137
/^\/teams\/([^/]+?)\/b$/i,
@@ -376,6 +379,48 @@ describe('MatcherPatternPathDynamic', () => {
376379
)
377380
})
378381

382+
it('repeatable param in a sub segment', () => {
383+
// produced by file-based routing for `set.[ids]+.other`
384+
const pattern = new MatcherPatternPathDynamic(
385+
/^\/set\/(.+?)\/other$/,
386+
{
387+
ids: [{}, true],
388+
},
389+
[['set/', 1, '/other']]
390+
)
391+
392+
expect(pattern.match('/set/123/other')).toEqual({ ids: ['123'] })
393+
expect(pattern.match('/set/123/456/other')).toEqual({
394+
ids: ['123', '456'],
395+
})
396+
expect(pattern.build({ ids: ['123'] })).toBe('/set/123/other')
397+
expect(pattern.build({ ids: ['123', '456'] })).toBe('/set/123/456/other')
398+
})
399+
400+
it('multiple repeatable params in a sub segment', () => {
401+
// produced by file-based routing for `before.[ids]+.middle.[other]+.after`
402+
const pattern = new MatcherPatternPathDynamic(
403+
/^\/before\/(.+?)\/middle\/(.+?)\/after$/,
404+
{
405+
ids: [{}, true],
406+
other: [{}, true],
407+
},
408+
[['before/', 1, '/middle/', 1, '/after']]
409+
)
410+
411+
expect(pattern.match('/before/a/middle/c/after')).toEqual({
412+
ids: ['a'],
413+
other: ['c'],
414+
})
415+
expect(pattern.match('/before/a/b/middle/c/d/after')).toEqual({
416+
ids: ['a', 'b'],
417+
other: ['c', 'd'],
418+
})
419+
expect(pattern.build({ ids: ['a', 'b'], other: ['c', 'd'] })).toBe(
420+
'/before/a/b/middle/c/d/after'
421+
)
422+
})
423+
379424
it('can have a trailing slash after a single param', () => {
380425
const pattern = new MatcherPatternPathDynamic(
381426
/^\/teams\/([^/]+?)\/$/i,

packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -272,17 +272,12 @@ export class MatcherPatternPathDynamic<
272272
;[parser, repeatable, optional] = this.params[paramName]
273273
value = (parser?.set || identityFn)(params[paramName])
274274

275-
// param cannot be repeatable when in a sub segment
276-
if (__DEV__ && repeatable) {
277-
warn(
278-
`Param "${String(paramName)}" is repeatable, but used in a sub segment of the path: "${this.pathParts.join('')}". Repeated params can only be used as a full path segment: "/file/[ids]+/something-else". This will break in production.`
279-
)
280-
return Array.isArray(value)
281-
? value.map(encodeParam).join('/')
282-
: encodeParam(value)
283-
}
284-
285-
return encodeParam(value as string | null | undefined)
275+
// a repeatable param in a sub segment joins its values with `/`
276+
// so file-based routes like `rep.[ids]+` (sub segment
277+
// ['rep/', 1]) and `set.[ids]+.other` round-trip correctly
278+
return Array.isArray(value)
279+
? value.map(encodeParam).join('/')
280+
: encodeParam(value as string | null | undefined)
286281
})
287282
.join('')
288283
}

0 commit comments

Comments
 (0)