Skip to content

Commit 26e3032

Browse files
committed
feat: runtime error on missing param parsers
1 parent c9921fe commit 26e3032

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
lines changed

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import {
33
warnMissingParamParsers,
4+
collectMissingParamParsers,
45
generateParamParsersTypesDeclarations,
56
generateParamsTypes,
67
generateParamParserOptions,
@@ -71,6 +72,87 @@ describe('warnMissingParamParsers', () => {
7172
})
7273
})
7374

75+
describe('collectMissingParamParsers', () => {
76+
it('returns empty array for routes without param parsers', () => {
77+
const tree = new PrefixTree(DEFAULT_OPTIONS)
78+
tree.insert('users', 'users.vue')
79+
tree.insert('posts/[id]', 'posts/[id].vue')
80+
81+
const paramParsers: ParamParsersMap = new Map()
82+
83+
const result = collectMissingParamParsers(tree, paramParsers)
84+
expect(result).toEqual([])
85+
})
86+
87+
it('returns empty array for native parsers', () => {
88+
const tree = new PrefixTree(DEFAULT_OPTIONS)
89+
tree.insert('users/[id=int]', 'users/[id=int].vue')
90+
tree.insert('posts/[active=bool]', 'posts/[active=bool].vue')
91+
92+
const paramParsers: ParamParsersMap = new Map()
93+
94+
const result = collectMissingParamParsers(tree, paramParsers)
95+
expect(result).toEqual([])
96+
})
97+
98+
it('collects missing custom parsers with route and file info', () => {
99+
const tree = new PrefixTree(DEFAULT_OPTIONS)
100+
tree.insert('users/[id=uuid]', 'users/[id=uuid].vue')
101+
102+
const paramParsers: ParamParsersMap = new Map()
103+
104+
const result = collectMissingParamParsers(tree, paramParsers)
105+
expect(result).toEqual([
106+
{
107+
parser: 'uuid',
108+
routePath: '/users/:id',
109+
filePaths: ['users/[id=uuid].vue'],
110+
},
111+
])
112+
})
113+
114+
it('returns empty array when custom parsers exist in map', () => {
115+
const tree = new PrefixTree(DEFAULT_OPTIONS)
116+
tree.insert('users/[id=uuid]', 'users/[id=uuid].vue')
117+
118+
const paramParsers: ParamParsersMap = new Map([
119+
[
120+
'uuid',
121+
{
122+
name: 'uuid',
123+
typeName: 'Param_uuid',
124+
relativePath: 'parsers/uuid',
125+
absolutePath: '/path/to/parsers/uuid',
126+
},
127+
],
128+
])
129+
130+
const result = collectMissingParamParsers(tree, paramParsers)
131+
expect(result).toEqual([])
132+
})
133+
134+
it('collects multiple missing parsers from different routes', () => {
135+
const tree = new PrefixTree(DEFAULT_OPTIONS)
136+
tree.insert('users/[id=uuid]', 'users/[id=uuid].vue')
137+
tree.insert('posts/[slug=slug]', 'posts/[slug=slug].vue')
138+
139+
const paramParsers: ParamParsersMap = new Map()
140+
141+
const result = collectMissingParamParsers(tree, paramParsers)
142+
expect(result).toHaveLength(2)
143+
expect(result).toContainEqual({
144+
parser: 'uuid',
145+
routePath: '/users/:id',
146+
filePaths: ['users/[id=uuid].vue'],
147+
})
148+
expect(result).toContainEqual({
149+
parser: 'slug',
150+
routePath: '/posts/:slug',
151+
filePaths: ['posts/[slug=slug].vue'],
152+
})
153+
})
154+
})
155+
74156
describe('generateParamParsersTypesDeclarations', () => {
75157
it('returns empty string for empty param parsers map', () => {
76158
const paramParsers: ParamParsersMap = new Map()

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@ export function warnMissingParamParsers(
3737
}
3838
}
3939

40+
export interface MissingParamParser {
41+
parser: string
42+
routePath: string
43+
filePaths: string[]
44+
}
45+
46+
export function collectMissingParamParsers(
47+
tree: PrefixTree,
48+
paramParsers: ParamParsersMap
49+
): MissingParamParser[] {
50+
const missing: MissingParamParser[] = []
51+
for (const node of tree.getChildrenDeepSorted()) {
52+
for (const param of node.params) {
53+
if (param.parser && !paramParsers.has(param.parser)) {
54+
if (!NATIVE_PARAM_PARSERS.includes(param.parser)) {
55+
missing.push({
56+
parser: param.parser,
57+
routePath: node.fullPath,
58+
filePaths: Array.from(node.value.components.values()),
59+
})
60+
}
61+
}
62+
}
63+
}
64+
return missing
65+
}
66+
4067
export function generateParamParsersTypesDeclarations(
4168
paramParsers: ParamParsersMap
4269
) {

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
generateParamParserCustomType,
2727
ParamParsersMap,
2828
warnMissingParamParsers,
29+
collectMissingParamParsers,
2930
} from '../codegen/generateParamParsers'
3031
import picomatch from 'picomatch'
3132
import { camelCase } from 'scule'
@@ -276,6 +277,24 @@ export function createRoutesContext(options: ResolvedOptions) {
276277
imports += '\n'
277278
}
278279

280+
// collect missing param parsers and generate runtime errors
281+
const missingParsers = collectMissingParamParsers(
282+
routeTree,
283+
paramParsersMap
284+
)
285+
let missingParserErrors = ''
286+
if (missingParsers.length > 0) {
287+
missingParserErrors =
288+
'\n' +
289+
missingParsers
290+
.map(
291+
({ parser, routePath, filePaths }) =>
292+
`console.error('[vue-router] Parameter parser "${parser}" not found for route "${routePath}". File: ${filePaths.join(', ')}')`
293+
)
294+
.join('\n') +
295+
'\n'
296+
}
297+
279298
const hmr = ts`
280299
export function handleHotUpdate(_router, _hotUpdateCallback) {
281300
if (import.meta.hot) {
@@ -304,7 +323,7 @@ if (import.meta.hot) {
304323
})
305324
}`
306325

307-
const newAutoResolver = `${imports}${resolverCode}\n${hmr}`
326+
const newAutoResolver = `${imports}${missingParserErrors}${resolverCode}\n${hmr}`
308327

309328
// prepend it to the code
310329
return newAutoResolver

0 commit comments

Comments
 (0)