From 74ac22b01336a6e0a99769c6e645a5bfe29233bb Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 22 May 2024 11:52:43 +0200 Subject: [PATCH] fix: watch all extensions not just root --- playground/typed-router.d.ts | 1 - playground/vite.config.ts | 2 +- src/core/extendRoutes.spec.ts | 31 +++---- src/core/tree.spec.ts | 55 ++++++------ src/core/tree.ts | 8 +- src/core/utils.ts | 16 ---- src/index.ts | 26 +++--- src/options.ts | 161 ++++++++++++++++++++++++---------- 8 files changed, 184 insertions(+), 116 deletions(-) diff --git a/playground/typed-router.d.ts b/playground/typed-router.d.ts index 4edafe6bc..67200b92f 100644 --- a/playground/typed-router.d.ts +++ b/playground/typed-router.d.ts @@ -50,7 +50,6 @@ declare module 'vue-router/auto-routes' { '/n-[[n]]/': RouteRecordInfo<'/n-[[n]]/', '/n-:n?', { n?: ParamValueZeroOrOne }, { n?: ParamValueZeroOrOne }>, '/n-[[n]]/[[more]]+/': RouteRecordInfo<'/n-[[n]]/[[more]]+/', '/n-:n?/:more*', { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }, { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }>, '/n-[[n]]/[[more]]+/[final]': RouteRecordInfo<'/n-[[n]]/[[more]]+/[final]', '/n-:n?/:more*/:final', { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }, { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }>, - '/not-used': RouteRecordInfo<'/not-used', '/not-used', Record, Record>, '/partial-[name]': RouteRecordInfo<'/partial-[name]', '/partial-:name', { name: ParamValue }, { name: ParamValue }>, '/custom-path': RouteRecordInfo<'/custom-path', '/surprise-:id(\d+)', Record, Record>, '/todos/': RouteRecordInfo<'/todos/', '/todos', Record, Record>, diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 7f0913c07..2268eb16d 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ plugins: [ VueRouter({ - extensions: ['.page.vue', '.vue', '.md'], + extensions: ['.page.vue', '.vue'], importMode: 'async', extendRoute(route) { // console.log('extending route', route.meta) diff --git a/src/core/extendRoutes.spec.ts b/src/core/extendRoutes.spec.ts index 7f330b8d3..730a0e647 100644 --- a/src/core/extendRoutes.spec.ts +++ b/src/core/extendRoutes.spec.ts @@ -1,18 +1,19 @@ import { expect, describe, it } from 'vitest' import { PrefixTree } from './tree' -import { DEFAULT_OPTIONS } from '../options' +import { DEFAULT_OPTIONS, resolveOptions } from '../options' import { EditableTreeNode } from './extendRoutes' describe('EditableTreeNode', () => { + const RESOLVED_OPTIONS = resolveOptions(DEFAULT_OPTIONS) it('creates an editable tree node', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) expect(editable.children).toEqual([]) }) it('reflects changes made on the tree', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) tree.insert('foo', 'file.vue') @@ -21,7 +22,7 @@ describe('EditableTreeNode', () => { }) it('reflects changes made on the editable tree', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert('foo', 'file.vue') @@ -30,7 +31,7 @@ describe('EditableTreeNode', () => { }) it('keeps nested routes flat', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert('foo/bar', 'file.vue') @@ -41,7 +42,7 @@ describe('EditableTreeNode', () => { }) it('can nest routes', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) const node = editable.insert('foo', 'file.vue') @@ -55,7 +56,7 @@ describe('EditableTreeNode', () => { }) it('adds params', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':id', 'file.vue') @@ -75,7 +76,7 @@ describe('EditableTreeNode', () => { }) it('adds params with modifiers', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':id+', 'file.vue') @@ -95,7 +96,7 @@ describe('EditableTreeNode', () => { }) it('can have multiple params', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':foo/:bar', 'file.vue') @@ -122,7 +123,7 @@ describe('EditableTreeNode', () => { }) it('can have multiple params with modifiers', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':foo/:bar+_:o(\\d+)', 'file.vue') @@ -156,7 +157,7 @@ describe('EditableTreeNode', () => { }) it('adds params with custom regex', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':id(\\d+)', 'file.vue') @@ -175,7 +176,7 @@ describe('EditableTreeNode', () => { }) it('adds a param with empty regex', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':id()', 'file.vue') @@ -194,7 +195,7 @@ describe('EditableTreeNode', () => { }) it('adds a param with a modifier and custom regex', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':id(\\d+)+', 'file.vue') @@ -213,7 +214,7 @@ describe('EditableTreeNode', () => { }) it('adds a param with a modifier and empty regex', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert(':id()+', 'file.vue') @@ -232,7 +233,7 @@ describe('EditableTreeNode', () => { }) it('detects a splat', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const editable = new EditableTreeNode(tree) editable.insert('/:path(.*)', 'file.vue') diff --git a/src/core/tree.spec.ts b/src/core/tree.spec.ts index 77096ec18..8390f5be7 100644 --- a/src/core/tree.spec.ts +++ b/src/core/tree.spec.ts @@ -1,16 +1,17 @@ import { describe, expect, it } from 'vitest' -import { DEFAULT_OPTIONS } from '../options' +import { DEFAULT_OPTIONS, resolveOptions } from '../options' import { PrefixTree } from './tree' import { TreeNodeType } from './treeNodeValue' describe('Tree', () => { + const RESOLVED_OPTIONS = resolveOptions(DEFAULT_OPTIONS) it('creates an empty tree', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) expect(tree.children.size).toBe(0) }) it('creates a tree with a single static path', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('foo.vue') expect(tree.children.size).toBe(1) const child = tree.children.get('foo')! @@ -24,7 +25,7 @@ describe('Tree', () => { }) it('creates a tree with a single param', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[id].vue') expect(tree.children.size).toBe(1) const child = tree.children.get('[id]')! @@ -39,7 +40,7 @@ describe('Tree', () => { }) it('separate param names from static segments', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[id]_a') tree.insert('[a]e[b]f') expect(tree.children.get('[id]_a')!.value).toMatchObject({ @@ -58,7 +59,7 @@ describe('Tree', () => { }) it('creates params in nested files', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const nestedId = tree.insert('nested/[id].vue') expect(nestedId.value.isParam()).toBe(true) @@ -86,7 +87,7 @@ describe('Tree', () => { }) it('creates params in nested folders', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) let node = tree.insert('nested/[id]/index.vue') const id = tree.children.get('nested')!.children.get('[id]')! @@ -138,7 +139,7 @@ describe('Tree', () => { }) it('handles repeatable params one or more', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[id]+.vue') expect(tree.children.get('[id]+')!.value).toMatchObject({ rawSegment: '[id]+', @@ -156,7 +157,7 @@ describe('Tree', () => { }) it('handles repeatable params zero or more', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[[id]]+.vue') expect(tree.children.get('[[id]]+')!.value).toMatchObject({ rawSegment: '[[id]]+', @@ -174,7 +175,7 @@ describe('Tree', () => { }) it('handles optional params', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[[id]].vue') expect(tree.children.get('[[id]]')!.value).toMatchObject({ rawSegment: '[[id]]', @@ -192,7 +193,7 @@ describe('Tree', () => { }) it('handles named views', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('index.vue') tree.insert('index@a.vue') tree.insert('index@b.vue') @@ -233,7 +234,7 @@ describe('Tree', () => { }) it('handles single named views that are not default', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('index@a.vue') expect([...tree.children.get('index')!.value.components.keys()]).toEqual([ 'a', @@ -241,7 +242,7 @@ describe('Tree', () => { }) it('removes the node after all named views', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('index.vue') tree.insert('index@a.vue') expect(tree.children.get('index')).toBeDefined() @@ -252,7 +253,7 @@ describe('Tree', () => { }) it('can remove itself from the tree', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('index.vue').insert('nested.vue') tree.insert('a.vue').insert('nested.vue') tree.insert('b.vue') @@ -264,7 +265,7 @@ describe('Tree', () => { }) it('handles multiple params', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[a]-[b].vue') tree.insert('o[a]-[b]c.vue') tree.insert('o[a][b]c.vue') @@ -276,7 +277,7 @@ describe('Tree', () => { }) it('creates a tree of nested routes', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('index.vue') tree.insert('a/index.vue') tree.insert('a/b/index.vue') @@ -313,7 +314,7 @@ describe('Tree', () => { }) it('handles a modifier for single params', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[id]+.vue') expect(tree.children.size).toBe(1) const child = tree.children.get('[id]+')! @@ -329,7 +330,7 @@ describe('Tree', () => { }) it('removes nodes', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('foo.vue') tree.insert('[id].vue') tree.remove('foo.vue') @@ -346,7 +347,7 @@ describe('Tree', () => { }) it('removes empty folders', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('a/b/c/d.vue') expect(tree.children.size).toBe(1) tree.remove('a/b/c/d.vue') @@ -354,7 +355,7 @@ describe('Tree', () => { }) it('insert returns the node', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) const a = tree.insert('a.vue') expect(tree.children.get('a')).toBe(a) const bC = tree.insert('b/c.vue') @@ -366,7 +367,7 @@ describe('Tree', () => { }) it('keeps parent with file but no children', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('a/b/c/d.vue') tree.insert('a/b.vue') expect(tree.children.size).toBe(1) @@ -381,7 +382,7 @@ describe('Tree', () => { }) it('allows a custom name', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) let node = tree.insert('[a]-[b].vue') node.value.setOverride('', { name: 'custom', @@ -396,7 +397,7 @@ describe('Tree', () => { }) it('allows a custom path', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) let node = tree.insert('[a]-[b].vue') node.value.setOverride('', { path: '/custom', @@ -413,7 +414,7 @@ describe('Tree', () => { }) it('removes trailing slash from path but not from name', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('a/index.vue') tree.insert('a/a.vue') let child = tree.children.get('a')! @@ -435,7 +436,7 @@ describe('Tree', () => { it('handles long extensions', () => { const tree = new PrefixTree({ - ...DEFAULT_OPTIONS, + ...RESOLVED_OPTIONS, extensions: ['.page.vue'], }) tree.insert('a.page.vue') @@ -467,7 +468,7 @@ describe('Tree', () => { describe('dot nesting', () => { it('transforms dots into nested routes by default', () => { - const tree = new PrefixTree(DEFAULT_OPTIONS) + const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('users.new.vue') expect(tree.children.size).toBe(1) const users = tree.children.get('users.new')! @@ -481,7 +482,7 @@ describe('Tree', () => { it('can ignore dot nesting', () => { const tree = new PrefixTree({ - ...DEFAULT_OPTIONS, + ...RESOLVED_OPTIONS, pathParser: { dotNesting: false, }, diff --git a/src/core/tree.ts b/src/core/tree.ts index ff76ec33d..aa4ac8d12 100644 --- a/src/core/tree.ts +++ b/src/core/tree.ts @@ -1,11 +1,15 @@ -import type { ResolvedOptions, RoutesFolderOption } from '../options' +import { + resolveOverridableOption, + type ResolvedOptions, + type RoutesFolderOption, +} from '../options' import { createTreeNodeValue, TreeNodeValueOptions, TreeRouteParam, } from './treeNodeValue' import type { TreeNodeValue } from './treeNodeValue' -import { resolveOverridableOption, trimExtension } from './utils' +import { trimExtension } from './utils' import { CustomRouteBlock } from './customBlock' import { RouteMeta } from 'vue-router' diff --git a/src/core/utils.ts b/src/core/utils.ts index 93b88e1c7..955fd5f7c 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -81,22 +81,6 @@ export function trimExtension( return path } -/** - * Resolves an overridable option by calling the function with the existing value if it's a function, otherwise - * returning the passed `value`. If `value` is undefined, it returns the `defaultValue` instead. - * - * @param defaultValue default value for the option - * @param value and overridable option - */ -export function resolveOverridableOption( - defaultValue: T, - value?: _OverridableOption -): T { - return typeof value === 'function' - ? (value as (existing: T) => T)(defaultValue) - : value ?? defaultValue -} - export function throttle(fn: () => void, wait: number, initialWait: number) { let pendingExecutionTimeout: ReturnType | null = null let pendingExecution = false diff --git a/src/index.ts b/src/index.ts index fdfb61ef6..c768ba546 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,10 +9,16 @@ import { ROUTE_BLOCK_ID, } from './core/moduleConstants' // TODO: export standalone createRoutesContext that resolves partial options -import { Options, resolveOptions, DEFAULT_OPTIONS } from './options' +import { + Options, + resolveOptions, + DEFAULT_OPTIONS, + mergeAllExtensions, +} from './options' import { createViteContext } from './core/vite' import { createFilter } from '@rollup/pluginutils' import { join } from 'pathe' +import { appendExtensionListToPattern } from './core/utils' export * from './types' @@ -34,17 +40,17 @@ export default createUnplugin((opt = {}, _meta) => { } // create the transform filter to detect `definePage()` inside page component - const pageFilePattern = - `**/*` + - (options.extensions.length === 1 - ? options.extensions[0] - : `.{${options.extensions - .map((extension) => extension.replace('.', '')) - .join(',')}}`) + const pageFilePattern = appendExtensionListToPattern( + options.filePatterns, + mergeAllExtensions(options) + ) + + // this is a larger filter that includes a bit too many files + // the RouteFolderWatcher will filter it down to the actual files const filterPageComponents = createFilter( [ - ...options.routesFolder.map((routeOption) => - join(routeOption.src, pageFilePattern) + ...options.routesFolder.flatMap((routeOption) => + pageFilePattern.map((pattern) => join(routeOption.src, pattern)) ), // importing the definePage block /definePage\&vue$/, diff --git a/src/options.ts b/src/options.ts index c8660fca8..4517bc109 100644 --- a/src/options.ts +++ b/src/options.ts @@ -6,8 +6,6 @@ import { EditableTreeNode } from './core/extendRoutes' import { type ParseSegmentOptions } from './core/treeNodeValue' import { type _Awaitable } from './utils' -// TODO: remove from exports and move export to src/index? - /** * Options for a routes folder. */ @@ -53,13 +51,13 @@ export interface RoutesFolderOption { * Allows to override the global `filePattern` option for this folder. It can also extend the global values by passing * a function that returns an array. */ - filePatterns?: _OverridableOption | string + filePatterns?: _OverridableOption /** * Allows to override the global `exclude` option for this folder. It can also extend the global values by passing a * function that returns an array. */ - exclude?: _OverridableOption + exclude?: _OverridableOption /** * Allows to override the global `extensions` option for this folder. It can also extend the global values by passing @@ -82,20 +80,48 @@ export interface RoutesFolderOptionResolved extends RoutesFolderOption { extensions: string[] } -export type _OverridableOption = T | ((existing: T) => T) +export type _OverridableOption = + | AllowedTypes + | ((existing: T) => T) + +/** + * Resolves an overridable option by calling the function with the existing value if it's a function, otherwise + * returning the passed `value`. If `value` is undefined, it returns the `defaultValue` instead. + * + * @param defaultValue default value for the option + * @param value and overridable option + */ +export function resolveOverridableOption( + defaultValue: T, + value?: _OverridableOption +): T { + return typeof value === 'function' + ? (value as (existing: T) => T)(defaultValue) + : value ?? defaultValue +} export type _RoutesFolder = string | RoutesFolderOption export type RoutesFolder = _RoutesFolder[] | _RoutesFolder -export interface ResolvedOptions { +/** + * unplugin-vue-router plugin options. + */ +export interface Options { /** * Extensions of files to be considered as pages. Cannot be empty. This allows to strip a * bigger part of the filename e.g. `index.page.vue` -> `index` if an extension of `.page.vue` is provided. * @default `['.vue']` */ - extensions: string[] + extensions?: string[] - routesFolder: RoutesFolderOption[] + /** + * Folder(s) to scan for files and generate routes. Can also be an array if you want to add multiple + * folders, or an object if you want to define a route prefix. Supports glob patterns but must be a folder, use + * `extensions` and `exclude` to filter files. + * + * @default `"src/pages"` + */ + routesFolder?: RoutesFolder /** * Array of `picomatch` globs to ignore. Note the globs are relative to the cwd, so avoid writing @@ -103,21 +129,21 @@ export interface ResolvedOptions { * `['src/pages/ignored/**']` or use `['**​/ignored']` to match every folder named `ignored`. * @default `[]` */ - exclude: string[] + exclude?: string[] | string // NOTE: the comment below contains ZWJ characters to allow the sequence `**/*` to be displayed correctly /** * Pattern to match files in the `routesFolder`. Defaults to `**‍/*` plus a combination of all the possible extensions, * e.g. `**‍/*.{vue,md}` if `extensions` is set to `['.vue', '.md']`. - * @default `'**‍/*'` + * @default `['**‍/*']` */ - filePatterns: string | string[] + filePatterns?: string[] | string /** * Method to generate the name of a route. It's recommended to keep the default value to guarantee a consistent, * unique, and predictable naming. */ - getRouteName: (node: TreeNode) => string + getRouteName?: (node: TreeNode) => string /** * Allows to extend a route by modifying its node, adding children, or even deleting it. This will be invoked once for @@ -142,42 +168,42 @@ export interface ResolvedOptions { * Defines how page components should be imported. Defaults to dynamic imports to enable lazy loading of pages. * @default `'async'` */ - importMode: 'sync' | 'async' | ((filepath: string) => 'sync' | 'async') + importMode?: 'sync' | 'async' | ((filepath: string) => 'sync' | 'async') /** * Root of the project. All paths are resolved relatively to this one. * @default `process.cwd()` */ - root: string + root?: string /** * Language for `` blocks in SFC files. * @default `'json5'` */ - routeBlockLang: 'yaml' | 'yml' | 'json5' | 'json' + routeBlockLang?: 'yaml' | 'yml' | 'json5' | 'json' /** * Should we generate d.ts files or ont. Defaults to `true` if `typescript` is installed. Can be set to a string of * the filepath to write the d.ts files to. By default it will generate a file named `typed-router.d.ts`. * @default `true` */ - dts: boolean | string + dts?: boolean | string /** * Allows inspection by vite-plugin-inspect by not adding the leading `\0` to the id of virtual modules. * @internal */ - _inspect: boolean + _inspect?: boolean /** * Activates debug logs. */ - logs: boolean + logs?: boolean /** * @inheritDoc ParseSegmentOptions */ - pathParser: ParseSegmentOptions + pathParser?: ParseSegmentOptions /** * Whether to watch the files for changes. @@ -186,29 +212,14 @@ export interface ResolvedOptions { * * @default `!process.env.CI` */ - watch: boolean + watch?: boolean } -/** - * unplugin-vue-router plugin options. - */ -export interface Options - extends Partial> { - /** - * Folder(s) to scan for files and generate routes. Can also be an array if you want to add multiple - * folders, or an object if you want to define a route prefix. Supports glob patterns but must be a folder, use - * `extensions` and `exclude` to filter files. - * - * @default `"src/pages"` - */ - routesFolder?: RoutesFolder -} - -export const DEFAULT_OPTIONS: ResolvedOptions = { +export const DEFAULT_OPTIONS = { extensions: ['.vue'], exclude: [], - routesFolder: [{ src: 'src/pages' }], - filePatterns: '**/*', + routesFolder: 'src/pages', + filePatterns: ['**/*'], routeBlockLang: 'json5', getRouteName: getFileBasedRouteName, importMode: 'async', @@ -220,29 +231,53 @@ export const DEFAULT_OPTIONS: ResolvedOptions = { dotNesting: true, }, watch: !process.env.CI, -} +} satisfies Options export interface ServerContext { invalidate: (module: string) => void reload: () => void } -function normalizeRoutesFolderOption( - routesFolder: RoutesFolder -): RoutesFolderOption[] { +function normalizeRoutesFolderOption(routesFolder: RoutesFolder) { return (isArray(routesFolder) ? routesFolder : [routesFolder]).map( (routeOption) => - typeof routeOption === 'string' ? { src: routeOption } : routeOption + // normalizing here allows to have a better type for the resolved options + normalizeRouteOption( + typeof routeOption === 'string' ? { src: routeOption } : routeOption + ) ) } +function normalizeRouteOption(routeOption: RoutesFolderOption) { + return { + ...routeOption, + // ensure filePatterns is always an array or a function + filePatterns: routeOption.filePatterns + ? typeof routeOption.filePatterns === 'function' + ? routeOption.filePatterns + : isArray(routeOption.filePatterns) + ? routeOption.filePatterns + : [routeOption.filePatterns] + : undefined, + + // same for exclude + exclude: routeOption.exclude + ? typeof routeOption.exclude === 'function' + ? routeOption.exclude + : isArray(routeOption.exclude) + ? routeOption.exclude + : [routeOption.exclude] + : undefined, + } +} + /** * Normalize user options with defaults and resolved paths. * * @param options - user provided options * @returns normalized options */ -export function resolveOptions(options: Options): ResolvedOptions { +export function resolveOptions(options: Options) { const root = options.root || DEFAULT_OPTIONS.root // normalize the paths with the root @@ -270,9 +305,47 @@ export function resolveOptions(options: Options): ResolvedOptions { .sort((a, b) => b.length - a.length) } + const filePatterns = options.filePatterns + ? isArray(options.filePatterns) + ? options.filePatterns + : [options.filePatterns] + : DEFAULT_OPTIONS.filePatterns + const exclude = options.exclude + ? isArray(options.exclude) + ? options.exclude + : [options.exclude] + : DEFAULT_OPTIONS.exclude + return { ...DEFAULT_OPTIONS, ...options, routesFolder, + filePatterns, + exclude, } } + +export type ResolvedOptions = ReturnType + +/** + * Merge all the possible extensions as an array of unique values + * @param options - user provided options + * @internal + */ +export function mergeAllExtensions(options: ResolvedOptions): string[] { + const allExtensions = new Set(options.extensions) + + for (const routeOption of options.routesFolder) { + if (routeOption.extensions) { + const extensions = resolveOverridableOption( + options.extensions, + routeOption.extensions + ) + for (const ext of extensions) { + allExtensions.add(ext) + } + } + } + + return Array.from(allExtensions.values()) +}