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 diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index cc7ac3abbce..eb91468cb89 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, } @@ -67,7 +69,6 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux const tokens = parseSegment(segment) 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 @@ -83,9 +84,6 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux route.path += '/' } else if (segmentName !== 'index') { route.path += getRoutePath(tokens) - if (isLastSegment && tokens.length === 1 && tokens[0].type === SegmentTokenType.dynamic) { - route.path += '?' - } } } @@ -99,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)) ) }, '/') } @@ -131,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 }) @@ -163,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.dynamic) { + 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 def36f0247c..ec2d5aeb82f 100644 --- a/packages/nuxt/test/pages.test.ts +++ b/packages/nuxt/test/pages.test.ts @@ -97,26 +97,48 @@ 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` + `${pagesDir}/[[sub]]/route-[slug].vue` ], output: [ { name: 'slug', - path: '/:slug?', + path: '/:slug', 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?', + 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: [] } ] @@ -150,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: [] } ]