Skip to content

Commit c807486

Browse files
authored
fix(unplugin): apply definePage path-param parser overrides (#2699)
1 parent b6346bd commit c807486

3 files changed

Lines changed: 105 additions & 32 deletions

File tree

packages/router/src/unplugin/core/customBlock.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ export interface CustomRouteBlock extends Partial<
2929
alias?: string[]
3030

3131
params?: {
32-
path?: Record<string, string>
32+
/**
33+
* Override the parser for a given path param. Set to `null` to remove a
34+
* filename-based parser (e.g. revert `[id=int]` back to no parser).
35+
*/
36+
path?: Record<string, string | null>
3337

38+
/**
39+
* Declare query params for the route. The value is either a parser name
40+
* or an options object with `parser`, `format`, `default`, and `required`.
41+
*/
3442
query?: Record<string, string | CustomRouteBlockQueryParamOptions>
3543
}
3644
}

packages/router/src/unplugin/core/tree.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,63 @@ describe('Tree', () => {
12001200
})
12011201
})
12021202

1203+
describe('Path param parsers from definePage', () => {
1204+
it('applies a parser from definePage to a plain path param', () => {
1205+
const tree = new PrefixTree(RESOLVED_OPTIONS)
1206+
const node = tree.insert('events/[when]', 'events/[when].vue')
1207+
1208+
node.setCustomRouteBlock('events/[when].vue', {
1209+
params: { path: { when: 'date' } },
1210+
})
1211+
1212+
expect(node.pathParams).toEqual([
1213+
expect.objectContaining({ paramName: 'when', parser: 'date' }),
1214+
])
1215+
})
1216+
1217+
it('definePage path parser overrides the filename parser', () => {
1218+
const tree = new PrefixTree(RESOLVED_OPTIONS)
1219+
const node = tree.insert('events/[when=int]', 'events/[when=int].vue')
1220+
1221+
node.setCustomRouteBlock('events/[when=int].vue', {
1222+
params: { path: { when: 'date' } },
1223+
})
1224+
1225+
expect(node.pathParams[0]).toMatchObject({
1226+
paramName: 'when',
1227+
parser: 'date',
1228+
})
1229+
})
1230+
1231+
it('leaves untouched path params when override only mentions some', () => {
1232+
const tree = new PrefixTree(RESOLVED_OPTIONS)
1233+
const node = tree.insert('[a]-[b=int]', '[a]-[b=int].vue')
1234+
1235+
node.setCustomRouteBlock('[a]-[b=int].vue', {
1236+
params: { path: { a: 'date' } },
1237+
})
1238+
1239+
expect(node.pathParams).toEqual([
1240+
expect.objectContaining({ paramName: 'a', parser: 'date' }),
1241+
expect.objectContaining({ paramName: 'b', parser: 'int' }),
1242+
])
1243+
})
1244+
1245+
it('removes the filename parser when the override is null', () => {
1246+
const tree = new PrefixTree(RESOLVED_OPTIONS)
1247+
const node = tree.insert('events/[when=int]', 'events/[when=int].vue')
1248+
1249+
node.setCustomRouteBlock('events/[when=int].vue', {
1250+
params: { path: { when: null } },
1251+
})
1252+
1253+
expect(node.pathParams[0]).toMatchObject({
1254+
paramName: 'when',
1255+
parser: null,
1256+
})
1257+
})
1258+
})
1259+
12031260
describe('_parent convention', () => {
12041261
it('handles _parent.vue as parent component', () => {
12051262
const tree = new PrefixTree(RESOLVED_OPTIONS)

packages/router/src/unplugin/core/treeNodeValue.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,19 @@
1-
import type {
2-
CustomRouteBlock,
3-
CustomRouteBlockQueryParamOptions,
4-
} from './customBlock'
1+
import type { CustomRouteBlock } from './customBlock'
52
import { joinPath, mergeRouteRecordOverride, warn } from './utils'
63
import { encodePath } from '../utils/encoding'
7-
import type { RouteRecordRaw } from '../../types'
84

95
export const enum TreeNodeType {
106
static,
117
group,
128
param,
139
}
1410

15-
export interface RouteRecordOverride extends Partial<
16-
Pick<RouteRecordRaw, 'meta' | 'props' | 'path'>
17-
> {
18-
name?: string | undefined | false
19-
20-
/**
21-
* Path aliases.
22-
*/
23-
alias?: string[]
24-
25-
/**
26-
* Param Parsers information.
27-
*/
28-
params?: {
29-
path?: Record<string, string>
30-
31-
query?: Record<string, string | RouteRecordOverrideQueryParamOptions>
32-
}
33-
}
34-
35-
export interface RouteRecordOverrideQueryParamOptions extends CustomRouteBlockQueryParamOptions {
36-
default?: string
37-
required?: boolean
38-
}
11+
/**
12+
* Internal merged-overrides shape used by tree nodes. Same structure as
13+
* {@link CustomRouteBlock} (the user-facing `<route>` block / `definePage()`
14+
* payload).
15+
*/
16+
export type RouteRecordOverride = CustomRouteBlock
3917

4018
export type SubSegment = string | TreePathParam
4119

@@ -221,7 +199,6 @@ class _TreeNodeValueBase {
221199
*/
222200
removeOverride(key: keyof CustomRouteBlock) {
223201
for (const [_filePath, routeBlock] of this._overrides) {
224-
// @ts-expect-error
225202
delete routeBlock[key]
226203
}
227204
}
@@ -402,16 +379,47 @@ export const escapeRegex = (str: string): string =>
402379
export class TreeNodeValueParam extends _TreeNodeValueBase {
403380
override _type: TreeNodeType.param = TreeNodeType.param
404381

382+
/**
383+
* @param rawSegment The raw segment as defined by the file structure, e.g.
384+
* `[id]`, `prefix-[param]-end`, etc.
385+
*
386+
* @param parent The parent node in the tree, if any.
387+
*
388+
* @param filenamePathParams Path params parsed from the file segment
389+
* (filename convention). The public `pathParams` getter overlays
390+
* `definePage()` parser overrides on top of these.
391+
*
392+
* @param pathSegment The transformed version of the segment into a
393+
* vue-router path, e.g. `:id`, `prefix-:param-end`, etc.
394+
*
395+
* @param subSegments Array of sub segments. This is usually one single
396+
* element but can have more for paths like `prefix-[param]-end.vue`.
397+
*/
405398
constructor(
406399
rawSegment: string,
407400
parent: TreeNodeValue | undefined,
408-
public pathParams: TreePathParam[],
401+
private filenamePathParams: TreePathParam[],
409402
pathSegment: string,
410403
subSegments: SubSegment[]
411404
) {
412405
super(rawSegment, parent, pathSegment, subSegments)
413406
}
414407

408+
/**
409+
* Path params for this node, with `definePage({ params: { path: ... } })`
410+
* parser overrides applied on top of the filename-based parsers.
411+
*/
412+
get pathParams(): TreePathParam[] {
413+
const overridePath = this.overrides.params?.path
414+
if (!overridePath) return this.filenamePathParams
415+
return this.filenamePathParams.map(p =>
416+
// an explicit `null` override removes the filename-based parser
417+
overridePath[p.paramName] !== undefined
418+
? { ...p, parser: overridePath[p.paramName] }
419+
: p
420+
)
421+
}
422+
415423
// Calculate score for each subsegment to handle mixed static/param parts
416424
get score(): number[] {
417425
return this.subSegments.map(segment => {

0 commit comments

Comments
 (0)