Skip to content

Commit be37b79

Browse files
committed
feat: allow string as a param parser for convenience
1 parent 27e9bbd commit be37b79

5 files changed

Lines changed: 145 additions & 2 deletions

File tree

packages/router/src/experimental/runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export interface DefinePage<FilePath extends string = string> extends Partial<
113113
export interface ParamParsers_Native {
114114
int: { type: number }
115115
bool: { type: boolean }
116+
string: { type: string }
116117
}
117118

118119
export type ParamParserType_Native = keyof ParamParsers_Native

packages/router/src/unplugin/codegen/generateParamParsers.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('warnMissingParamParsers', () => {
4040
const tree = new PrefixTree(DEFAULT_OPTIONS)
4141
tree.insert('users/[id=int]', 'users/[id=int].vue')
4242
tree.insert('posts/[active=bool]', 'posts/[active=bool].vue')
43+
tree.insert('blog/[slug=string]', 'blog/[slug=string].vue')
4344

4445
const paramParsers: ParamParsersMap = new Map()
4546

@@ -95,6 +96,7 @@ describe('collectMissingParamParsers', () => {
9596
const tree = new PrefixTree(DEFAULT_OPTIONS)
9697
tree.insert('users/[id=int]', 'users/[id=int].vue')
9798
tree.insert('posts/[active=bool]', 'posts/[active=bool].vue')
99+
tree.insert('blog/[slug=string]', 'blog/[slug=string].vue')
98100

99101
const paramParsers: ParamParsersMap = new Map()
100102

@@ -344,11 +346,19 @@ describe('generateParamsTypes', () => {
344346
isSplat: false,
345347
parser: 'bool',
346348
},
349+
{
350+
paramName: 'slug',
351+
modifier: '',
352+
optional: false,
353+
repeatable: false,
354+
isSplat: false,
355+
parser: 'string',
356+
},
347357
]
348358
const paramParsers: ParamParsersMap = new Map()
349359

350360
const result = generateParamsTypes(params, paramParsers)
351-
expect(result).toEqual(['number', 'boolean'])
361+
expect(result).toEqual(['number', 'boolean', 'string'])
352362
})
353363

354364
it('handles mixed params with and without parsers', () => {
@@ -478,6 +488,49 @@ describe('generateParamParserOptions', () => {
478488
)
479489
})
480490

491+
it("returns empty string for native 'string' parser (treated as no parser)", () => {
492+
const param: TreePathParam = {
493+
paramName: 'slug',
494+
modifier: '',
495+
optional: false,
496+
repeatable: false,
497+
isSplat: false,
498+
parser: 'string',
499+
}
500+
const importsMap = new ImportsMap()
501+
const paramParsers: ParamParsersMap = new Map()
502+
503+
const result = generateParamParserOptions(param, importsMap, paramParsers)
504+
expect(result).toBe('')
505+
expect(importsMap.toString()).not.toContain('PARAM_PARSER_STRING')
506+
})
507+
508+
it("lets custom parser named 'string' override the native default", () => {
509+
const param: TreePathParam = {
510+
paramName: 'slug',
511+
modifier: '',
512+
optional: false,
513+
repeatable: false,
514+
isSplat: false,
515+
parser: 'string',
516+
}
517+
const importsMap = new ImportsMap()
518+
const paramParsers: ParamParsersMap = new Map([
519+
[
520+
'string',
521+
{
522+
name: 'string',
523+
typeName: 'Param_string',
524+
relativePath: 'parsers/string',
525+
absolutePath: '/path/to/parsers/string',
526+
},
527+
],
528+
])
529+
530+
const result = generateParamParserOptions(param, importsMap, paramParsers)
531+
expect(result).toBe('_normalized_PARAM_PARSER__string')
532+
})
533+
481534
it('returns empty string for missing parser', () => {
482535
const param: TreePathParam = {
483536
paramName: 'id',

packages/router/src/unplugin/codegen/generateParamParsers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ export type ParamParsersMap = Map<
3838
>
3939

4040
// just for type strictness
41-
const _NATIVE_PARAM_PARSERS = ['int', 'bool'] as const
41+
const _NATIVE_PARAM_PARSERS = ['int', 'bool', 'string'] as const
4242
const NATIVE_PARAM_PARSERS = _NATIVE_PARAM_PARSERS as readonly string[]
4343
const NATIVE_PARAM_PARSERS_TYPES = {
4444
int: 'number',
4545
bool: 'boolean',
46+
string: 'string',
4647
} satisfies Record<(typeof _NATIVE_PARAM_PARSERS)[number], string>
4748

4849
const RAW_PARAM_PARSER_DEFINER = 'defineParamParserRaw'
@@ -337,6 +338,10 @@ export function generateParamParserOptions(
337338
if (paramParsers.has(param.parser)) {
338339
const { name } = paramParsers.get(param.parser)!
339340
return `_normalized_PARAM_PARSER__${name}`
341+
// 'string' is the implicit default but it's part of NATIVE_PARAM_PARSERS
342+
// so we need to skip it here
343+
} else if (param.parser === 'string') {
344+
return ''
340345
} else if (NATIVE_PARAM_PARSERS.includes(param.parser)) {
341346
const varName = `PARAM_PARSER_${param.parser.toUpperCase()}`
342347
importsMap.add('vue-router/experimental', varName)

packages/router/src/unplugin/codegen/generateRouteParams.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,28 @@ describe('EXPERIMENTAL_generateRouteParams', () => {
8686
const result = EXPERIMENTAL_generateRouteParams(node, [null], false)
8787
expect(result).toBe('{ id: string | null }')
8888
})
89+
90+
it("native 'string' type matches no-parser output for required path", () => {
91+
const node = createTreeWithParam('[id]')
92+
const nullResult = EXPERIMENTAL_generateRouteParams(node, [null], false)
93+
const stringResult = EXPERIMENTAL_generateRouteParams(
94+
node,
95+
['string'],
96+
false
97+
)
98+
expect(stringResult).toBe(nullResult)
99+
})
100+
101+
it("native 'string' type matches no-parser output for optional path", () => {
102+
const node = createTreeWithParam('[[id]]')
103+
const nullResult = EXPERIMENTAL_generateRouteParams(node, [null], false)
104+
const stringResult = EXPERIMENTAL_generateRouteParams(
105+
node,
106+
['string'],
107+
false
108+
)
109+
expect(stringResult).toBe(nullResult)
110+
})
89111
})
90112

91113
describe('raw param parsers', () => {

packages/router/src/unplugin/codegen/generateRouteResolver.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,37 @@ describe('generateRouteRecordQuery', () => {
107107
`)
108108
})
109109

110+
it("emits identical query code for parser: 'string' and no parser", () => {
111+
const treeNone = new PrefixTree(DEFAULT_OPTIONS)
112+
const nodeNone = treeNone.insert('a', 'a.vue')
113+
nodeNone.value.setEditOverride('params', {
114+
query: { search: {} },
115+
})
116+
117+
const treeString = new PrefixTree(DEFAULT_OPTIONS)
118+
const nodeString = treeString.insert('a', 'a.vue')
119+
nodeString.value.setEditOverride('params', {
120+
query: { search: { parser: 'string' } },
121+
})
122+
123+
const noneImports = new ImportsMap()
124+
const stringImports = new ImportsMap()
125+
126+
const noneCode = generateRouteRecordQuery({
127+
importsMap: noneImports,
128+
node: nodeNone,
129+
paramParsersMap: new Map(),
130+
})
131+
const stringCode = generateRouteRecordQuery({
132+
importsMap: stringImports,
133+
node: nodeString,
134+
paramParsersMap: new Map(),
135+
})
136+
137+
expect(stringCode).toBe(noneCode)
138+
expect(stringImports.toString()).toBe(noneImports.toString())
139+
})
140+
110141
it('generates query property with multiple query params', () => {
111142
const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue')
112143
node.value.setEditOverride('params', {
@@ -371,6 +402,37 @@ describe('generateRouteRecord', () => {
371402
})"
372403
`)
373404
})
405+
406+
it('emits identical path code for [id=string] and [id]', () => {
407+
const treeNone = new PrefixTree(DEFAULT_OPTIONS)
408+
const treeString = new PrefixTree(DEFAULT_OPTIONS)
409+
410+
const noneImports = new ImportsMap()
411+
const stringImports = new ImportsMap()
412+
413+
const noneCode = generateRouteRecord({
414+
node: treeNone.insert('p/[id]', 'p/[id].vue'),
415+
parentVar: null,
416+
parentNode: null,
417+
state: { id: 0, matchableRecords: [] },
418+
options: DEFAULT_OPTIONS,
419+
importsMap: noneImports,
420+
paramParsersMap: new Map(),
421+
})
422+
const stringCode = generateRouteRecord({
423+
node: treeString.insert('p/[id=string]', 'p/[id=string].vue'),
424+
parentVar: null,
425+
parentNode: null,
426+
state: { id: 0, matchableRecords: [] },
427+
options: DEFAULT_OPTIONS,
428+
importsMap: stringImports,
429+
paramParsersMap: new Map(),
430+
})
431+
432+
// file paths differ between fixtures; align them before comparison
433+
expect(stringCode.replace(/\[id=string\]/g, '[id]')).toBe(noneCode)
434+
expect(stringImports.toString()).toBe(noneImports.toString())
435+
})
374436
})
375437

376438
describe('generateRouteResolver', () => {

0 commit comments

Comments
 (0)