fix(nuxt): support custom route name meta with typedPages (#21659)

This commit is contained in:
ChronicStone 2023-07-04 07:24:50 +02:00 committed by GitHub
parent 435ac87961
commit fd2b36a64d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 37 deletions

View File

@ -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 (

View File

@ -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)
}
})
}

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
definePageMeta({
validate: () => true,
middleware: [
function () {}
],
name: 'some-custom-name'
})
</script>
<template>
<div>
<!-- -->
</div>
</template>

View File

@ -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