mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +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 { extname, normalize, relative, resolve } from 'pathe'
|
||||||
import { encodePath } from 'ufo'
|
import { encodePath } from 'ufo'
|
||||||
import { resolveFiles, useNuxt } from '@nuxt/kit'
|
import { resolveFiles, useNuxt } from '@nuxt/kit'
|
||||||
@ -5,6 +6,9 @@ import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } fro
|
|||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import { filename } from 'pathe/utils'
|
import { filename } from 'pathe/utils'
|
||||||
import { hash } from 'ohash'
|
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 type { NuxtPage } from 'nuxt/schema'
|
||||||
|
|
||||||
import { uniqueBy } from '../core/utils'
|
import { uniqueBy } from '../core/utils'
|
||||||
@ -41,14 +45,14 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
|||||||
const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(',')}}`)
|
const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(',')}}`)
|
||||||
// Sort to make sure parent are listed first
|
// Sort to make sure parent are listed first
|
||||||
files.sort()
|
files.sort()
|
||||||
return generateRoutesFromFiles(files, dir)
|
return generateRoutesFromFiles(files, dir, nuxt.options.experimental.typedPages, nuxt.vfs)
|
||||||
})
|
})
|
||||||
)).flat()
|
)).flat()
|
||||||
|
|
||||||
return uniqueBy(allRoutes, 'path')
|
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[] = []
|
const routes: NuxtPage[] = []
|
||||||
|
|
||||||
for (const file of files) {
|
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)
|
parent.push(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
return prepareRoutes(routes)
|
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 {
|
function getRoutePath (tokens: SegmentToken[]): string {
|
||||||
return tokens.reduce((path, token) => {
|
return tokens.reduce((path, token) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import type { NuxtPage } from 'nuxt/schema'
|
||||||
import { generateRoutesFromFiles } from '../src/pages/utils'
|
import { generateRoutesFromFiles } from '../src/pages/utils'
|
||||||
import { generateRouteKey } from '../src/pages/runtime/utils'
|
import { generateRouteKey } from '../src/pages/runtime/utils'
|
||||||
|
|
||||||
describe('pages:generateRoutesFromFiles', () => {
|
describe('pages:generateRoutesFromFiles', () => {
|
||||||
const pagesDir = 'pages'
|
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',
|
description: 'should generate correct routes for index pages',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/index.vue`,
|
{ path: `${pagesDir}/index.vue` },
|
||||||
`${pagesDir}/parent/index.vue`,
|
{ path: `${pagesDir}/parent/index.vue` },
|
||||||
`${pagesDir}/parent/child/index.vue`
|
{ path: `${pagesDir}/parent/child/index.vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -36,8 +42,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
{
|
{
|
||||||
description: 'should generate correct routes for parent/child',
|
description: 'should generate correct routes for parent/child',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/parent.vue`,
|
{ path: `${pagesDir}/parent.vue` },
|
||||||
`${pagesDir}/parent/child.vue`
|
{ path: `${pagesDir}/parent/child.vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -58,8 +64,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
{
|
{
|
||||||
description: 'should not generate colliding route names when hyphens are in file name',
|
description: 'should not generate colliding route names when hyphens are in file name',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/parent/[child].vue`,
|
{ path: `${pagesDir}/parent/[child].vue` },
|
||||||
`${pagesDir}/parent-[child].vue`
|
{ path: `${pagesDir}/parent-[child].vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -79,8 +85,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
{
|
{
|
||||||
description: 'should generate correct id for catchall (order 1)',
|
description: 'should generate correct id for catchall (order 1)',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/[...stories].vue`,
|
{ path: `${pagesDir}/[...stories].vue` },
|
||||||
`${pagesDir}/stories/[id].vue`
|
{ path: `${pagesDir}/stories/[id].vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -100,8 +106,8 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
{
|
{
|
||||||
description: 'should generate correct id for catchall (order 2)',
|
description: 'should generate correct id for catchall (order 2)',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/stories/[id].vue`,
|
{ path: `${pagesDir}/stories/[id].vue` },
|
||||||
`${pagesDir}/[...stories].vue`
|
{ path: `${pagesDir}/[...stories].vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -121,7 +127,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
{
|
{
|
||||||
description: 'should generate correct route for snake_case file',
|
description: 'should generate correct route for snake_case file',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/snake_case.vue`
|
{ path: `${pagesDir}/snake_case.vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -134,7 +140,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'should generate correct route for kebab-case file',
|
description: 'should generate correct route for kebab-case file',
|
||||||
files: [`${pagesDir}/kebab-case.vue`],
|
files: [{ path: `${pagesDir}/kebab-case.vue` }],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: 'kebab-case',
|
name: 'kebab-case',
|
||||||
@ -147,14 +153,14 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
{
|
{
|
||||||
description: 'should generate correct dynamic routes',
|
description: 'should generate correct dynamic routes',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/index.vue`,
|
{ path: `${pagesDir}/index.vue` },
|
||||||
`${pagesDir}/[slug].vue`,
|
{ path: `${pagesDir}/[slug].vue` },
|
||||||
`${pagesDir}/[[foo]]`,
|
{ path: `${pagesDir}/[[foo]]` },
|
||||||
`${pagesDir}/[[foo]]/index.vue`,
|
{ path: `${pagesDir}/[[foo]]/index.vue` },
|
||||||
`${pagesDir}/[bar]/index.vue`,
|
{ path: `${pagesDir}/[bar]/index.vue` },
|
||||||
`${pagesDir}/nonopt/[slug].vue`,
|
{ path: `${pagesDir}/nonopt/[slug].vue` },
|
||||||
`${pagesDir}/opt/[[slug]].vue`,
|
{ path: `${pagesDir}/opt/[[slug]].vue` },
|
||||||
`${pagesDir}/[[sub]]/route-[slug].vue`
|
{ path: `${pagesDir}/[[sub]]/route-[slug].vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -210,7 +216,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'should generate correct catch-all route',
|
description: 'should generate correct catch-all route',
|
||||||
files: [`${pagesDir}/[...slug].vue`, `${pagesDir}/index.vue`],
|
files: [{ path: `${pagesDir}/[...slug].vue` }, { path: `${pagesDir}/index.vue` }],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
name: 'slug',
|
name: 'slug',
|
||||||
@ -228,24 +234,24 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'should throw unfinished param error for dynamic route',
|
description: 'should throw unfinished param error for dynamic route',
|
||||||
files: [`${pagesDir}/[slug.vue`],
|
files: [{ path: `${pagesDir}/[slug.vue` }],
|
||||||
error: 'Unfinished param "slug"'
|
error: 'Unfinished param "slug"'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'should throw empty param error for dynamic route',
|
description: 'should throw empty param error for dynamic route',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/[].vue`
|
{ path: `${pagesDir}/[].vue` }
|
||||||
],
|
],
|
||||||
error: 'Empty param'
|
error: 'Empty param'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'should only allow "_" & "." as special character for dynamic route',
|
description: 'should only allow "_" & "." as special character for dynamic route',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/[a1_1a].vue`,
|
{ path: `${pagesDir}/[a1_1a].vue` },
|
||||||
`${pagesDir}/[b2.2b].vue`,
|
{ path: `${pagesDir}/[b2.2b].vue` },
|
||||||
`${pagesDir}/[b2]_[2b].vue`,
|
{ path: `${pagesDir}/[b2]_[2b].vue` },
|
||||||
`${pagesDir}/[[c3@3c]].vue`,
|
{ path: `${pagesDir}/[[c3@3c]].vue` },
|
||||||
`${pagesDir}/[[d4-4d]].vue`
|
{ path: `${pagesDir}/[[d4-4d]].vue` }
|
||||||
],
|
],
|
||||||
output: [
|
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',
|
description: 'should allow pages with `:` in their path',
|
||||||
files: [
|
files: [
|
||||||
`${pagesDir}/test:name.vue`
|
{ path: `${pagesDir}/test:name.vue` }
|
||||||
],
|
],
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
@ -298,10 +327,15 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
|
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
it(test.description, async () => {
|
it(test.description, async () => {
|
||||||
if (test.error) {
|
const vfs = Object.fromEntries(
|
||||||
expect(() => generateRoutesFromFiles(test.files, pagesDir)).to.throws(test.error)
|
test.files.map(file => [file.path, 'template' in file ? file.template : ''])
|
||||||
} else {
|
) as Record<string, string>
|
||||||
expect(await generateRoutesFromFiles(test.files, pagesDir)).to.deep.equal(test.output)
|
|
||||||
|
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 } })
|
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', () => {
|
it('allows typing useRoute', () => {
|
||||||
const route = useRoute('param-id')
|
const route = useRoute('param-id')
|
||||||
// @ts-expect-error this param does not exist
|
// @ts-expect-error this param does not exist
|
||||||
|
Loading…
Reference in New Issue
Block a user