diff --git a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap index 5fc90982..7652611a 100644 --- a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap +++ b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap @@ -37,21 +37,6 @@ exports[`generateRouteRecord > adds children and name when folder and component ]" `; -exports[`generateRouteRecord > adds meta data 1`] = ` -"[ - { - path: '/', - name: '/', - component: () => import('index.vue'), - /* no children */ - meta: { - \\"auth\\": true, - \\"title\\": \\"Home\\" - } - } -]" -`; - exports[`generateRouteRecord > correctly names index.vue files 1`] = ` "[ { @@ -409,6 +394,88 @@ exports[`generateRouteRecord > nested children 2`] = ` ]" `; +exports[`generateRouteRecord > route block > adds meta data 1`] = ` +"[ + { + path: '/', + name: '/', + component: () => import('index.vue'), + /* no children */ + meta: { + \\"auth\\": true, + \\"title\\": \\"Home\\" + } + } +]" +`; + +exports[`generateRouteRecord > route block > handles named views with empty route blocks 1`] = ` +"[ + { + path: '/', + name: '/', + components: { + 'default': () => import('index.vue'), + 'named': () => import('index@named.vue') + }, + /* no children */ + meta: { + \\"auth\\": true, + \\"title\\": \\"Home\\" + } + } +]" +`; + +exports[`generateRouteRecord > route block > merges deep meta properties 1`] = ` +"[ + { + path: '/', + name: '/', + component: () => import('index.vue'), + /* no children */ + meta: { + \\"a\\": { + \\"one\\": 1, + \\"two\\": 1 + }, + \\"b\\": { + \\"a\\": [ + 2, + 3 + ] + } + } + } +]" +`; + +exports[`generateRouteRecord > route block > merges multiple meta properties 1`] = ` +"[ + { + path: '/custom', + name: 'hello', + component: () => import('index.vue'), + /* no children */ + meta: { + \\"one\\": true, + \\"two\\": true + } + } +]" +`; + +exports[`generateRouteRecord > route block > merges regardless of order 1`] = ` +"[ + { + path: '/', + name: 'b', + component: () => import('index.vue'), + /* no children */ + } +]" +`; + exports[`generateRouteRecord > works with some paths at root 1`] = ` "[ { diff --git a/src/codegen/generateRouteRecords.spec.ts b/src/codegen/generateRouteRecords.spec.ts index 09a88f04..14a8ba63 100644 --- a/src/codegen/generateRouteRecords.spec.ts +++ b/src/codegen/generateRouteRecords.spec.ts @@ -116,16 +116,121 @@ describe('generateRouteRecord', () => { }) }) - it('adds meta data', () => { - const tree = createPrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - node.setCustomRouteBlock({ - meta: { - auth: true, - title: 'Home', - }, + describe('route block', () => { + it('adds meta data', async () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('index.vue') + node.setCustomRouteBlock('index.vue', { + meta: { + auth: true, + title: 'Home', + }, + }) + + expect(generateRouteRecord(tree)).toMatchSnapshot() }) - expect(generateRouteRecord(tree)).toMatchSnapshot() + it('merges multiple meta properties', async () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('index.vue') + node.setCustomRouteBlock('index.vue', { + path: '/custom', + meta: { + one: true, + }, + }) + node.setCustomRouteBlock('index@named.vue', { + name: 'hello', + meta: { + two: true, + }, + }) + + expect(generateRouteRecord(tree)).toMatchSnapshot() + }) + + it('merges regardless of order', async () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('index.vue') + node.setCustomRouteBlock('index.vue', { + name: 'a', + }) + node.setCustomRouteBlock('index@named.vue', { + name: 'b', + }) + + const one = generateRouteRecord(tree) + + node.setCustomRouteBlock('index@named.vue', { + name: 'b', + }) + node.setCustomRouteBlock('index.vue', { + name: 'a', + }) + + expect(generateRouteRecord(tree)).toBe(one) + + expect(one).toMatchSnapshot() + }) + + it('handles named views with empty route blocks', () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('index.vue') + const n2 = tree.insert('index@named.vue') + expect(node).toBe(n2) + // coming from index.vue + node.setCustomRouteBlock('index.vue', { + meta: { + auth: true, + title: 'Home', + }, + }) + // coming from index@named.vue (no route block) + node.setCustomRouteBlock('index@named.vue', undefined) + + expect(generateRouteRecord(tree)).toMatchSnapshot() + }) + + // FIXME: allow aliases + it('merges alias properties', async () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('index.vue') + node.setCustomRouteBlock('index.vue', { + alias: '/one', + }) + node.setCustomRouteBlock('index@named.vue', { + alias: ['/two', '/three'], + }) + + expect(generateRouteRecord(tree)).toMatchInlineSnapshot(` + "[ + { + path: '/', + name: '/', + component: () => import('index.vue'), + /* no children */ + } + ]" + `) + }) + + it('merges deep meta properties', async () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('index.vue') + node.setCustomRouteBlock('index.vue', { + meta: { + a: { one: 1 }, + b: { a: [2] }, + }, + }) + node.setCustomRouteBlock('index@named.vue', { + meta: { + a: { two: 1 }, + b: { a: [3] }, + }, + }) + + expect(generateRouteRecord(tree)).toMatchSnapshot() + }) }) }) diff --git a/src/core/context.ts b/src/core/context.ts index ff53a25e..506c98c3 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -103,7 +103,7 @@ export function createRoutesContext(options: ResolvedOptions) { // './' + path resolve(root, path) ) - node.setCustomRouteBlock(routeBlock) + node.setCustomRouteBlock(path, routeBlock) node.value.includeLoaderGuard = await hasNamedExports(path) routeMap.set(path, node) @@ -116,7 +116,7 @@ export function createRoutesContext(options: ResolvedOptions) { console.warn(`Cannot update "${path}": Not found.`) return } - node.setCustomRouteBlock(await getRouteBlock(path, options)) + node.setCustomRouteBlock(path, await getRouteBlock(path, options)) node.value.includeLoaderGuard = await hasNamedExports(path) } diff --git a/src/core/tree.spec.ts b/src/core/tree.spec.ts index aa43ead4..5a5da5ff 100644 --- a/src/core/tree.spec.ts +++ b/src/core/tree.spec.ts @@ -297,31 +297,31 @@ describe('Tree', () => { it('allows a custom name', () => { const tree = createPrefixTree(DEFAULT_OPTIONS) let leaf = tree.insert('[a]-[b].vue') - leaf.value.overrides = { + leaf.value.setOverride('', { name: 'custom', - } + }) expect(leaf.name).toBe('custom') leaf = tree.insert('auth/login.vue') - leaf.value.overrides = { + leaf.value.setOverride('', { name: 'custom-child', - } + }) expect(leaf.name).toBe('custom-child') }) it('allows a custom path', () => { const tree = createPrefixTree(DEFAULT_OPTIONS) let leaf = tree.insert('[a]-[b].vue') - leaf.value.overrides = { + leaf.value.setOverride('', { path: '/custom', - } + }) expect(leaf.path).toBe('/custom') expect(leaf.fullPath).toBe('/custom') leaf = tree.insert('auth/login.vue') - leaf.value.overrides = { + leaf.value.setOverride('', { path: '/custom-child', - } + }) expect(leaf.path).toBe('/custom-child') expect(leaf.fullPath).toBe('/custom-child') }) diff --git a/src/core/tree.ts b/src/core/tree.ts index 3842f1bf..d13968d6 100644 --- a/src/core/tree.ts +++ b/src/core/tree.ts @@ -9,6 +9,7 @@ export class TreeLeaf { * value of the node */ value: TreeLeafValue + /** * children of the node */ @@ -54,8 +55,8 @@ export class TreeLeaf { return child } - setCustomRouteBlock(routeBlock: CustomRouteBlock | undefined) { - this.value.overrides = routeBlock || {} + setCustomRouteBlock(path: string, routeBlock: CustomRouteBlock | undefined) { + this.value.setOverride(path, routeBlock) } getSortedChildren() { diff --git a/src/core/treeLeafValue.ts b/src/core/treeLeafValue.ts index 1cca3042..45837f5d 100644 --- a/src/core/treeLeafValue.ts +++ b/src/core/treeLeafValue.ts @@ -1,11 +1,17 @@ -import { RouteMeta } from 'vue-router' -import { joinPath } from './utils' +import { RouteRecordRaw } from 'vue-router' +import { CustomRouteBlock } from './customBlock' +import { joinPath, mergeRouteRecordOverride } from './utils' export const enum TreeLeafType { static, param, } +export interface RouteRecordOverride + extends Partial> { + name?: string +} + export type SubSegment = string | TreeRouteParam class _TreeLeafValueBase { @@ -32,17 +38,10 @@ class _TreeLeafValueBase { */ path: string - overrides: { - path?: string - /** - * Overridden name for this route. - */ - name?: string - /** - * Meta of the route - */ - meta?: RouteMeta - } = {} + /** + * Overrides defined by each file. The map is necessary to handle named views. + */ + private _overrides = new Map() includeLoaderGuard: boolean = false @@ -81,6 +80,20 @@ class _TreeLeafValueBase { isStatic(): this is TreeLeafValueStatic { return this._type === TreeLeafType.static } + + get overrides() { + return [...this._overrides.entries()] + .sort(([nameA], [nameB]) => + nameA === nameB ? 0 : nameA < nameB ? -1 : 1 + ) + .reduce((acc, [_path, routeBlock]) => { + return mergeRouteRecordOverride(acc, routeBlock) + }, {} as RouteRecordOverride) + } + + setOverride(path: string, routeBlock: CustomRouteBlock | undefined) { + this._overrides.set(path, routeBlock || {}) + } } export class TreeLeafValueStatic extends _TreeLeafValueBase { diff --git a/src/core/utils.ts b/src/core/utils.ts index 4252a3f0..3a6eaa6a 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,5 +1,5 @@ import { TreeLeaf } from './tree' -import type { TreeRouteParam } from './treeLeafValue' +import type { RouteRecordOverride, TreeRouteParam } from './treeLeafValue' import { pascalCase } from 'scule' export type Awaitable = T | PromiseLike @@ -157,3 +157,51 @@ export function getFileBasedRouteName(node: TreeLeaf): string { if (!node.parent) return '' return getFileBasedRouteName(node.parent) + '/' + node.value.rawSegment } + +export function mergeRouteRecordOverride( + a: RouteRecordOverride, + b: RouteRecordOverride +): RouteRecordOverride { + const merged: RouteRecordOverride = {} + const keys = [ + ...new Set([ + ...(Object.keys(a) as (keyof RouteRecordOverride)[]), + ...(Object.keys(b) as (keyof RouteRecordOverride)[]), + ]), + ] + for (const key of keys) { + if (key === 'alias') { + merged[key] = [...(a[key] || []), ...(b[key] || [])] + } else if (key === 'meta') { + merged[key] = mergeDeep(a[key] || {}, b[key] || {}) + } else { + // @ts-expect-error: TS cannot see it's the same key + merged[key] = b[key] ?? a[key] + } + } + + return merged +} + +function isObject(obj: any): obj is Record { + return obj && typeof obj === 'object' +} + +function mergeDeep(...objects: Array>): Record { + return objects.reduce((prev, obj) => { + Object.keys(obj).forEach((key) => { + const pVal = prev[key] + const oVal = obj[key] + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + prev[key] = pVal.concat(...oVal) + } else if (isObject(pVal) && isObject(oVal)) { + prev[key] = mergeDeep(pVal, oVal) + } else { + prev[key] = oVal + } + }) + + return prev + }, {}) +} diff --git a/src/data-fetching/defineLoader.ts b/src/data-fetching/defineLoader.ts index 667f02fc..ed7b370d 100644 --- a/src/data-fetching/defineLoader.ts +++ b/src/data-fetching/defineLoader.ts @@ -31,7 +31,7 @@ export function defineLoader

>( // TODO: dev only if (!entry) { if (import.meta.hot) { - // reload the page if the loader is new + // reload the page if the loader is new and we have no way to // TODO: test with webpack import.meta.hot.invalidate() }