feat(nuxt): allow organising pages within route groups (#28276)

This commit is contained in:
Horu 2024-08-13 07:16:04 +10:00 committed by Daniel Roe
parent 5bf971f4ef
commit c4ae151de5
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
10 changed files with 150 additions and 4 deletions

View File

@ -226,6 +226,22 @@ definePageMeta({
:link-example{to="/docs/examples/routing/pages"}
## Route Groups
In some cases, you may want to group a set of routes together in a way which doesn't affect file-based routing. For this purpose, you can put files in a folder which is wrapped in parentheses - `(` and `)`.
For example:
```bash [Directory structure]
-| pages/
---| index.vue
---| (marketing)/
-----| about.vue
-----| contact.vue
```
This will produce `/`, `/about` and `/contact` pages in your app. The `marketing` group is ignored for purposes of your URL structure.
## Page Metadata
You might want to define metadata for each route in your app. You can do this using the `definePageMeta` macro, which will work both in `<script>` and in `<script setup>`:

View File

@ -23,6 +23,7 @@ enum SegmentParserState {
dynamic,
optional,
catchall,
group,
}
enum SegmentTokenType {
@ -30,6 +31,7 @@ enum SegmentTokenType {
dynamic,
optional,
catchall,
group,
}
interface SegmentToken {
@ -115,7 +117,13 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
const segment = segments[i]
const tokens = parseSegment(segment)
const segmentName = tokens.map(({ value }) => value).join('')
// Skip group segments
if (tokens.every(token => token.type === SegmentTokenType.group)) {
continue
}
const segmentName = tokens.map(({ value, type }) => type === SegmentTokenType.group ? '' : value).join('')
// ex: parent/[slug].vue -> parent-slug
route.name += (route.name && '/') + segmentName
@ -298,7 +306,9 @@ function getRoutePath (tokens: SegmentToken[]): string {
? `:${token.value}()`
: token.type === SegmentTokenType.catchall
? `:${token.value}(.*)*`
: encodePath(token.value).replace(/:/g, '\\:'))
: token.type === SegmentTokenType.group
? ''
: encodePath(token.value).replace(/:/g, '\\:'))
)
}, '/')
}
@ -328,7 +338,9 @@ function parseSegment (segment: string) {
? SegmentTokenType.dynamic
: state === SegmentParserState.optional
? SegmentTokenType.optional
: SegmentTokenType.catchall,
: state === SegmentParserState.catchall
? SegmentTokenType.catchall
: SegmentTokenType.group,
value: buffer,
})
@ -343,6 +355,8 @@ function parseSegment (segment: string) {
buffer = ''
if (c === '[') {
state = SegmentParserState.dynamic
} else if (c === '(') {
state = SegmentParserState.group
} else {
i--
state = SegmentParserState.static
@ -353,6 +367,9 @@ function parseSegment (segment: string) {
if (c === '[') {
consumeBuffer()
state = SegmentParserState.dynamic
} else if (c === '(') {
consumeBuffer()
state = SegmentParserState.group
} else {
buffer += c
}
@ -361,6 +378,7 @@ function parseSegment (segment: string) {
case SegmentParserState.catchall:
case SegmentParserState.dynamic:
case SegmentParserState.optional:
case SegmentParserState.group:
if (buffer === '...') {
buffer = ''
state = SegmentParserState.catchall
@ -375,10 +393,16 @@ function parseSegment (segment: string) {
consumeBuffer()
}
state = SegmentParserState.initial
} else if (c === ')' && state === SegmentParserState.group) {
if (!buffer) {
throw new Error('Empty group')
} else {
consumeBuffer()
}
state = SegmentParserState.initial
} else if (PARAM_CHAR_RE.test(c)) {
buffer += c
} else {
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
}
break

View File

@ -339,6 +339,34 @@
"redirect": "mockMeta?.redirect",
},
],
"should handle route groups": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"redirect": "mockMeta?.redirect",
},
{
"alias": "mockMeta?.alias || []",
"children": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "about"",
"path": "mockMeta?.path ?? """,
"redirect": "mockMeta?.redirect",
},
],
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/about"",
"redirect": "mockMeta?.redirect",
},
],
"should handle trailing slashes with index routes": [
{
"alias": "mockMeta?.alias || []",

View File

@ -234,6 +234,25 @@
"path": ""/parent"",
},
],
"should handle route groups": [
{
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
"name": ""index"",
"path": ""/"",
},
{
"children": [
{
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
"name": ""about"",
"path": """",
},
],
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
"name": "mockMeta?.name",
"path": ""/about"",
},
],
"should handle trailing slashes with index routes": [
{
"children": [

View File

@ -601,6 +601,37 @@ describe('pages:generateRoutesFromFiles', () => {
},
],
},
{
description: 'should handle route groups',
files: [
{ path: `${pagesDir}/(foo)/index.vue` },
{ path: `${pagesDir}/(foo)/about.vue` },
{ path: `${pagesDir}/(bar)/about/index.vue` },
],
output: [
{
name: 'index',
path: '/',
file: `${pagesDir}/(foo)/index.vue`,
meta: undefined,
children: [],
},
{
path: '/about',
file: `${pagesDir}/(foo)/about.vue`,
meta: undefined,
children: [
{
name: 'about',
path: '',
file: `${pagesDir}/(bar)/about/index.vue`,
children: [],
},
],
},
],
},
]
const normalizedResults: Record<string, any> = {}

View File

@ -570,6 +570,14 @@ describe('pages', () => {
await normalInitialPage.close()
})
it('groups routes', async () => {
for (const targetRoute of ['/group-page', '/nested-group/group-page', '/nested-group']) {
const { status } = await fetch(targetRoute)
expect(status).toBe(200)
}
})
})
describe('nuxt composables', () => {

View File

@ -0,0 +1,5 @@
<template>
<div>
Hello from new group
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
Page deep in group
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
Index page of a group
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
Page deep, deep in group
</div>
</template>