Skip to content

Commit 91cdec3

Browse files
committed
feat(param-parsers): add include/exclude options
Close #2706
1 parent 8af50c9 commit 91cdec3

4 files changed

Lines changed: 145 additions & 10 deletions

File tree

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it } from 'vitest'
1+
import { describe, expect, it, test } from 'vitest'
22
import {
33
warnMissingParamParsers,
44
collectMissingParamParsers,
@@ -8,13 +8,17 @@ import {
88
generatePathParamsOptions,
99
generateCustomParamParsersList,
1010
generateNormalizedParamParsersDeclarations,
11+
scanParamParserFiles,
1112
type ParamParsersMap,
1213
} from './generateParamParsers'
1314
import { PrefixTree } from '../core/tree'
14-
import { resolveOptions } from '../options'
15+
import { DEFAULT_PARAM_PARSERS_OPTIONS, resolveOptions } from '../options'
1516
import { ImportsMap } from '../core/utils'
1617
import type { TreePathParam } from '../core/treeNodeValue'
1718
import { mockWarn } from '../../tests/vitest-mock-warn'
19+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
20+
import { tmpdir } from 'node:os'
21+
import { join } from 'node:path'
1822

1923
const DEFAULT_OPTIONS = resolveOptions({})
2024

@@ -785,3 +789,69 @@ describe('generateNormalizedParamParsersDeclarations', () => {
785789
)
786790
})
787791
})
792+
793+
// Per-test temp folder via the recommended vitest `test.extend` fixture pattern.
794+
// Each test gets a fresh isolated directory; cleanup runs after `use` resolves.
795+
const parsersTest = test.extend<{ parsersDir: string }>({
796+
parsersDir: async ({}, use) => {
797+
const dir = mkdtempSync(join(tmpdir(), 'vue-router-param-parsers-'))
798+
writeFileSync(join(dir, 'uuid.ts'), 'export const parser = {}')
799+
writeFileSync(join(dir, 'slug.ts'), 'export const parser = {}')
800+
writeFileSync(join(dir, 'uuid.test.ts'), 'test code')
801+
writeFileSync(join(dir, 'slug.spec.ts'), 'test code')
802+
writeFileSync(join(dir, 'legacy.js'), 'module.exports = {}')
803+
writeFileSync(join(dir, 'legacy.test.js'), 'module.exports = {}')
804+
writeFileSync(join(dir, 'README.md'), '# parsers')
805+
mkdirSync(join(dir, 'nested'))
806+
writeFileSync(join(dir, 'nested', 'deep.ts'), 'export const parser = {}')
807+
await use(dir)
808+
rmSync(dir, { recursive: true, force: true })
809+
},
810+
})
811+
812+
describe('scanParamParserFiles', () => {
813+
parsersTest(
814+
'picks parsers and ignores tests/specs/nested by default',
815+
async ({ parsersDir }) => {
816+
const files = await scanParamParserFiles(
817+
parsersDir,
818+
DEFAULT_PARAM_PARSERS_OPTIONS.include,
819+
DEFAULT_PARAM_PARSERS_OPTIONS.exclude
820+
)
821+
expect(files.sort()).toEqual(['slug.ts', 'uuid.ts'])
822+
}
823+
)
824+
825+
parsersTest(
826+
'respects a custom include that adds js',
827+
async ({ parsersDir }) => {
828+
const files = await scanParamParserFiles(
829+
parsersDir,
830+
['*.{ts,js}'],
831+
DEFAULT_PARAM_PARSERS_OPTIONS.exclude
832+
)
833+
expect(files.sort()).toEqual(['legacy.js', 'slug.ts', 'uuid.ts'])
834+
}
835+
)
836+
837+
parsersTest('respects a custom exclude', async ({ parsersDir }) => {
838+
const files = await scanParamParserFiles(parsersDir, ['*.ts'], ['uuid*'])
839+
expect(files.sort()).toEqual(['slug.spec.ts', 'slug.ts'])
840+
})
841+
842+
parsersTest(
843+
'returns no files when include is empty',
844+
async ({ parsersDir }) => {
845+
const files = await scanParamParserFiles(parsersDir, [], [])
846+
expect(files).toEqual([])
847+
}
848+
)
849+
850+
parsersTest(
851+
'never picks nested files even without exclude',
852+
async ({ parsersDir }) => {
853+
const files = await scanParamParserFiles(parsersDir, ['*.ts'], [])
854+
expect(files.some(f => f.includes('nested'))).toBe(false)
855+
}
856+
)
857+
})

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { TreePathParam, TreeQueryParam } from '../core/treeNodeValue'
22
import type { ImportsMap } from '../core/utils'
33
import type { PrefixTree } from '../core/tree'
44
import { toStringLiteral } from '../utils'
5+
import { glob } from 'tinyglobby'
56

67
export type ParamParsersMap = Map<
78
string,
@@ -21,6 +22,27 @@ const NATIVE_PARAM_PARSERS_TYPES = {
2122
bool: 'boolean',
2223
} satisfies Record<(typeof _NATIVE_PARAM_PARSERS)[number], string>
2324

