From 136ca631fef002750c9d8cc84621aeab632aa52a Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 22 Apr 2022 15:35:01 +0100 Subject: [PATCH 1/4] fix(pages): `[slug]/index.vue` param should be optional --- packages/nuxt/src/pages/utils.ts | 17 ++++++++++++----- packages/nuxt/test/pages.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index cc7ac3abbce..2426687ffa1 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -60,14 +60,15 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux // Array where routes should be added, useful when adding child routes let parent = routes + const parsedSegments = [] for (let i = 0; i < segments.length; i++) { const segment = segments[i] const tokens = parseSegment(segment) + parsedSegments.push(tokens) const segmentName = tokens.map(({ value }) => value).join('') const isSingleSegment = segments.length === 1 - const isLastSegment = i === segments.length - 1 // ex: parent/[slug].vue -> parent-slug route.name += (route.name && '-') + segmentName @@ -81,10 +82,16 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux route.path += '/:catchAll(.*)*' } else if (segmentName === 'index' && !route.path) { route.path += '/' - } else if (segmentName !== 'index') { - route.path += getRoutePath(tokens) - if (isLastSegment && tokens.length === 1 && tokens[0].type === SegmentTokenType.dynamic) { - route.path += '?' + } else { + if (segmentName !== 'index') { + route.path += getRoutePath(tokens) + } + // if this is the final segment & it's dynamic, we can indicate it's optional + if (i === segments.length - 1) { + const finalTokens = segmentName === 'index' ? parsedSegments[i - 1] : tokens + if (finalTokens.length === 1 && finalTokens[0].type === SegmentTokenType.dynamic) { + route.path += '?' + } } } } diff --git a/packages/nuxt/test/pages.test.ts b/packages/nuxt/test/pages.test.ts index def36f0247c..cb196c90b33 100644 --- a/packages/nuxt/test/pages.test.ts +++ b/packages/nuxt/test/pages.test.ts @@ -97,6 +97,9 @@ describe('pages:generateRoutesFromFiles', () => { description: 'should generate correct dynamic routes', files: [ `${pagesDir}/[slug].vue`, + `${pagesDir}/[foo]`, + `${pagesDir}/[foo]/index.vue`, + `${pagesDir}/[bar]/index.vue`, `${pagesDir}/sub/[slug].vue`, `${pagesDir}/[sub]/route-[slug].vue` ], @@ -107,6 +110,25 @@ describe('pages:generateRoutesFromFiles', () => { file: `${pagesDir}/[slug].vue`, children: [] }, + { + children: [ + { + + name: 'foo', + path: '', + file: `${pagesDir}/[foo]/index.vue`, + children: [] + } + ], + file: 'pages/[foo]', + path: '/:foo?' + }, + { + children: [], + name: 'bar', + file: 'pages/[bar]/index.vue', + path: '/:bar?' + }, { name: 'sub-slug', path: '/sub/:slug?', From 649f8ed8fe367e2647a69e1aca75caa734cd69d1 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 25 Apr 2022 10:24:16 +0100 Subject: [PATCH 2/4] feat!: explicitly enable optional params with `[[` --- packages/nuxt/src/pages/utils.ts | 39 ++++++++++++++++---------------- packages/nuxt/test/pages.test.ts | 32 +++++++++++++------------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index 2426687ffa1..276c5967a49 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -10,12 +10,14 @@ enum SegmentParserState { initial, static, dynamic, + optional, catchall, } enum SegmentTokenType { static, dynamic, + optional, catchall, } @@ -60,13 +62,11 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux // Array where routes should be added, useful when adding child routes let parent = routes - const parsedSegments = [] for (let i = 0; i < segments.length; i++) { const segment = segments[i] const tokens = parseSegment(segment) - parsedSegments.push(tokens) const segmentName = tokens.map(({ value }) => value).join('') const isSingleSegment = segments.length === 1 @@ -82,17 +82,8 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux route.path += '/:catchAll(.*)*' } else if (segmentName === 'index' && !route.path) { route.path += '/' - } else { - if (segmentName !== 'index') { - route.path += getRoutePath(tokens) - } - // if this is the final segment & it's dynamic, we can indicate it's optional - if (i === segments.length - 1) { - const finalTokens = segmentName === 'index' ? parsedSegments[i - 1] : tokens - if (finalTokens.length === 1 && finalTokens[0].type === SegmentTokenType.dynamic) { - route.path += '?' - } - } + } else if (segmentName !== 'index') { + route.path += getRoutePath(tokens) } } @@ -106,11 +97,13 @@ function getRoutePath (tokens: SegmentToken[]): string { return tokens.reduce((path, token) => { return ( path + - (token.type === SegmentTokenType.dynamic - ? `:${token.value}` - : token.type === SegmentTokenType.catchall - ? `:${token.value}(.*)*` - : encodePath(token.value)) + (token.type === SegmentTokenType.optional + ? `:${token.value}?` + : token.type === SegmentTokenType.dynamic + ? `:${token.value}` + : token.type === SegmentTokenType.catchall + ? `:${token.value}(.*)*` + : encodePath(token.value)) ) }, '/') } @@ -138,7 +131,9 @@ function parseSegment (segment: string) { ? SegmentTokenType.static : state === SegmentParserState.dynamic ? SegmentTokenType.dynamic - : SegmentTokenType.catchall, + : state === SegmentParserState.optional + ? SegmentTokenType.optional + : SegmentTokenType.catchall, value: buffer }) @@ -170,11 +165,15 @@ function parseSegment (segment: string) { case SegmentParserState.catchall: case SegmentParserState.dynamic: + case SegmentParserState.optional: if (buffer === '...') { buffer = '' state = SegmentParserState.catchall } - if (c === ']') { + if (c === '[' && state !== SegmentParserState.optional) { + state = SegmentParserState.optional + } + if (c === ']' && (state !== SegmentParserState.optional || buffer[buffer.length - 1] === ']')) { if (!buffer) { throw new Error('Empty param') } else { diff --git a/packages/nuxt/test/pages.test.ts b/packages/nuxt/test/pages.test.ts index cb196c90b33..ec2d5aeb82f 100644 --- a/packages/nuxt/test/pages.test.ts +++ b/packages/nuxt/test/pages.test.ts @@ -97,16 +97,16 @@ describe('pages:generateRoutesFromFiles', () => { description: 'should generate correct dynamic routes', files: [ `${pagesDir}/[slug].vue`, - `${pagesDir}/[foo]`, - `${pagesDir}/[foo]/index.vue`, + `${pagesDir}/[[foo]]`, + `${pagesDir}/[[foo]]/index.vue`, `${pagesDir}/[bar]/index.vue`, `${pagesDir}/sub/[slug].vue`, - `${pagesDir}/[sub]/route-[slug].vue` + `${pagesDir}/[[sub]]/route-[slug].vue` ], output: [ { name: 'slug', - path: '/:slug?', + path: '/:slug', file: `${pagesDir}/[slug].vue`, children: [] }, @@ -116,29 +116,29 @@ describe('pages:generateRoutesFromFiles', () => { name: 'foo', path: '', - file: `${pagesDir}/[foo]/index.vue`, + file: `${pagesDir}/[[foo]]/index.vue`, children: [] } ], - file: 'pages/[foo]', + file: 'pages/[[foo]]', path: '/:foo?' }, { children: [], name: 'bar', file: 'pages/[bar]/index.vue', - path: '/:bar?' + path: '/:bar' }, { name: 'sub-slug', - path: '/sub/:slug?', + path: '/sub/:slug', file: `${pagesDir}/sub/[slug].vue`, children: [] }, { name: 'sub-route-slug', - path: '/:sub/route-:slug', - file: `${pagesDir}/[sub]/route-[slug].vue`, + path: '/:sub?/route-:slug', + file: `${pagesDir}/[[sub]]/route-[slug].vue`, children: [] } ] @@ -172,32 +172,32 @@ describe('pages:generateRoutesFromFiles', () => { files: [ `${pagesDir}/[a1_1a].vue`, `${pagesDir}/[b2.2b].vue`, - `${pagesDir}/[c3@3c].vue`, - `${pagesDir}/[d4-4d].vue` + `${pagesDir}/[[c3@3c]].vue`, + `${pagesDir}/[[d4-4d]].vue` ], output: [ { name: 'a1_1a', - path: '/:a1_1a?', + path: '/:a1_1a', file: `${pagesDir}/[a1_1a].vue`, children: [] }, { name: 'b2.2b', - path: '/:b2.2b?', + path: '/:b2.2b', file: `${pagesDir}/[b2.2b].vue`, children: [] }, { name: 'c33c', path: '/:c33c?', - file: `${pagesDir}/[c3@3c].vue`, + file: `${pagesDir}/[[c3@3c]].vue`, children: [] }, { name: 'd44d', path: '/:d44d?', - file: `${pagesDir}/[d4-4d].vue`, + file: `${pagesDir}/[[d4-4d]].vue`, children: [] } ] From dce39054a90a600b52b06e986ba13cb3ab22beee Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 25 Apr 2022 10:32:18 +0100 Subject: [PATCH 3/4] fix: narrow case --- packages/nuxt/src/pages/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index 276c5967a49..eb91468cb89 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -170,7 +170,7 @@ function parseSegment (segment: string) { buffer = '' state = SegmentParserState.catchall } - if (c === '[' && state !== SegmentParserState.optional) { + if (c === '[' && state === SegmentParserState.dynamic) { state = SegmentParserState.optional } if (c === ']' && (state !== SegmentParserState.optional || buffer[buffer.length - 1] === ']')) { From 0e9a0bc2f79d323f86b788fc8358e9495c1957f9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 26 Apr 2022 16:21:07 +0100 Subject: [PATCH 4/4] docs: update documentation --- docs/content/2.guide/3.directory-structure/10.pages.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/2.guide/3.directory-structure/10.pages.md b/docs/content/2.guide/3.directory-structure/10.pages.md index d1eeea8a0fe..0bfa39e4b18 100644 --- a/docs/content/2.guide/3.directory-structure/10.pages.md +++ b/docs/content/2.guide/3.directory-structure/10.pages.md @@ -95,6 +95,8 @@ Here are some examples to illustrate what a page with a single root element look If you place anything within square brackets, it will be turned into a [dynamic route](https://router.vuejs.org/guide/essentials/dynamic-matching.html) parameter. You can mix and match multiple parameters and even non-dynamic text within a file name or directory. +If you want a parameter to be _optional_, you must enclose it in double square brackets - for example, `~/pages/[[slug]]/index.vue` or `~/pages/[[slug]].vue` will match both `/` and `/test`. + ### Example ```bash