diff --git a/playground/src/routes/custom-name-and-path.vue b/playground/src/routes/custom-name-and-path.vue new file mode 100644 index 000000000..52ce11206 --- /dev/null +++ b/playground/src/routes/custom-name-and-path.vue @@ -0,0 +1,6 @@ + +{ + "name": "the most rebel", + "path": "/most-rebel" +} + diff --git a/playground/src/routes/custom-name.vue b/playground/src/routes/custom-name.vue new file mode 100644 index 000000000..4b261be07 --- /dev/null +++ b/playground/src/routes/custom-name.vue @@ -0,0 +1,5 @@ + +{ + "name": "a rebel" +} + diff --git a/playground/src/routes/custom-path.vue b/playground/src/routes/custom-path.vue new file mode 100644 index 000000000..44370200e --- /dev/null +++ b/playground/src/routes/custom-path.vue @@ -0,0 +1,5 @@ + +{ + "path": "/surprise-:id(\\d+)" +} + diff --git a/playground/typed-router.d.ts b/playground/typed-router.d.ts index 79e29c8b8..f05c41c91 100644 --- a/playground/typed-router.d.ts +++ b/playground/typed-router.d.ts @@ -23,11 +23,14 @@ declare module '@vue-router/routes' { '/': RouteRecordInfo<'/', '/', Record, Record>, '/[name]': RouteRecordInfo<'/[name]', '/:name', { name: _ParamValue }, { name: _ParamValue }>, '/[...path]': RouteRecordInfo<'/[...path]', '/:path(.*)', { path: _ParamValue }, { path: _ParamValue }>, + 'the most rebel': RouteRecordInfo<'the most rebel', '/most-rebel', Record, Record>, + '/custom-path': RouteRecordInfo<'/custom-path', '/surprise-:id(\d+)', Record, Record>, '/about': RouteRecordInfo<'/about', '/about', Record, Record>, '/articles': RouteRecordInfo<'/articles', '/articles', Record, Record>, '/articles/': RouteRecordInfo<'/articles/', '/articles/', Record, Record>, '/articles/[id]': RouteRecordInfo<'/articles/[id]', '/articles/:id', { id: _ParamValue }, { id: _ParamValue }>, '/articles/[id]+': RouteRecordInfo<'/articles/[id]+', '/articles/:id+', { id: _ParamValueOneOrMore }, { id: _ParamValueOneOrMore }>, + 'a rebel': RouteRecordInfo<'a rebel', '/custom-name', Record, Record>, '/deep/nesting/works/[[files]]+': RouteRecordInfo<'/deep/nesting/works/[[files]]+', '/deep/nesting/works/:files*', { files?: _ParamValueZeroOrMore }, { files?: _ParamValueZeroOrMore }>, '/deep/nesting/works/too': RouteRecordInfo<'/deep/nesting/works/too', '/deep/nesting/works/too', Record, Record>, '/multiple-[a]-[b]-params': RouteRecordInfo<'/multiple-[a]-[b]-params', '/multiple-:a-:b-params', { a: _ParamValue, b: _ParamValue }, { a: _ParamValue, b: _ParamValue }>, diff --git a/src/codegen/generateRouteMap.ts b/src/codegen/generateRouteMap.ts index 29fa11500..5fe10d8ff 100644 --- a/src/codegen/generateRouteMap.ts +++ b/src/codegen/generateRouteMap.ts @@ -18,9 +18,7 @@ ${node.getSortedChildren().map(generateRouteNamedMap).join('')}}` // if the node has a filePath, it's a component, it has a routeName and it should be referenced in the RouteNamedMap // otherwise it should be skipped to avoid navigating to a route that doesn't render anything (node.value.filePaths.size - ? ` '${node.options.getRouteName(node)}': ${generateRouteRecordInfo( - node - )},\n` + ? ` '${node.name}': ${generateRouteRecordInfo(node)},\n` : '') + (node.children.size > 0 ? node.getSortedChildren().map(generateRouteNamedMap).join('\n') @@ -29,8 +27,8 @@ ${node.getSortedChildren().map(generateRouteNamedMap).join('')}}` } export function generateRouteRecordInfo(node: TreeLeaf) { - return `RouteRecordInfo<'${node.options.getRouteName(node)}', '${ - node.value.path + return `RouteRecordInfo<'${node.name}', '${ + node.fullPath }', ${generateRouteParams(node, true)}, ${generateRouteParams(node, false)}>` } diff --git a/src/codegen/generateRouteRecords.ts b/src/codegen/generateRouteRecords.ts index eabccc03a..8acbbcf84 100644 --- a/src/codegen/generateRouteRecords.ts +++ b/src/codegen/generateRouteRecords.ts @@ -18,11 +18,14 @@ ${node const startIndent = ' '.repeat(indent * 2) const indentStr = ' '.repeat((indent + 1) * 2) - const name = node.options.getRouteName(node) + // TODO: should meta be defined a different way to allow preserving imports? + // const meta = node.value.overrides.meta return `${startIndent}{ -${indentStr}path: '${(parent ? '' : '/') + node.value.pathSegment}', -${indentStr}${node.value.filePaths.size ? `name: '${name}',` : '/* no name */'} +${indentStr}path: '${node.path}', +${indentStr}${ + node.value.filePaths.size ? `name: '${node.name}',` : '/* no name */' + } ${indentStr}${ node.value.filePaths.size ? generateRouteRecordComponent(node, indentStr) diff --git a/src/core/context.ts b/src/core/context.ts index 134829490..d22251b08 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -14,6 +14,7 @@ import { generateRouteRecord } from '../codegen/generateRouteRecords' import fg from 'fast-glob' import { resolve } from 'pathe' import { ServerContext } from '../options' +import { getRouteBlock, parseCustomBlock } from './customBlock' export function createRoutesContext(options: Required) { const { dts: preferDTS, root } = options @@ -72,21 +73,30 @@ export function createRoutesContext(options: Required) { ) ).flat() - for (const file of files) { - addPage(file) - } + await Promise.all(files.map((file) => addPage(file))) + await _writeConfigFiles() } - function addPage(path: string) { + async function addPage(path: string) { + const routePath = stripRouteFolder(path) + const routeBlock = await getRouteBlock(path, options) log('added', path) - const route = stripRouteFolder(path) + if (routeBlock) console.log(routeBlock) // TODO: handle top level named view HMR - routeTree.insert( - route, + const node = routeTree.insert( + routePath, // './' + path resolve(root, path) ) + node.mergeCustomRouteBlock(routeBlock) + } + + async function updatePage(path: string) { + const routeBlock = await getRouteBlock(path, options) + // TODO: + // const node = routeTree.findByPath(path) + // node.mergeCustomRouteBlock(routeBlock) } function removePage(path: string) { @@ -100,14 +110,15 @@ export function createRoutesContext(options: Required) { .on('change', (path) => { // TODO: parse defineRoute macro? log('change', path) + // updatePage(path) // writeConfigFiles() }) - .on('add', (path) => { - addPage(path) + .on('add', async (path) => { + await addPage(path) writeConfigFiles() }) - .on('unlink', (path) => { - removePage(path) + .on('unlink', async (path) => { + await removePage(path) writeConfigFiles() }) } diff --git a/src/core/customBlock.ts b/src/core/customBlock.ts index fa7ac9800..24171dbe4 100644 --- a/src/core/customBlock.ts +++ b/src/core/customBlock.ts @@ -19,12 +19,25 @@ export async function getRouteBlock(path: string, options: Required) { result = parseCustomBlock(blockStr, path, options) } + // validation + if (result) { + if (result.path != null && !result.path.startsWith('/')) { + console.error(`Overridden path must start with "/". Found in "${path}".`) + } + } + return result } -export type CustomRouteBlock = Partial< - Omit -> +export interface CustomRouteBlock + extends Partial< + Omit< + RouteRecordRaw, + 'components' | 'component' | 'children' | 'beforeEnter' | 'name' + > + > { + name?: string +} function parseCustomBlock( block: SFCBlock, diff --git a/src/core/tree.ts b/src/core/tree.ts index e2cfd1424..1cff9b007 100644 --- a/src/core/tree.ts +++ b/src/core/tree.ts @@ -57,17 +57,12 @@ export class TreeLeaf { mergeCustomRouteBlock(routeBlock: CustomRouteBlock | undefined) { if (!routeBlock) return - this.value.name = routeBlock.name - if (routeBlock.path != null) { - this.value.path = routeBlock.path - this.value.pathSegment = routeBlock.path - } - this.value.meta = routeBlock.meta + this.value.overrides = routeBlock } getSortedChildren() { return Array.from(this.children.values()).sort((a, b) => - a.value.path.localeCompare(b.value.path) + a.path.localeCompare(b.path) ) } @@ -77,7 +72,7 @@ export class TreeLeaf { const child = this.children.get(segment) if (!child) { throw new Error( - `Cannot Delete "${path}". "${segment}" not found at "${this.value.path}".` + `Cannot Delete "${path}". "${segment}" not found at "${this.path}".` ) } @@ -99,6 +94,21 @@ export class TreeLeaf { } } + get path() { + return ( + this.value.overrides.path ?? + (this.parent ? '' : '/') + this.value.pathSegment + ) + } + + get fullPath() { + return this.value.overrides.path ?? this.value.path + } + + get name() { + return this.value.overrides.name || this.options.getRouteName(this) + } + isRoot() { return this.value.path === '/' && !this.value.filePaths.size } diff --git a/src/core/treeLeafValue.ts b/src/core/treeLeafValue.ts index 7d2f3cc15..fafe2dabf 100644 --- a/src/core/treeLeafValue.ts +++ b/src/core/treeLeafValue.ts @@ -32,15 +32,17 @@ class _TreeLeafValueBase { */ path: string - /** - * Overridden name for this route. - */ - name?: RouteRecordName - - /** - * Meta of the route - */ - meta?: RouteMeta + overrides: { + path?: string + /** + * Overridden name for this route. + */ + name?: string + /** + * Meta of the route + */ + meta?: RouteMeta + } = {} /** * Component path that maps to a view name, which is used for vue-router's named view feature.