25+
/**
26+
* Scans a folder for param parser files matching `include` while filtering out `exclude`.
27+
* Only flat matches are returned (no nested folders). Exported solely to make this
28+
* filesystem-touching behavior testable.
29+
*
30+
* @internal
31+
*/
32+
export function scanParamParserFiles(
33+
folder: string,
34+
include: string[],
35+
exclude: string[]
36+
): Promise<string[]> {
37+
if (!include.length) return Promise.resolve([])
38+
return glob(include as string[], {
39+
cwd: folder,
40+
onlyFiles: true,
41+
ignore: exclude as string[],
42+
expandDirectories: false,
43+
})
44+
}
45+
2446
export function warnMissingParamParsers(
2547
tree: PrefixTree,
2648
paramParsers: ParamParsersMap

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type { ParamParsersMap } from '../codegen/generateParamParsers'
2828
import {
2929
generateParamParsersTypesDeclarations,
3030
generateCustomParamParsersList,
31+
scanParamParserFiles,
3132
warnMissingParamParsers,
3233
collectMissingParamParsers,
3334
} from '../codegen/generateParamParsers'
@@ -73,8 +74,14 @@ export function createRoutesContext(options: ResolvedOptions) {
7374
return
7475
}
7576

76-
const PARAM_PARSER_GLOB = '*.{ts,js}'
77-
const isParamParserMatch = picomatch(PARAM_PARSER_GLOB)
77+
const paramParsersInclude = options.experimental.paramParsers?.include ?? []
78+
const paramParsersExclude = options.experimental.paramParsers?.exclude ?? []
79+
const isParamParserExcluded = paramParsersExclude.length
80+
? picomatch(paramParsersExclude)
81+
: () => false
82+
const isParamParserIncluded = paramParsersInclude.length
83+
? picomatch(paramParsersInclude)
84+
: () => false
7885

7986
// get the initial list of pages
8087
await Promise.all([
@@ -125,19 +132,23 @@ export function createRoutesContext(options: ResolvedOptions) {
125132
return false
126133
}
127134

128-
return !isParamParserMatch(relative(folder, filePath))
135+
const fileName = relative(folder, filePath)
136+
return (
137+
!isParamParserIncluded(fileName) ||
138+
isParamParserExcluded(fileName)
139+
)
129140
},
130141
}),
131142
folder
132143
)
133144
)
134145
}
135146

136-
return glob(PARAM_PARSER_GLOB, {
137-
cwd: folder,
138-
onlyFiles: true,
139-
expandDirectories: false,
140-
}).then(paramParserFiles => {
147+
return scanParamParserFiles(
148+
folder,
149+
paramParsersInclude,
150+
paramParsersExclude
151+
).then(paramParserFiles => {
141152
for (const file of paramParserFiles) {
142153
const fileName = parsePathe(file).name
143154
const name = camelCase(fileName)

packages/router/src/unplugin/options.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,31 @@ export interface ParamParsersOptions {
262262
* @default `['src/params']`
263263
*/
264264
dir?: string | string[]
265+
266+
/**
267+
* Glob pattern(s) of files to include when scanning param parser folders. Only flat
268+
* matches are supported (no nested files).
269+
*
270+
* @default `['*.ts']`
271+
*/
272+
include?: string | string[]
273+
274+
/**
275+
* Glob pattern(s) of files to ignore when scanning param parser folders. Useful to skip
276+
* test files that live next to their implementations.
277+
*
278+
* @default `['*.test.{ts,js}', '*.spec.{ts,js}']`
279+
*/
280+
exclude?: string | string[]
265281
}
266282

267283
/**
268284
* Default options for experimental param parsers.
269285
*/
270286
export const DEFAULT_PARAM_PARSERS_OPTIONS = {
271287
dir: ['src/params'],
288+
include: ['*.ts'],
289+
exclude: ['*.test.{ts,js}', '*.spec.{ts,js}'],
272290
} satisfies Required<ParamParsersOptions>
273291

274292
/**
@@ -394,6 +412,18 @@ export function resolveOptions(options: Options) {
394412
: []
395413
).map(dir => resolve(root, dir))
396414

415+
const paramParsersInclude = paramParsers?.include
416+
? isArray(paramParsers.include)
417+
? paramParsers.include
418+
: [paramParsers.include]
419+
: DEFAULT_PARAM_PARSERS_OPTIONS.include
420+
421+
const paramParsersExclude = paramParsers?.exclude
422+
? isArray(paramParsers.exclude)
423+
? paramParsers.exclude
424+
: [paramParsers.exclude]
425+
: DEFAULT_PARAM_PARSERS_OPTIONS.exclude
426+
397427
const autoExportsDataLoaders = options.experimental?.autoExportsDataLoaders
398428
? (isArray(options.experimental.autoExportsDataLoaders)
399429
? options.experimental.autoExportsDataLoaders
@@ -408,6 +438,8 @@ export function resolveOptions(options: Options) {
408438
paramParsers: paramParsers && {
409439
...paramParsers,
410440
dir: paramParsersDir,
441+
include: paramParsersInclude,
442+
exclude: paramParsersExclude,
411443
},
412444
}
413445

0 commit comments

Comments
 (0)