diff --git a/e2e/fixtures/filenames/multi-extensions/about.vue b/e2e/fixtures/filenames/multi-extensions/about.vue new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/fixtures/filenames/multi-extensions/docs/[...pathMatch].vue b/e2e/fixtures/filenames/multi-extensions/docs/[...pathMatch].vue new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/fixtures/filenames/multi-extensions/docs/index.md b/e2e/fixtures/filenames/multi-extensions/docs/index.md new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/fixtures/filenames/multi-extensions/index.vue b/e2e/fixtures/filenames/multi-extensions/index.vue new file mode 100644 index 000000000..e69de29bb diff --git a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap index 0ed593db7..e81f96f1b 100644 --- a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap +++ b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap @@ -37,7 +37,7 @@ exports[`generateRouteRecord > adds children and name when folder and component ]" `; -exports[`generateRouteRecord > correctly names index.vue files 1`] = ` +exports[`generateRouteRecord > correctly names index files 1`] = ` "[ { path: '/', @@ -483,14 +483,14 @@ exports[`generateRouteRecord > nested children 2`] = ` exports[`generateRouteRecord > raw paths insertions > dedupes sync imports for the same component 1`] = ` "[ { - path: '/a/b.vue', - name: '/a/b.vue', + path: '/a/b', + name: '/a/b', component: _page_0, /* no children */ }, { - path: '/a/c.vue', - name: '/a/c.vue', + path: '/a/c', + name: '/a/c', component: _page_0, /* no children */ } diff --git a/src/codegen/generateRouteMap.spec.ts b/src/codegen/generateRouteMap.spec.ts index 7a5f24947..fd70cadb2 100644 --- a/src/codegen/generateRouteMap.spec.ts +++ b/src/codegen/generateRouteMap.spec.ts @@ -16,10 +16,10 @@ function formatExports(exports: string) { describe('generateRouteNamedMap', () => { it('works with some paths at root', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('index.vue') - tree.insert('a.vue') - tree.insert('b.vue') - tree.insert('c.vue') + tree.insert('index') + tree.insert('a') + tree.insert('b') + tree.insert('c') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, @@ -32,13 +32,13 @@ describe('generateRouteNamedMap', () => { it('adds params', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('[a].vue') - tree.insert('partial-[a].vue') - tree.insert('[[a]].vue') // optional - tree.insert('partial-[[a]].vue') // partial-optional - tree.insert('[a]+.vue') // repeated - tree.insert('[[a]]+.vue') // optional repeated - tree.insert('[...a].vue') // splat + tree.insert('[a]') + tree.insert('partial-[a]') + tree.insert('[[a]]') // optional + tree.insert('partial-[[a]]') // partial-optional + tree.insert('[a]+') // repeated + tree.insert('[[a]]+') // optional repeated + tree.insert('[...a]') // splat expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/[a]': RouteRecordInfo<'/[a]', '/:a', { a: ParamValue }, { a: ParamValue }>, @@ -68,10 +68,10 @@ describe('generateRouteNamedMap', () => { it('handles nested params in folders', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('n/[a]/index.vue') // normal - tree.insert('n/[a]/other.vue') - tree.insert('n/[a]/[b].vue') - tree.insert('n/[a]/[c]/other-[d].vue') + tree.insert('n/[a]/index') // normal + tree.insert('n/[a]/other') + tree.insert('n/[a]/[b]') + tree.insert('n/[a]/[c]/other-[d]') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/n/[a]/': RouteRecordInfo<'/n/[a]/', '/n/:a', { a: ParamValue }, { a: ParamValue }>, @@ -84,12 +84,12 @@ describe('generateRouteNamedMap', () => { it('adds nested params', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('n/[a].vue') // normal - // tree.insert('n/partial-[a].vue') // partial - tree.insert('n/[[a]].vue') // optional - tree.insert('n/[a]+.vue') // repeated - tree.insert('n/[[a]]+.vue') // optional repeated - tree.insert('n/[...a].vue') // splat + tree.insert('n/[a]') // normal + // tree.insert('n/partial-[a]') // partial + tree.insert('n/[[a]]') // optional + tree.insert('n/[a]+') // repeated + tree.insert('n/[[a]]+') // optional repeated + tree.insert('n/[...a]') // splat expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/n/[a]': RouteRecordInfo<'/n/[a]', '/n/:a', { a: ParamValue }, { a: ParamValue }>, @@ -108,9 +108,9 @@ describe('generateRouteNamedMap', () => { }) ) - tree.insert('[lang]/index.vue', 'src/pages/index.vue') - tree.insert('[lang]/a.vue', 'src/pages/a.vue') - tree.insert('[lang]/[id].vue', 'src/pages/[id].vue') + tree.insert('[lang]/index', 'src/pages/index.vue') + tree.insert('[lang]/a', 'src/pages/a.vue') + tree.insert('[lang]/[id]', 'src/pages/[id].vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { @@ -123,14 +123,14 @@ describe('generateRouteNamedMap', () => { it('nested children', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('a/a.vue') - tree.insert('a/b.vue') - tree.insert('a/c.vue') - tree.insert('b/b.vue') - tree.insert('b/c.vue') - tree.insert('b/d.vue') - tree.insert('c.vue') - tree.insert('d.vue') + tree.insert('a/a') + tree.insert('a/b') + tree.insert('a/c') + tree.insert('b/b') + tree.insert('b/c') + tree.insert('b/d') + tree.insert('c') + tree.insert('d') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/a/a': RouteRecordInfo<'/a/a', '/a/a', Record, Record>, @@ -147,9 +147,9 @@ describe('generateRouteNamedMap', () => { it('keeps parent path overrides', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const parent = tree.insert('parent.vue') - const child = tree.insert('parent/child.vue') - parent.value.setOverride('parent.vue', { path: '/' }) + const parent = tree.insert('parent') + const child = tree.insert('parent/child') + parent.value.setOverride('parent', { path: '/' }) expect(child.fullPath).toBe('/child') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { @@ -166,9 +166,9 @@ describe('generateRouteNamedMap', () => { }) ) - tree.insert('[lang]/index.vue', 'src/pages/index.vue') - tree.insert('[lang]/a.vue', 'src/pages/a.vue') - tree.insert('[lang]/[id].vue', 'src/pages/[id].vue') + tree.insert('[lang]/index', 'src/pages/index.vue') + tree.insert('[lang]/a', 'src/pages/a.vue') + tree.insert('[lang]/[id]', 'src/pages/[id].vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { @@ -178,32 +178,6 @@ describe('generateRouteNamedMap', () => { }" `) }) - - // https://github.com/posva/unplugin-vue-router/issues/274 - it('removes routeFolders.extensions from the path', () => { - const tree = new PrefixTree( - resolveOptions({ - extensions: ['.pagina.vue'], - routesFolder: [ - { src: 'src/pages', extensions: ['.page.vue'] }, - { src: 'src/paginas', extensions: ['.pagina.md'] }, - ], - }) - ) - - tree.insert('other.pagina.md', resolve('src/paginas/other.pagina.md')) - tree.insert('index.page.vue', resolve('src/pages/index.page.vue')) - tree.insert('about.page.vue', resolve('src/pages/about.page.vue')) - tree.insert('ignored.pagina.vue', resolve('src/pages/ignored.pagine.vue')) - - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` - "export interface RouteNamedMap { - '/': RouteRecordInfo<'/', '/', Record, Record>, - '/about': RouteRecordInfo<'/about', '/about', Record, Record>, - '/other': RouteRecordInfo<'/other', '/other', Record, Record>, - }" - `) - }) }) /** diff --git a/src/codegen/generateRouteRecords.spec.ts b/src/codegen/generateRouteRecords.spec.ts index 630160794..738027b30 100644 --- a/src/codegen/generateRouteRecords.spec.ts +++ b/src/codegen/generateRouteRecords.spec.ts @@ -1,7 +1,7 @@ import { basename } from 'pathe' import { describe, expect, it } from 'vitest' import { PrefixTree, TreeNode } from '../core/tree' -import { ResolvedOptions, resolveOptions } from '../options' +import { resolveOptions } from '../options' import { generateRouteRecord } from './generateRouteRecords' import { ImportsMap } from '../core/utils' @@ -31,89 +31,89 @@ describe('generateRouteRecord', () => { it('works with some paths at root', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('a.vue') - tree.insert('b.vue') - tree.insert('c.vue') + tree.insert('a', 'a.vue') + tree.insert('b', 'b.vue') + tree.insert('c', 'c.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('handles multiple named views', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('foo.vue') - tree.insert('foo@a.vue') - tree.insert('foo@b.vue') + tree.insert('foo', 'foo.vue') + tree.insert('foo@a', 'foo@a.vue') + tree.insert('foo@b', 'foo@b.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('handles single named views', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('foo@a.vue') + tree.insert('foo@a', 'foo@a.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('nested children', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('a/a.vue') - tree.insert('a/b.vue') - tree.insert('a/c.vue') - tree.insert('b/b.vue') - tree.insert('b/c.vue') - tree.insert('b/d.vue') + tree.insert('a/a', 'a/a.vue') + tree.insert('a/b', 'a/b.vue') + tree.insert('a/c', 'a/c.vue') + tree.insert('b/b', 'b/b.vue') + tree.insert('b/c', 'b/c.vue') + tree.insert('b/d', 'b/d.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() - tree.insert('c.vue') - tree.insert('d.vue') + tree.insert('c', 'c.vue') + tree.insert('d', 'd.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('adds children and name when folder and component exist', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('a/c.vue') - tree.insert('b/c.vue') - tree.insert('a.vue') - tree.insert('d.vue') + tree.insert('a/c', 'a/c.vue') + tree.insert('b/c', 'b/c.vue') + tree.insert('a', 'a.vue') + tree.insert('d', 'd.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) - it('correctly names index.vue files', () => { + it('correctly names index files', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('index.vue') - tree.insert('b/index.vue') + tree.insert('index', 'index.vue') + tree.insert('b/index', 'b/index.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('handles non nested routes', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('users.vue') - tree.insert('users/index.vue') - tree.insert('users/other.vue') - tree.insert('users.not-nested.vue') - tree.insert('users/[id]/index.vue') - tree.insert('users/[id]/other.vue') - tree.insert('users/[id].vue') - tree.insert('users/[id].not-nested.vue') - tree.insert('users.[id].also-not-nested.vue') + tree.insert('users', 'users.vue') + tree.insert('users/index', 'users/index.vue') + tree.insert('users/other', 'users/other.vue') + tree.insert('users.not-nested', 'users.not-nested.vue') + tree.insert('users/[id]/index', 'users/[id]/index.vue') + tree.insert('users/[id]/other', 'users/[id]/other.vue') + tree.insert('users/[id]', 'users/[id].vue') + tree.insert('users/[id].not-nested', 'users/[id].not-nested.vue') + tree.insert('users.[id].also-not-nested', 'users.[id].also-not-nested.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('removes trailing slashes', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('users/index.vue') - tree.insert('users/other.vue') - tree.insert('nested.vue') - tree.insert('nested/index.vue') - tree.insert('nested/other.vue') + tree.insert('users/index', 'users/index.vue') + tree.insert('users/other', 'users/other.vue') + tree.insert('nested', 'nested.vue') + tree.insert('nested/index', 'nested/index.vue') + tree.insert('nested/other', 'nested/other.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('generate static imports', () => { - const options: ResolvedOptions = { + const options = resolveOptions({ ...DEFAULT_OPTIONS, importMode: 'sync', - } as const + }) const tree = new PrefixTree(options) - tree.insert('a.vue') - tree.insert('b.vue') - tree.insert('nested/file/c.vue') + tree.insert('a', 'a.vue') + tree.insert('b', 'b.vue') + tree.insert('nested/file/c', 'nested/file/c.vue') const importList = new ImportsMap() expect(generateRouteRecord(tree, options, importList)).toMatchSnapshot() @@ -121,16 +121,15 @@ describe('generateRouteRecord', () => { }) it('generate custom imports', () => { - const options: ResolvedOptions = { - ...DEFAULT_OPTIONS, + const options = resolveOptions({ importMode: (filepath) => basename(filepath) === 'a.vue' ? 'sync' : 'async', - } + }) const tree = new PrefixTree(options) - tree.insert('a.vue') - tree.insert('b.vue') - tree.insert('nested/file/c.vue') + tree.insert('a', 'a.vue') + tree.insert('b', 'b.vue') + tree.insert('nested/file/c', 'nested/file/c.vue') const importList = new ImportsMap() expect(generateRouteRecord(tree, options, importList)).toMatchSnapshot() @@ -140,33 +139,36 @@ describe('generateRouteRecord', () => { describe('names', () => { it('creates single word names', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('index.vue') - tree.insert('about.vue') - tree.insert('users/index.vue') - tree.insert('users/[id].vue') - tree.insert('users/[id]/edit.vue') - tree.insert('users/new.vue') + tree.insert('index', 'index.vue') + tree.insert('about', 'about.vue') + tree.insert('users/index', 'users/index.vue') + tree.insert('users/[id]', 'users/[id].vue') + tree.insert('users/[id]/edit', 'users/[id]/edit.vue') + tree.insert('users/new', 'users/new.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('creates multi word names', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('index.vue') - tree.insert('my-users.vue') - tree.insert('MyPascalCaseUsers.vue') - tree.insert('some-nested/file-with-[id]-in-the-middle.vue') + tree.insert('index', 'index.vue') + tree.insert('my-users', 'my-users.vue') + tree.insert('MyPascalCaseUsers', 'MyPascalCaseUsers.vue') + tree.insert( + 'some-nested/file-with-[id]-in-the-middle', + 'some-nested/file-with-[id]-in-the-middle.vue' + ) expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('works with nested views', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('index.vue') - tree.insert('users.vue') - tree.insert('users/index.vue') - tree.insert('users/[id]/edit.vue') - tree.insert('users/[id].vue') + tree.insert('index', 'index.vue') + tree.insert('users', 'users.vue') + tree.insert('users/index', 'users/index.vue') + tree.insert('users/[id]/edit', 'users/[id]/edit.vue') + tree.insert('users/[id]', 'users/[id].vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) @@ -175,8 +177,8 @@ describe('generateRouteRecord', () => { describe('route block', () => { it('adds meta data', async () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - node.setCustomRouteBlock('index.vue', { + const node = tree.insert('index', 'index.vue') + node.setCustomRouteBlock('index', { meta: { auth: true, title: 'Home', @@ -188,14 +190,14 @@ describe('generateRouteRecord', () => { it('merges multiple meta properties', async () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - node.setCustomRouteBlock('index.vue', { + const node = tree.insert('index', 'index.vue') + node.setCustomRouteBlock('index', { path: '/custom', meta: { one: true, }, }) - node.setCustomRouteBlock('index@named.vue', { + node.setCustomRouteBlock('index@named', { name: 'hello', meta: { two: true, @@ -207,20 +209,20 @@ describe('generateRouteRecord', () => { it('merges regardless of order', async () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - node.setCustomRouteBlock('index.vue', { + const node = tree.insert('index', 'index.vue') + node.setCustomRouteBlock('index', { name: 'a', }) - node.setCustomRouteBlock('index@named.vue', { + node.setCustomRouteBlock('index@named', { name: 'b', }) const one = generateRouteRecordSimple(tree) - node.setCustomRouteBlock('index@named.vue', { + node.setCustomRouteBlock('index@named', { name: 'b', }) - node.setCustomRouteBlock('index.vue', { + node.setCustomRouteBlock('index', { name: 'a', }) @@ -231,18 +233,18 @@ describe('generateRouteRecord', () => { it('handles named views with empty route blocks', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - const n2 = tree.insert('index@named.vue') + const node = tree.insert('index', 'index.vue') + const n2 = tree.insert('index@named', 'index@named.vue') expect(node).toBe(n2) - // coming from index.vue - node.setCustomRouteBlock('index.vue', { + // coming from index + node.setCustomRouteBlock('index', { meta: { auth: true, title: 'Home', }, }) - // coming from index@named.vue (no route block) - node.setCustomRouteBlock('index@named.vue', undefined) + // coming from index@named (no route block) + node.setCustomRouteBlock('index@named', undefined) expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) @@ -250,11 +252,11 @@ describe('generateRouteRecord', () => { // FIXME: allow aliases it.todo('merges alias properties', async () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - node.setCustomRouteBlock('index.vue', { + const node = tree.insert('index', 'index.vue') + node.setCustomRouteBlock('index', { alias: '/one', }) - node.setCustomRouteBlock('index@named.vue', { + node.setCustomRouteBlock('index@named', { alias: ['/two', '/three'], }) @@ -263,7 +265,7 @@ describe('generateRouteRecord', () => { { path: '/', name: '/', - component: () => import('index.vue'), + component: () => import('index'), /* no props */ /* no children */ } @@ -273,14 +275,14 @@ describe('generateRouteRecord', () => { it('merges deep meta properties', async () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - const node = tree.insert('index.vue') - node.setCustomRouteBlock('index.vue', { + const node = tree.insert('index', 'index.vue') + node.setCustomRouteBlock('index', { meta: { a: { one: 1 }, b: { a: [2] }, }, }) - node.setCustomRouteBlock('index@named.vue', { + node.setCustomRouteBlock('index@named', { meta: { a: { two: 1 }, b: { a: [3] }, @@ -303,7 +305,7 @@ describe('generateRouteRecord', () => { it('works with mixed nodes', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) tree.insertParsedPath('a', 'a.vue') - tree.insert('b.vue') + tree.insert('b', 'b.vue') tree.insertParsedPath('c', 'c.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) @@ -318,20 +320,21 @@ describe('generateRouteRecord', () => { it('do not nest raw segments with file based', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) - tree.insert('a/b.vue') + tree.insert('a/b', 'a/b.vue') // should be separated tree.insertParsedPath('a/b/c', 'a.vue') expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) it('dedupes sync imports for the same component', () => { - const tree = new PrefixTree({ - ...DEFAULT_OPTIONS, - importMode: 'sync', - }) - - tree.insertParsedPath('a/b.vue', 'a.vue') - tree.insertParsedPath('a/c.vue', 'a.vue') + const tree = new PrefixTree( + resolveOptions({ + importMode: 'sync', + }) + ) + + tree.insertParsedPath('a/b', 'a.vue') + tree.insertParsedPath('a/c', 'a.vue') // what matters is that the import name is reused _page_0 expect(generateRouteRecordSimple(tree)).toMatchSnapshot() diff --git a/src/core/RoutesFolderWatcher.ts b/src/core/RoutesFolderWatcher.ts index 23ca16abe..df822cc21 100644 --- a/src/core/RoutesFolderWatcher.ts +++ b/src/core/RoutesFolderWatcher.ts @@ -53,7 +53,14 @@ export class RoutesFolderWatcher { handler({ filePath, - routePath: asRoutePath({ src: this.src, path: this.path }, filePath), + routePath: asRoutePath( + { + src: this.src, + path: this.path, + extensions: this.extensions, + }, + filePath + ), }) }) return this diff --git a/src/core/context.ts b/src/core/context.ts index c37bd6037..53432bbcf 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -123,6 +123,7 @@ export function createRoutesContext(options: ResolvedOptions) { ) { logger.log(`added "${routePath}" for "${filePath}"`) // TODO: handle top level named view HMR + const node = routeTree.insert(routePath, filePath) await writeRouteInfoToNode(node, filePath) diff --git a/src/core/tree.spec.ts b/src/core/tree.spec.ts index 8390f5be7..266a0962f 100644 --- a/src/core/tree.spec.ts +++ b/src/core/tree.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { DEFAULT_OPTIONS, resolveOptions } from '../options' import { PrefixTree } from './tree' import { TreeNodeType } from './treeNodeValue' +import { resolve } from 'pathe' describe('Tree', () => { const RESOLVED_OPTIONS = resolveOptions(DEFAULT_OPTIONS) @@ -12,7 +13,7 @@ describe('Tree', () => { it('creates a tree with a single static path', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('foo.vue') + tree.insert('foo', 'foo.vue') expect(tree.children.size).toBe(1) const child = tree.children.get('foo')! expect(child).toBeDefined() @@ -26,7 +27,7 @@ describe('Tree', () => { it('creates a tree with a single param', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('[id].vue') + tree.insert('[id]', '[id].vue') expect(tree.children.size).toBe(1) const child = tree.children.get('[id]')! expect(child).toBeDefined() @@ -41,8 +42,8 @@ describe('Tree', () => { it('separate param names from static segments', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('[id]_a') - tree.insert('[a]e[b]f') + tree.insert('[id]_a', '[id]_a.vue') + tree.insert('[a]e[b]f', '[a]e[b]f.vue') expect(tree.children.get('[id]_a')!.value).toMatchObject({ rawSegment: '[id]_a', params: [{ paramName: 'id' }], @@ -60,7 +61,7 @@ describe('Tree', () => { it('creates params in nested files', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - const nestedId = tree.insert('nested/[id].vue') + const nestedId = tree.insert('nested/[id]', 'nested/[id].vue') expect(nestedId.value.isParam()).toBe(true) expect(nestedId.params).toEqual([ @@ -73,7 +74,7 @@ describe('Tree', () => { }), ]) - const nestedAId = tree.insert('nested/a/[id].vue') + const nestedAId = tree.insert('nested/a/[id]', 'nested/a/[id].vue') expect(nestedAId.value.isParam()).toBe(true) expect(nestedAId.params).toEqual([ expect.objectContaining({ @@ -89,7 +90,7 @@ describe('Tree', () => { it('creates params in nested folders', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - let node = tree.insert('nested/[id]/index.vue') + let node = tree.insert('nested/[id]/index', 'nested/[id]/index.vue') const id = tree.children.get('nested')!.children.get('[id]')! expect(id.value.isParam()).toBe(true) expect(id.params).toEqual([ @@ -113,7 +114,7 @@ describe('Tree', () => { }), ]) - node = tree.insert('nested/[a]/other.vue') + node = tree.insert('nested/[a]/other', 'nested/[a]/other.vue') expect(node.value.isParam()).toBe(false) expect(node.params).toEqual([ expect.objectContaining({ @@ -125,7 +126,7 @@ describe('Tree', () => { }), ]) - node = tree.insert('nested/a/[id]/index.vue') + node = tree.insert('nested/a/[id]/index', 'nested/a/[id]/index.vue') expect(node.value.isParam()).toBe(false) expect(node.params).toEqual([ expect.objectContaining({ @@ -140,7 +141,7 @@ describe('Tree', () => { it('handles repeatable params one or more', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('[id]+.vue') + tree.insert('[id]+', '[id]+.vue') expect(tree.children.get('[id]+')!.value).toMatchObject({ rawSegment: '[id]+', params: [ @@ -158,7 +159,7 @@ describe('Tree', () => { it('handles repeatable params zero or more', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('[[id]]+.vue') + tree.insert('[[id]]+', '[[id]]+.vue') expect(tree.children.get('[[id]]+')!.value).toMatchObject({ rawSegment: '[[id]]+', params: [ @@ -176,7 +177,7 @@ describe('Tree', () => { it('handles optional params', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('[[id]].vue') + tree.insert('[[id]]', '[[id]].vue') expect(tree.children.get('[[id]]')!.value).toMatchObject({ rawSegment: '[[id]]', params: [ @@ -194,17 +195,17 @@ describe('Tree', () => { it('handles named views', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('index.vue') - tree.insert('index@a.vue') - tree.insert('index@b.vue') - tree.insert('nested/foo@a.vue') - tree.insert('nested/foo@b.vue') - tree.insert('nested/[id]@a.vue') - tree.insert('nested/[id]@b.vue') - tree.insert('not.nested.path@a.vue') - tree.insert('not.nested.path@b.vue') - tree.insert('deep/not.nested.path@a.vue') - tree.insert('deep/not.nested.path@b.vue') + tree.insert('index', 'index.vue') + tree.insert('index@a', 'index@a.vue') + tree.insert('index@b', 'index@b.vue') + tree.insert('nested/foo@a', 'nested/foo@a.vue') + tree.insert('nested/foo@b', 'nested/foo@b.vue') + tree.insert('nested/[id]@a', 'nested/[id]@a.vue') + tree.insert('nested/[id]@b', 'nested/[id]@b.vue') + tree.insert('not.nested.path@a', 'not.nested.path@a.vue') + tree.insert('not.nested.path@b', 'not.nested.path@b.vue') + tree.insert('deep/not.nested.path@a', 'deep/not.nested.path@a.vue') + tree.insert('deep/not.nested.path@b', 'deep/not.nested.path@b.vue') expect([...tree.children.get('index')!.value.components.keys()]).toEqual([ 'default', 'a', @@ -235,7 +236,7 @@ describe('Tree', () => { it('handles single named views that are not default', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('index@a.vue') + tree.insert('index@a', 'index@a.vue') expect([...tree.children.get('index')!.value.components.keys()]).toEqual([ 'a', ]) @@ -243,20 +244,22 @@ describe('Tree', () => { it('removes the node after all named views', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('index.vue') - tree.insert('index@a.vue') + tree.insert('index', 'index.vue') + tree.insert('index@a', 'index@a.vue') expect(tree.children.get('index')).toBeDefined() - tree.remove('index@a.vue') + tree.remove('index@a') expect(tree.children.get('index')).toBeDefined() - tree.remove('index.vue') + tree.remove('index') expect(tree.children.get('index')).toBeUndefined() }) it('can remove itself from the tree', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('index.vue').insert('nested.vue') - tree.insert('a.vue').insert('nested.vue') - tree.insert('b.vue') + tree + .insert('index', 'index.vue') + .insert('nested', resolve('index/nested.vue')) + tree.insert('a', 'a.vue').insert('nested', resolve('a/nested.vue')) + tree.insert('b', 'b.vue') expect(tree.children.size).toBe(3) tree.children.get('a')!.delete() expect(tree.children.size).toBe(2) @@ -266,10 +269,10 @@ describe('Tree', () => { it('handles multiple params', () => { 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') - tree.insert('nested/o[a][b]c.vue') + tree.insert('[a]-[b]', '[a]-[b].vue') + tree.insert('o[a]-[b]c', 'o[a]-[b]c.vue') + tree.insert('o[a][b]c', 'o[a][b]c.vue') + tree.insert('nested/o[a][b]c', 'nested/o[a][b]c.vue') expect(tree.children.size).toBe(4) expect(tree.children.get('[a]-[b]')!.value).toMatchObject({ pathSegment: ':a-:b', @@ -278,9 +281,9 @@ describe('Tree', () => { it('creates a tree of nested routes', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('index.vue') - tree.insert('a/index.vue') - tree.insert('a/b/index.vue') + tree.insert('index', 'index.vue') + tree.insert('a/index', 'a/index.vue') + tree.insert('a/b/index', 'a/b/index.vue') expect(Array.from(tree.children.keys())).toEqual(['index', 'a']) const index = tree.children.get('index')! expect(index.value).toMatchObject({ @@ -305,7 +308,7 @@ describe('Tree', () => { path: '/a', }) - tree.insert('a.vue') + tree.insert('a', 'a.vue') expect(a.value.components.get('default')).toBe('a.vue') expect(a.value).toMatchObject({ rawSegment: 'a', @@ -315,7 +318,7 @@ describe('Tree', () => { it('handles a modifier for single params', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('[id]+.vue') + tree.insert('[id]+', '[id]+.vue') expect(tree.children.size).toBe(1) const child = tree.children.get('[id]+')! expect(child).toBeDefined() @@ -331,9 +334,9 @@ describe('Tree', () => { it('removes nodes', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('foo.vue') - tree.insert('[id].vue') - tree.remove('foo.vue') + tree.insert('foo', 'foo.vue') + tree.insert('[id]', '[id].vue') + tree.remove('foo') expect(tree.children.size).toBe(1) const child = tree.children.get('[id]')! expect(child).toBeDefined() @@ -348,19 +351,19 @@ describe('Tree', () => { it('removes empty folders', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('a/b/c/d.vue') + tree.insert('a/b/c/d', 'a/b/c/d.vue') expect(tree.children.size).toBe(1) - tree.remove('a/b/c/d.vue') + tree.remove('a/b/c/d') expect(tree.children.size).toBe(0) }) it('insert returns the node', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - const a = tree.insert('a.vue') + const a = tree.insert('a', 'a.vue') expect(tree.children.get('a')).toBe(a) - const bC = tree.insert('b/c.vue') + const bC = tree.insert('b/c', 'b/c.vue') expect(tree.children.get('b')!.children.get('c')).toBe(bC) - const bCD = tree.insert('b/c/d.vue') + const bCD = tree.insert('b/c/d', 'b/c/d.vue') expect(tree.children.get('b')!.children.get('c')!.children.get('d')).toBe( bCD ) @@ -368,14 +371,14 @@ describe('Tree', () => { it('keeps parent with file but no children', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('a/b/c/d.vue') - tree.insert('a/b.vue') + tree.insert('a/b/c/d', 'a/b/c/d.vue') + tree.insert('a/b', 'a/b.vue') expect(tree.children.size).toBe(1) const child = tree.children.get('a')!.children.get('b')! expect(child).toBeDefined() expect(child.children.size).toBe(1) - tree.remove('a/b/c/d.vue') + tree.remove('a/b/c/d') expect(tree.children.size).toBe(1) expect(tree.children.get('a')!.children.size).toBe(1) expect(child.children.size).toBe(0) @@ -383,13 +386,13 @@ describe('Tree', () => { it('allows a custom name', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - let node = tree.insert('[a]-[b].vue') + let node = tree.insert('[a]-[b]', '[a]-[b].vue') node.value.setOverride('', { name: 'custom', }) expect(node.name).toBe('custom') - node = tree.insert('auth/login.vue') + node = tree.insert('auth/login', 'auth/login.vue') node.value.setOverride('', { name: 'custom-child', }) @@ -398,14 +401,14 @@ describe('Tree', () => { it('allows a custom path', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - let node = tree.insert('[a]-[b].vue') + let node = tree.insert('[a]-[b]', '[a]-[b].vue') node.value.setOverride('', { path: '/custom', }) expect(node.path).toBe('/custom') expect(node.fullPath).toBe('/custom') - node = tree.insert('auth/login.vue') + node = tree.insert('auth/login', 'auth/login.vue') node.value.setOverride('', { path: '/custom-child', }) @@ -415,8 +418,8 @@ describe('Tree', () => { it('removes trailing slash from path but not from name', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('a/index.vue') - tree.insert('a/a.vue') + tree.insert('a/index', 'a/index.vue') + tree.insert('a/a', 'a/a.vue') let child = tree.children.get('a')! expect(child).toBeDefined() expect(child.fullPath).toBe('/a') @@ -427,49 +430,17 @@ describe('Tree', () => { expect(child.fullPath).toBe('/a') // it stays the same with a parent component in the parent route record - tree.insert('a.vue') + tree.insert('a', 'a.vue') child = tree.children.get('a')!.children.get('index')! expect(child).toBeDefined() expect(child.name).toBe('/a/') expect(child.fullPath).toBe('/a') }) - it('handles long extensions', () => { - const tree = new PrefixTree({ - ...RESOLVED_OPTIONS, - extensions: ['.page.vue'], - }) - tree.insert('a.page.vue') - tree.insert('nested/b/c.page.vue') - expect(tree.children.size).toBe(2) - - const a = tree.children.get('a')! - expect(a).toBeDefined() - expect(a.value.components.get('default')).toBe('a.page.vue') - expect(a.fullPath).toBe('/a') - - const nested = tree.children.get('nested')! - expect(nested).toBeDefined() - expect(nested.children.size).toBe(1) - const b = nested.children.get('b')! - expect(b).toBeDefined() - expect(b.children.size).toBe(1) - const c = b.children.get('c')! - expect(c).toBeDefined() - expect(c.value.components.get('default')).toBe('nested/b/c.page.vue') - expect(c.fullPath).toBe('/nested/b/c') - - tree.insert('a/nested.page.vue') - const aNested = a.children.get('nested')! - expect(aNested).toBeDefined() - expect(aNested.value.components.get('default')).toBe('a/nested.page.vue') - expect(aNested.fullPath).toBe('/a/nested') - }) - describe('dot nesting', () => { it('transforms dots into nested routes by default', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) - tree.insert('users.new.vue') + tree.insert('users.new', 'users.new.vue') expect(tree.children.size).toBe(1) const users = tree.children.get('users.new')! expect(users.value).toMatchObject({ @@ -487,7 +458,7 @@ describe('Tree', () => { dotNesting: false, }, }) - tree.insert('1.2.3-lesson.vue') + tree.insert('1.2.3-lesson', '1.2.3-lesson.vue') expect(tree.children.size).toBe(1) const lesson = tree.children.get('1.2.3-lesson')! diff --git a/src/core/tree.ts b/src/core/tree.ts index aa4ac8d12..1dcfbe6d3 100644 --- a/src/core/tree.ts +++ b/src/core/tree.ts @@ -1,15 +1,10 @@ -import { - resolveOverridableOption, - type ResolvedOptions, - type RoutesFolderOption, -} from '../options' +import { type ResolvedOptions } from '../options' import { createTreeNodeValue, TreeNodeValueOptions, TreeRouteParam, } from './treeNodeValue' import type { TreeNodeValue } from './treeNodeValue' -import { trimExtension } from './utils' import { CustomRouteBlock } from './customBlock' import { RouteMeta } from 'vue-router' @@ -68,33 +63,21 @@ export class TreeNode { /** * Adds a path to the tree. `path` cannot start with a `/`. * - * @param path - path segment to insert. **It must contain the file extension** this allows to - * differentiate between folders and files. - * @param filePath - file path, defaults to path for convenience and testing + * @param path - path segment to insert. **It shouldn't contain the file extension** + * @param filePath - file path, must be a file (not a folder) */ - insert(path: string, filePath: string = path): TreeNode { - // find the `routesFolder` resolved option that matches the filepath - const folderOptions = findFolderOptions(this.options.routesFolder, filePath) - - const { tail, segment, viewName, isComponent } = splitFilePath( - path, - // use the correct extensions for the folder - resolveOverridableOption( - this.options.extensions, - folderOptions?.extensions - ) - ) + insert(path: string, filePath: string): TreeNode { + const { tail, segment, viewName } = splitFilePath(path) if (!this.children.has(segment)) { this.children.set(segment, new TreeNode(this.options, segment, this)) } // TODO: else error or still override? const child = this.children.get(segment)! - if (isComponent) { + // we reached the end of the filePath, therefore it's a component + if (!tail) { child.value.components.set(viewName, filePath) - } - - if (tail) { + } else { return child.insert(tail, filePath) } return child @@ -166,21 +149,14 @@ export class TreeNode { } /** - * Remove a route from the tree. The path shouldn't start with a `/` but it can be a nested one. e.g. `foo/bar.vue`. + * Remove a route from the tree. The path shouldn't start with a `/` but it can be a nested one. e.g. `foo/bar`. * The `path` should be relative to the page folder. * * @param path - path segment of the file */ remove(path: string) { - const folderOptions = findFolderOptions(this.options.routesFolder, path) // TODO: rename remove to removeChild - const { tail, segment, viewName, isComponent } = splitFilePath( - path, - resolveOverridableOption( - this.options.extensions, - folderOptions?.extensions - ) - ) + const { tail, segment, viewName } = splitFilePath(path) const child = this.children.get(segment) if (!child) { @@ -197,9 +173,7 @@ export class TreeNode { } } else { // it can only be component because we only listen for removed files, not folders - if (isComponent) { - child.value.components.delete(viewName) - } + child.value.components.delete(viewName) // this is the file we wanted to remove if (child.children.size === 0 && child.value.components.size === 0) { this.children.delete(segment) @@ -299,7 +273,7 @@ export class PrefixTree extends TreeNode { super(options, '') } - override insert(path: string, filePath: string = path) { + override insert(path: string, filePath: string) { const node = super.insert(path, filePath) this.map.set(filePath, node) @@ -334,16 +308,12 @@ export class PrefixTree extends TreeNode { * * @param filePath - filePath to split */ -function splitFilePath(filePath: string, extensions: string[]) { +function splitFilePath(filePath: string) { const slashPos = filePath.indexOf('/') let head = slashPos < 0 ? filePath : filePath.slice(0, slashPos) const tail = slashPos < 0 ? '' : filePath.slice(slashPos + 1) let segment = head - // only the last segment can be a filename with an extension - if (!tail) { - segment = trimExtension(head, extensions) - } let viewName = 'default' const namedSeparatorPos = segment.indexOf('@') @@ -353,27 +323,9 @@ function splitFilePath(filePath: string, extensions: string[]) { segment = segment.slice(0, namedSeparatorPos) } - // this means we effectively trimmed an extension - const isComponent = segment !== head - return { segment, tail, viewName, - isComponent, } } - -/** - * Find the folder options that match the file path. - * - * @param folderOptions `options.routesFolder` option - * @param filePath resolved file path - * @returns - */ -function findFolderOptions( - folderOptions: RoutesFolderOption[], - filePath: string -): RoutesFolderOption | undefined { - return folderOptions.find((folder) => filePath.includes(folder.src)) -} diff --git a/src/core/utils.ts b/src/core/utils.ts index 955fd5f7c..171ff7d2b 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -3,7 +3,7 @@ import type { RouteRecordOverride, TreeRouteParam } from './treeNodeValue' import { pascalCase } from 'scule' import { ResolvedOptions, - RoutesFolderOption, + RoutesFolderOptionResolved, _OverridableOption, } from '../options' @@ -232,22 +232,30 @@ function mergeDeep(...objects: Array>): Record { } /** - * Returns a route path to be used by the router with any defined prefix from an absolute path to a file. + * Returns a route path to be used by the router with any defined prefix from an absolute path to a file. Since it + * returns a route path, it will remove the extension from the file. * * @param options - RoutesFolderOption to apply * @param filePath - absolute path to file * @returns a route path to be used by the router with any defined prefix */ export function asRoutePath( - { src, path = '' }: RoutesFolderOption, + { + src, + path = '', + extensions, + }: Pick, filePath: string ) { - return typeof path === 'string' - ? // add the path prefix if any - path + - // remove the absolute path to the pages folder - filePath.slice(src.length + 1) - : path(filePath) + return trimExtension( + typeof path === 'string' + ? // add the path prefix if any + path + + // remove the absolute path to the pages folder + filePath.slice(src.length + 1) + : path(filePath), + extensions + ) } /**