From f0cfb362f161b696f08aa6dcfe19bbd373328b8c Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 30 Sep 2022 10:41:03 +0200 Subject: [PATCH] fix(types): remove trailing slash in path for nested routes Fix #70 --- playground/typed-router.d.ts | 10 ++-- .../generateRouteRecords.spec.ts.snap | 49 +++++++++++++++++++ src/codegen/generateRouteMap.spec.ts | 4 +- src/codegen/generateRouteRecords.spec.ts | 10 ++++ src/core/tree.spec.ts | 24 ++++++++- src/core/treeNodeValue.ts | 4 +- src/core/utils.ts | 4 +- 7 files changed, 95 insertions(+), 10 deletions(-) diff --git a/playground/typed-router.d.ts b/playground/typed-router.d.ts index b1eac4714..613b4255f 100644 --- a/playground/typed-router.d.ts +++ b/playground/typed-router.d.ts @@ -40,7 +40,7 @@ declare module 'vue-router/auto/routes' { '/[...path]': RouteRecordInfo<'/[...path]', '/:path(.*)', { path: ParamValue }, { path: ParamValue }>, '/about': RouteRecordInfo<'/about', '/about', Record, Record>, '/articles': RouteRecordInfo<'/articles', '/articles', 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>, @@ -49,17 +49,17 @@ declare module 'vue-router/auto/routes' { '/deep/nesting/works/custom-path': RouteRecordInfo<'/deep/nesting/works/custom-path', '/deep-surprise-:id(\d+)', Record, Record>, 'deep a rebel': RouteRecordInfo<'deep a rebel', '/deep/nesting/works/custom-name', Record, Record>, '/deep/nesting/works/too': RouteRecordInfo<'/deep/nesting/works/too', '/deep/nesting/works/too', Record, Record>, - '/docs/:lang/': RouteRecordInfo<'/docs/:lang/', '/docs/:lang/', Record, Record>, + '/docs/:lang/': RouteRecordInfo<'/docs/:lang/', '/docs/:lang', Record, Record>, '/docs/:lang/about': RouteRecordInfo<'/docs/:lang/about', '/docs/:lang/about', Record, Record>, 'the most rebel': RouteRecordInfo<'the most rebel', '/most-rebel', Record, Record>, '/multiple-[a]-[b]-params': RouteRecordInfo<'/multiple-[a]-[b]-params', '/multiple-:a-:b-params', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }>, '/my-optional-[[slug]]': RouteRecordInfo<'/my-optional-[[slug]]', '/my-optional-:slug?', { slug?: ParamValueZeroOrOne }, { slug?: ParamValueZeroOrOne }>, - '/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]]/': 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 }>, '/partial-[name]': RouteRecordInfo<'/partial-[name]', '/partial-:name', { name: ParamValue }, { name: ParamValue }>, '/custom-path': RouteRecordInfo<'/custom-path', '/surprise-:id(\d+)', Record, Record>, - '/users/': RouteRecordInfo<'/users/', '/users/', Record, Record>, + '/users/': RouteRecordInfo<'/users/', '/users', Record, Record>, '/users/[id]': RouteRecordInfo<'/users/[id]', '/users/:id', { id: ParamValue }, { id: ParamValue }>, '/users/[id].edit': RouteRecordInfo<'/users/[id].edit', '/users/:id/edit', { id: ParamValue }, { id: ParamValue }>, '/users/nested.route.deep': RouteRecordInfo<'/users/nested.route.deep', '/users/nested/route/deep', Record, Record>, diff --git a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap index 1ca6d5e9e..c8a496275 100644 --- a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap +++ b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap @@ -546,6 +546,55 @@ exports[`generateRouteRecord > nested children 2`] = ` ]" `; +exports[`generateRouteRecord > removes trailing slashes 1`] = ` +"[ + { + path: '/nested', + name: '/nested', + component: () => import('nested.vue'), + /* no props */ + children: [ + { + path: '', + name: '/nested/', + component: () => import('nested/index.vue'), + /* no props */ + /* no children */ + }, + { + path: 'other', + name: '/nested/other', + component: () => import('nested/other.vue'), + /* no props */ + /* no children */ + } + ], + }, + { + path: '/users', + /* no name */ + /* no component */ + /* no props */ + children: [ + { + path: '', + name: '/users/', + component: () => import('users/index.vue'), + /* no props */ + /* no children */ + }, + { + path: 'other', + name: '/users/other', + component: () => import('users/other.vue'), + /* no props */ + /* no children */ + } + ], + } +]" +`; + exports[`generateRouteRecord > route block > adds meta data 1`] = ` "[ { diff --git a/src/codegen/generateRouteMap.spec.ts b/src/codegen/generateRouteMap.spec.ts index 76932ce21..8c0f09ff8 100644 --- a/src/codegen/generateRouteMap.spec.ts +++ b/src/codegen/generateRouteMap.spec.ts @@ -15,11 +15,13 @@ function formatExports(exports: string) { describe('generateRouteNamedMap', () => { it('works with some paths at root', () => { const tree = createPrefixTree(DEFAULT_OPTIONS) + tree.insert('index.vue') tree.insert('a.vue') tree.insert('b.vue') tree.insert('c.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { + '/': RouteRecordInfo<'/', '/', Record, Record>, '/a': RouteRecordInfo<'/a', '/a', Record, Record>, '/b': RouteRecordInfo<'/b', '/b', Record, Record>, '/c': RouteRecordInfo<'/c', '/c', Record, Record>, @@ -57,7 +59,7 @@ describe('generateRouteNamedMap', () => { tree.insert('n/[a]/[c]/other-[d].vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/n/[a]/': RouteRecordInfo<'/n/[a]/', '/n/:a/', { a: ParamValue }, { a: ParamValue }>, + '/n/[a]/': RouteRecordInfo<'/n/[a]/', '/n/:a', { a: ParamValue }, { a: ParamValue }>, '/n/[a]/[b]': RouteRecordInfo<'/n/[a]/[b]', '/n/:a/:b', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }>, '/n/[a]/[c]/other-[d]': RouteRecordInfo<'/n/[a]/[c]/other-[d]', '/n/:a/:c/other-:d', { a: ParamValue, c: ParamValue, d: ParamValue }, { a: ParamValue, c: ParamValue, d: ParamValue }>, '/n/[a]/other': RouteRecordInfo<'/n/[a]/other', '/n/:a/other', { a: ParamValue }, { a: ParamValue }>, diff --git a/src/codegen/generateRouteRecords.spec.ts b/src/codegen/generateRouteRecords.spec.ts index 767a1eb80..754da602a 100644 --- a/src/codegen/generateRouteRecords.spec.ts +++ b/src/codegen/generateRouteRecords.spec.ts @@ -87,6 +87,16 @@ describe('generateRouteRecord', () => { expect(generateRouteRecordSimple(tree)).toMatchSnapshot() }) + it('removes trailing slashes', () => { + const tree = createPrefixTree(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') + expect(generateRouteRecordSimple(tree)).toMatchSnapshot() + }) + it('generate static imports', () => { const options: ResolvedOptions = { ...DEFAULT_OPTIONS, diff --git a/src/core/tree.spec.ts b/src/core/tree.spec.ts index af63dc664..23a19c4de 100644 --- a/src/core/tree.spec.ts +++ b/src/core/tree.spec.ts @@ -199,6 +199,7 @@ describe('Tree', () => { const index = tree.children.get('index')! expect(index.value).toMatchObject({ rawSegment: '', + // the root should have a '/' instead of '' for the autocompletion path: '/', }) expect(index).toBeDefined() @@ -215,7 +216,7 @@ describe('Tree', () => { expect(Array.from(aIndex.children.keys())).toEqual([]) expect(aIndex.value).toMatchObject({ rawSegment: '', - path: '/a/', + path: '/a', }) tree.insert('a.vue') @@ -325,4 +326,25 @@ describe('Tree', () => { expect(node.path).toBe('/custom-child') expect(node.fullPath).toBe('/custom-child') }) + + it('removes trailing slash from path but not from name', () => { + const tree = createPrefixTree(DEFAULT_OPTIONS) + tree.insert('a/index.vue') + tree.insert('a/a.vue') + let child = tree.children.get('a')! + expect(child).toBeDefined() + expect(child.fullPath).toBe('/a') + + child = tree.children.get('a')!.children.get('index')! + expect(child).toBeDefined() + expect(child.name).toBe('/a/') + expect(child.fullPath).toBe('/a') + + // it stays the same with a parent component in the parent route record + tree.insert('a.vue') + child = tree.children.get('a')!.children.get('index')! + expect(child).toBeDefined() + expect(child.name).toBe('/a/') + expect(child.fullPath).toBe('/a') + }) }) diff --git a/src/core/treeNodeValue.ts b/src/core/treeNodeValue.ts index 720857832..ea7a8c53c 100644 --- a/src/core/treeNodeValue.ts +++ b/src/core/treeNodeValue.ts @@ -65,8 +65,10 @@ class _TreeNodeValueBase { this.rawSegment = rawSegment this.pathSegment = pathSegment this.subSegments = subSegments + const parentPath = parent?.path this.path = - !parent?.path && this.pathSegment === '' + // both the root record and the index record have a path of / + (!parentPath || parentPath === '/') && this.pathSegment === '' ? '/' : joinPath(parent?.path || '', this.pathSegment) this.filePaths = new Map() diff --git a/src/core/utils.ts b/src/core/utils.ts index b040d9be6..6b19b4f2a 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -94,8 +94,8 @@ export function joinPath(...paths: string[]): string { for (const path of paths) { result = result.replace(TRAILING_SLASH_RE, '') + - '/' + - path.replace(LEADING_SLASH_RE, '') + // check path to avoid adding a trailing slash when joining an empty string + (path && '/' + path.replace(LEADING_SLASH_RE, '')) } return result }