mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
fix(nuxt): support custom route name meta with typedPages
(#21659)
This commit is contained in:
parent
435ac87961
commit
fd2b36a64d
@ -1,3 +1,4 @@
|
||||
import fs from 'node:fs'
|
||||
import { extname, normalize, relative, resolve } from 'pathe'
|
||||
import { encodePath } from 'ufo'
|
||||
import { resolveFiles, useNuxt } from '@nuxt/kit'
|
||||
@ -5,6 +6,9 @@ import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } fro
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import { filename } from 'pathe/utils'
|
||||
import { hash } from 'ohash'
|
||||
import { transform } from 'esbuild'
|
||||
import { parse } from 'acorn'
|
||||
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
|
||||
import type { NuxtPage } from 'nuxt/schema'
|
||||
|
||||
import { uniqueBy } from '../core/utils'
|
||||
@ -41,14 +45,14 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
||||
const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(',')}}`)
|
||||
// Sort to make sure parent are listed first
|
||||
files.sort()
|
||||
return generateRoutesFromFiles(files, dir)
|
||||
return generateRoutesFromFiles(files, dir, nuxt.options.experimental.typedPages, nuxt.vfs)
|
||||
})
|
||||
)).flat()
|
||||
|
||||
return uniqueBy(allRoutes, 'path')
|
||||
}
|
||||
|
||||
export function generateRoutesFromFiles (files: string[], pagesDir: string): NuxtPage[] {
|
||||
export async function generateRoutesFromFiles (files: string[], pagesDir: string, shouldExtractBuildMeta = false, vfs?: Record<string, string>): Promise<NuxtPage[]> {
|
||||
const routes: NuxtPage[] = []
|
||||
|
||||
for (const file of files) {
|
||||
@ -88,12 +92,54 @@ export function generateRoutesFromFiles (files: string[], pagesDir: string): Nux
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldExtractBuildMeta && vfs) {
|
||||
const fileContent = file in vfs ? vfs[file] : fs.readFileSync(resolve(pagesDir, file), 'utf-8')
|
||||
const overrideRouteName = await getRouteName(fileContent)
|
||||
if (overrideRouteName) {
|
||||
route.name = overrideRouteName
|
||||
}
|
||||
}
|
||||
|
||||
parent.push(route)
|
||||
}
|
||||
|
||||
return prepareRoutes(routes)
|
||||
}
|
||||
|
||||
const SFC_SCRIPT_RE = /<script\s*[^>]*>([\s\S]*?)<\/script\s*[^>]*>/i
|
||||
function extractScriptContent (html: string) {
|
||||
const match = html.match(SFC_SCRIPT_RE)
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1].trim()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/
|
||||
|
||||
async function getRouteName (file: string) {
|
||||
const script = extractScriptContent(file)
|
||||
if (!script) { return null }
|
||||
|
||||
if (!PAGE_META_RE.test(script)) { return null }
|
||||
|
||||
const js = await transform(script, { loader: 'ts' })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest'
|
||||
}) as unknown as Program
|
||||
const pageMetaAST = ast.body.find(node => node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.type === 'Identifier' && node.expression.callee.name === 'definePageMeta')
|
||||
if (!pageMetaAST) { return null }
|
||||
|
||||
const pageMetaArgument = ((pageMetaAST as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
const nameProperty = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === 'name') as Property
|
||||
if (!nameProperty || nameProperty.value.type !== 'Literal' || typeof nameProperty.value.value !== 'string') { return null }
|
||||
|
||||
return nameProperty.value.value
|
||||
}
|
||||
|
||||
function getRoutePath (tokens: SegmentToken[]): string {
|
||||
return tokens.reduce((path, token) => {
|
||||
return (
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { NuxtPage } from 'nuxt/schema'
|
||||
import { generateRoutesFromFiles } from '../src/pages/utils'
|
||||
import { generateRouteKey } from '../src/pages/runtime/utils'
|
||||
|
||||
describe('pages:generateRoutesFromFiles', () => {
|
||||
const pagesDir = 'pages'
|
||||
const tests = [
|
||||
const tests: Array<{
|
||||
description: string
|
||||
files: Array<{ path: string; template?: string; }>
|
||||
output?: NuxtPage[]
|
||||
error?: string
|
||||
}> = [
|
||||
{
|
||||
description: 'should generate correct routes for index pages',
|
||||
files: [
|
||||
`${pagesDir}/index.vue`,
|
||||
`${pagesDir}/parent/index.vue`,
|
||||
`${pagesDir}/parent/child/index.vue`
|
||||
{ path: `${pagesDir}/index.vue` },
|
||||
{ path: `${pagesDir}/parent/index.vue` },
|
||||
{ path: `${pagesDir}/parent/child/index.vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -36,8 +42,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
{
|
||||
description: 'should generate correct routes for parent/child',
|
||||
files: [
|
||||
`${pagesDir}/parent.vue`,
|
||||
`${pagesDir}/parent/child.vue`
|
||||
{ path: `${pagesDir}/parent.vue` },
|
||||
{ path: `${pagesDir}/parent/child.vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -58,8 +64,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
{
|
||||
description: 'should not generate colliding route names when hyphens are in file name',
|
||||
files: [
|
||||
`${pagesDir}/parent/[child].vue`,
|
||||
`${pagesDir}/parent-[child].vue`
|
||||
{ path: `${pagesDir}/parent/[child].vue` },
|
||||
{ path: `${pagesDir}/parent-[child].vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -79,8 +85,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
{
|
||||
description: 'should generate correct id for catchall (order 1)',
|
||||
files: [
|
||||
`${pagesDir}/[...stories].vue`,
|
||||
`${pagesDir}/stories/[id].vue`
|
||||
{ path: `${pagesDir}/[...stories].vue` },
|
||||
{ path: `${pagesDir}/stories/[id].vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -100,8 +106,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
{
|
||||
description: 'should generate correct id for catchall (order 2)',
|
||||
files: [
|
||||
`${pagesDir}/stories/[id].vue`,
|
||||
`${pagesDir}/[...stories].vue`
|
||||
{ path: `${pagesDir}/stories/[id].vue` },
|
||||
{ path: `${pagesDir}/[...stories].vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -121,7 +127,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
{
|
||||
description: 'should generate correct route for snake_case file',
|
||||
files: [
|
||||
`${pagesDir}/snake_case.vue`
|
||||
{ path: `${pagesDir}/snake_case.vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -134,7 +140,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
},
|
||||
{
|
||||
description: 'should generate correct route for kebab-case file',
|
||||
files: [`${pagesDir}/kebab-case.vue`],
|
||||
files: [{ path: `${pagesDir}/kebab-case.vue` }],
|
||||
output: [
|
||||
{
|
||||
name: 'kebab-case',
|
||||
@ -147,14 +153,14 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
{
|
||||
description: 'should generate correct dynamic routes',
|
||||
files: [
|
||||
`${pagesDir}/index.vue`,
|
||||
`${pagesDir}/[slug].vue`,
|
||||
`${pagesDir}/[[foo]]`,
|
||||
`${pagesDir}/[[foo]]/index.vue`,
|
||||
`${pagesDir}/[bar]/index.vue`,
|
||||
`${pagesDir}/nonopt/[slug].vue`,
|
||||
`${pagesDir}/opt/[[slug]].vue`,
|
||||
`${pagesDir}/[[sub]]/route-[slug].vue`
|
||||
{ path: `${pagesDir}/index.vue` },
|
||||
{ path: `${pagesDir}/[slug].vue` },
|
||||
{ path: `${pagesDir}/[[foo]]` },
|
||||
{ path: `${pagesDir}/[[foo]]/index.vue` },
|
||||
{ path: `${pagesDir}/[bar]/index.vue` },
|
||||
{ path: `${pagesDir}/nonopt/[slug].vue` },
|
||||
{ path: `${pagesDir}/opt/[[slug]].vue` },
|
||||
{ path: `${pagesDir}/[[sub]]/route-[slug].vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -210,7 +216,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
},
|
||||
{
|
||||
description: 'should generate correct catch-all route',
|
||||
files: [`${pagesDir}/[...slug].vue`, `${pagesDir}/index.vue`],
|
||||
files: [{ path: `${pagesDir}/[...slug].vue` }, { path: `${pagesDir}/index.vue` }],
|
||||
output: [
|
||||
{
|
||||
name: 'slug',
|
||||
@ -228,24 +234,24 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
},
|
||||
{
|
||||
description: 'should throw unfinished param error for dynamic route',
|
||||
files: [`${pagesDir}/[slug.vue`],
|
||||
files: [{ path: `${pagesDir}/[slug.vue` }],
|
||||
error: 'Unfinished param "slug"'
|
||||
},
|
||||
{
|
||||
description: 'should throw empty param error for dynamic route',
|
||||
files: [
|
||||
`${pagesDir}/[].vue`
|
||||
{ path: `${pagesDir}/[].vue` }
|
||||
],
|
||||
error: 'Empty param'
|
||||
},
|
||||
{
|
||||
description: 'should only allow "_" & "." as special character for dynamic route',
|
||||
files: [
|
||||
`${pagesDir}/[a1_1a].vue`,
|
||||
`${pagesDir}/[b2.2b].vue`,
|
||||
`${pagesDir}/[b2]_[2b].vue`,
|
||||
`${pagesDir}/[[c3@3c]].vue`,
|
||||
`${pagesDir}/[[d4-4d]].vue`
|
||||
{ path: `${pagesDir}/[a1_1a].vue` },
|
||||
{ path: `${pagesDir}/[b2.2b].vue` },
|
||||
{ path: `${pagesDir}/[b2]_[2b].vue` },
|
||||
{ path: `${pagesDir}/[[c3@3c]].vue` },
|
||||
{ path: `${pagesDir}/[[d4-4d]].vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -280,10 +286,33 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
description: 'should properly override route name if definePageMeta name override is defined.',
|
||||
files: [
|
||||
{
|
||||
path: `${pagesDir}/index.vue`,
|
||||
template: `
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
name: 'home'
|
||||
})
|
||||
</script>
|
||||
`
|
||||
}
|
||||
],
|
||||
output: [
|
||||
{
|
||||
name: 'home',
|
||||
path: '/',
|
||||
file: `${pagesDir}/index.vue`,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
description: 'should allow pages with `:` in their path',
|
||||
files: [
|
||||
`${pagesDir}/test:name.vue`
|
||||
{ path: `${pagesDir}/test:name.vue` }
|
||||
],
|
||||
output: [
|
||||
{
|
||||
@ -298,10 +327,15 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
|
||||
for (const test of tests) {
|
||||
it(test.description, async () => {
|
||||
if (test.error) {
|
||||
expect(() => generateRoutesFromFiles(test.files, pagesDir)).to.throws(test.error)
|
||||
} else {
|
||||
expect(await generateRoutesFromFiles(test.files, pagesDir)).to.deep.equal(test.output)
|
||||
const vfs = Object.fromEntries(
|
||||
test.files.map(file => [file.path, 'template' in file ? file.template : ''])
|
||||
) as Record<string, string>
|
||||
|
||||
try {
|
||||
const result = await generateRoutesFromFiles(test.files.map(file => file.path), pagesDir, true, vfs)
|
||||
expect(result).to.deep.equal(test.output)
|
||||
} catch (error: any) {
|
||||
expect(error.message).toEqual(test.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
15
test/fixtures/basic-types/pages/custom-name.vue
vendored
Normal file
15
test/fixtures/basic-types/pages/custom-name.vue
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
validate: () => true,
|
||||
middleware: [
|
||||
function () {}
|
||||
],
|
||||
name: 'some-custom-name'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- -->
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic-types/types.ts
vendored
5
test/fixtures/basic-types/types.ts
vendored
@ -130,6 +130,11 @@ describe('typed router integration', () => {
|
||||
router.push({ name: 'param-id', params: { id: 4 } })
|
||||
})
|
||||
|
||||
it('correctly reads custom names typed in `definePageMeta`', () => {
|
||||
const router = useRouter()
|
||||
router.push({ name: 'some-custom-name' })
|
||||
})
|
||||
|
||||
it('allows typing useRoute', () => {
|
||||
const route = useRoute('param-id')
|
||||
// @ts-expect-error this param does not exist
|
||||
|
Loading…
Reference in New Issue
Block a user