mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 09:02:03 +00:00
feat(nuxt): experimentally extract route metadata at build time (#25210)
Co-authored-by: Bobbie Goede <bobbiegoede@gmail.com>
This commit is contained in:
parent
90ca0e8797
commit
407fde6765
@ -375,6 +375,12 @@ globalThis.Buffer = globalThis.Buffer || Buffer
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## scanPageMeta
|
||||||
|
|
||||||
|
This option allows exposing some route metadata defined in `definePageMeta` at build-time to modules (specifically `alias`, `name`, `path`, `redirect`).
|
||||||
|
|
||||||
|
This only works with static or strings/arrays rather than variables or conditional assignment. See [original issue](https://github.com/nuxt/nuxt/issues/24770) for more information and context.
|
||||||
|
|
||||||
## cookieStore
|
## cookieStore
|
||||||
|
|
||||||
Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
||||||
|
@ -216,11 +216,17 @@ export default defineNuxtModule({
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isPage (file: string, pages = nuxt.apps.default.pages): boolean {
|
||||||
|
if (!pages) { return false }
|
||||||
|
return pages.some(page => page.file === file) || pages.some(page => page.children && isPage(file, page.children))
|
||||||
|
}
|
||||||
nuxt.hook('builder:watch', async (event, relativePath) => {
|
nuxt.hook('builder:watch', async (event, relativePath) => {
|
||||||
if (event === 'change') { return }
|
|
||||||
|
|
||||||
const path = resolve(nuxt.options.srcDir, relativePath)
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
if (updateTemplatePaths.some(dir => path.startsWith(dir))) {
|
const shouldAlwaysRegenerate = nuxt.options.experimental.scanPageMeta && isPage(path)
|
||||||
|
|
||||||
|
if (event === 'change' && !shouldAlwaysRegenerate) { return }
|
||||||
|
|
||||||
|
if (shouldAlwaysRegenerate || updateTemplatePaths.some(dir => path.startsWith(dir))) {
|
||||||
await updateTemplates({
|
await updateTemplates({
|
||||||
filter: template => template.filename === 'routes.mjs'
|
filter: template => template.filename === 'routes.mjs'
|
||||||
})
|
})
|
||||||
@ -398,7 +404,7 @@ export default defineNuxtModule({
|
|||||||
filename: 'routes.mjs',
|
filename: 'routes.mjs',
|
||||||
getContents ({ app }) {
|
getContents ({ app }) {
|
||||||
if (!app.pages) return 'export default []'
|
if (!app.pages) return 'export default []'
|
||||||
const { routes, imports } = normalizeRoutes(app.pages)
|
const { routes, imports } = normalizeRoutes(app.pages, new Set(), nuxt.options.experimental.scanPageMeta)
|
||||||
return [...imports, `export default ${routes}`].join('\n')
|
return [...imports, `export default ${routes}`].join('\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { runInNewContext } from 'node:vm'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { extname, normalize, relative, resolve } from 'pathe'
|
import { extname, normalize, relative, resolve } from 'pathe'
|
||||||
import { encodePath, joinURL, withLeadingSlash } from 'ufo'
|
import { encodePath, joinURL, withLeadingSlash } from 'ufo'
|
||||||
@ -55,12 +56,20 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
|||||||
// sort scanned files using en-US locale to make the result consistent across different system locales
|
// sort scanned files using en-US locale to make the result consistent across different system locales
|
||||||
scannedFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath, 'en-US'))
|
scannedFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath, 'en-US'))
|
||||||
|
|
||||||
const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), nuxt.options.experimental.typedPages, nuxt.vfs)
|
const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
|
||||||
|
shouldExtractBuildMeta: nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages,
|
||||||
|
vfs: nuxt.vfs
|
||||||
|
})
|
||||||
|
|
||||||
return uniqueBy(allRoutes, 'path')
|
return uniqueBy(allRoutes, 'path')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateRoutesFromFiles (files: ScannedFile[], shouldExtractBuildMeta = false, vfs?: Record<string, string>): Promise<NuxtPage[]> {
|
type GenerateRoutesFromFilesOptions = {
|
||||||
|
shouldExtractBuildMeta?: boolean
|
||||||
|
vfs?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): Promise<NuxtPage[]> {
|
||||||
const routes: NuxtPage[] = []
|
const routes: NuxtPage[] = []
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@ -101,12 +110,9 @@ export async function generateRoutesFromFiles (files: ScannedFile[], shouldExtra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldExtractBuildMeta && vfs) {
|
if (options.shouldExtractBuildMeta && options.vfs) {
|
||||||
const fileContent = file.absolutePath in vfs ? vfs[file.absolutePath] : fs.readFileSync(file.absolutePath, 'utf-8')
|
const fileContent = file.absolutePath in options.vfs ? options.vfs[file.absolutePath] : fs.readFileSync(file.absolutePath, 'utf-8')
|
||||||
const overrideRouteName = await getRouteName(fileContent)
|
Object.assign(route, await getRouteMeta(fileContent, file.absolutePath))
|
||||||
if (overrideRouteName) {
|
|
||||||
route.name = overrideRouteName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.push(route)
|
parent.push(route)
|
||||||
@ -127,26 +133,96 @@ export function extractScriptContent (html: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/
|
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/
|
||||||
|
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||||
|
|
||||||
async function getRouteName (file: string) {
|
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
|
||||||
const script = extractScriptContent(file)
|
async function getRouteMeta (contents: string, absolutePath?: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
|
||||||
if (!script) { return null }
|
if (contents in metaCache) { return metaCache[contents] }
|
||||||
|
|
||||||
if (!PAGE_META_RE.test(script)) { return null }
|
const script = extractScriptContent(contents)
|
||||||
|
if (!script) {
|
||||||
|
metaCache[contents] = {}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PAGE_META_RE.test(script)) {
|
||||||
|
metaCache[contents] = {}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const js = await transform(script, { loader: 'ts' })
|
const js = await transform(script, { loader: 'ts' })
|
||||||
const ast = parse(js.code, {
|
const ast = parse(js.code, {
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaVersion: 'latest'
|
ecmaVersion: 'latest',
|
||||||
|
ranges: true
|
||||||
}) as unknown as Program
|
}) 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')
|
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 }
|
if (!pageMetaAST) {
|
||||||
|
metaCache[contents] = {}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const pageMetaArgument = ((pageMetaAST as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
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
|
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
|
||||||
if (!nameProperty || nameProperty.value.type !== 'Literal' || typeof nameProperty.value.value !== 'string') { return null }
|
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
|
||||||
|
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||||
|
|
||||||
return nameProperty.value.value
|
for (const key of extractionKeys) {
|
||||||
|
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||||
|
if (!property) { continue }
|
||||||
|
|
||||||
|
if (property.value.type === 'ObjectExpression') {
|
||||||
|
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||||
|
try {
|
||||||
|
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
|
||||||
|
} catch {
|
||||||
|
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
|
||||||
|
dynamicProperties.add(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.value.type === 'ArrayExpression') {
|
||||||
|
const values = []
|
||||||
|
for (const element of property.value.elements) {
|
||||||
|
if (!element) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (element.type !== 'Literal' || typeof element.value !== 'string') {
|
||||||
|
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
|
||||||
|
dynamicProperties.add(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values.push(element.value)
|
||||||
|
}
|
||||||
|
extractedMeta[key] = values
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
|
||||||
|
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
||||||
|
dynamicProperties.add(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
extractedMeta[key] = property.value.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraneousMetaKeys = pageMetaArgument.properties
|
||||||
|
.filter(property => property.type === 'Property' && property.key.type === 'Identifier' && !(extractionKeys as unknown as string[]).includes(property.key.name))
|
||||||
|
// @ts-expect-error inferred types have been filtered out
|
||||||
|
.map(property => property.key.name)
|
||||||
|
|
||||||
|
if (extraneousMetaKeys.length) {
|
||||||
|
dynamicProperties.add('meta')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicProperties.size) {
|
||||||
|
extractedMeta.meta ??= {}
|
||||||
|
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
metaCache[contents] = extractedMeta
|
||||||
|
return extractedMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoutePath (tokens: SegmentToken[]): string {
|
function getRoutePath (tokens: SegmentToken[]): string {
|
||||||
@ -301,26 +377,42 @@ function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<s
|
|||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
|
function serializeRouteValue (value: any, skipSerialisation = false) {
|
||||||
|
if (skipSerialisation || value === undefined) return undefined
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NormalizedRoute = Partial<Record<Exclude<keyof NuxtPage, 'file'>, string>> & { component?: string }
|
||||||
|
type NormalizedRouteKeys = (keyof NormalizedRoute)[]
|
||||||
|
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set(), overrideMeta = false): { imports: Set<string>, routes: string } {
|
||||||
return {
|
return {
|
||||||
imports: metaImports,
|
imports: metaImports,
|
||||||
routes: genArrayFromRaw(routes.map((page) => {
|
routes: genArrayFromRaw(routes.map((page) => {
|
||||||
const route: Record<Exclude<keyof NuxtPage, 'file'>, string> & { component?: string } = Object.create(null)
|
const markedDynamic = page.meta?.[DYNAMIC_META_KEY] ?? new Set()
|
||||||
for (const [key, value] of Object.entries(page)) {
|
const metaFiltered: Record<string, any> = {}
|
||||||
if (key !== 'file' && (Array.isArray(value) ? value.length : value)) {
|
let skipMeta = true
|
||||||
route[key as Exclude<keyof NuxtPage, 'file'>] = JSON.stringify(value)
|
for (const key in page.meta || {}) {
|
||||||
|
if (key !== DYNAMIC_META_KEY && page.meta![key] !== undefined) {
|
||||||
|
skipMeta = false
|
||||||
|
metaFiltered[key] = page.meta![key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const skipAlias = toArray(page.alias).every(val => !val)
|
||||||
|
|
||||||
|
const route: NormalizedRoute = {
|
||||||
|
path: serializeRouteValue(page.path),
|
||||||
|
name: serializeRouteValue(page.name),
|
||||||
|
meta: serializeRouteValue(metaFiltered, skipMeta),
|
||||||
|
alias: serializeRouteValue(toArray(page.alias), skipAlias),
|
||||||
|
redirect: serializeRouteValue(page.redirect),
|
||||||
|
}
|
||||||
|
|
||||||
if (page.children?.length) {
|
if (page.children?.length) {
|
||||||
route.children = normalizeRoutes(page.children, metaImports).routes
|
route.children = normalizeRoutes(page.children, metaImports, overrideMeta).routes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Without a file, we can't use `definePageMeta` to extract route-level meta from the file
|
// Without a file, we can't use `definePageMeta` to extract route-level meta from the file
|
||||||
if (!page.file) {
|
if (!page.file) {
|
||||||
for (const key of ['name', 'path', 'meta', 'alias', 'redirect'] as const) {
|
|
||||||
if (page[key]) { route[key] = JSON.stringify(page[key]) }
|
|
||||||
}
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,20 +420,56 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
|
|||||||
const metaImportName = genSafeVariableName(filename(file) + hash(file)) + 'Meta'
|
const metaImportName = genSafeVariableName(filename(file) + hash(file)) + 'Meta'
|
||||||
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'default', as: metaImportName }]))
|
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'default', as: metaImportName }]))
|
||||||
|
|
||||||
let aliasCode = `${metaImportName}?.alias || []`
|
const metaRoute: NormalizedRoute = {
|
||||||
const alias = toArray(page.alias).filter(Boolean)
|
name: `${metaImportName}?.name ?? ${route.name}`,
|
||||||
if (alias.length) {
|
path: `${metaImportName}?.path ?? ${route.path}`,
|
||||||
aliasCode = `${JSON.stringify(alias)}.concat(${aliasCode})`
|
meta: `${metaImportName} || {}`,
|
||||||
|
alias: `${metaImportName}?.alias || []`,
|
||||||
|
redirect: `${metaImportName}?.redirect`,
|
||||||
|
component: genDynamicImport(file, { interopDefault: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
route.name = `${metaImportName}?.name ?? ${page.name ? JSON.stringify(page.name) : 'undefined'}`
|
if (route.children != null) {
|
||||||
route.path = `${metaImportName}?.path ?? ${JSON.stringify(page.path)}`
|
metaRoute.children = route.children
|
||||||
route.meta = page.meta && Object.values(page.meta).filter(value => value !== undefined).length ? `{...(${metaImportName} || {}), ...${JSON.stringify(page.meta)}}` : `${metaImportName} || {}`
|
}
|
||||||
route.alias = aliasCode
|
|
||||||
route.redirect = page.redirect ? JSON.stringify(page.redirect) : `${metaImportName}?.redirect || undefined`
|
|
||||||
route.component = genDynamicImport(file, { interopDefault: true })
|
|
||||||
|
|
||||||
return route
|
if (overrideMeta) {
|
||||||
|
metaRoute.name = `${metaImportName}?.name`
|
||||||
|
metaRoute.path = `${metaImportName}?.path ?? ''`
|
||||||
|
|
||||||
|
// skip and retain fallback if marked dynamic
|
||||||
|
// set to extracted value or fallback if none extracted
|
||||||
|
for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) {
|
||||||
|
if (markedDynamic.has(key)) continue
|
||||||
|
metaRoute[key] = route[key] ?? metaRoute[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// set to extracted value or delete if none extracted
|
||||||
|
for (const key of ['meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) {
|
||||||
|
if (markedDynamic.has(key)) continue
|
||||||
|
|
||||||
|
if (route[key] == null) {
|
||||||
|
delete metaRoute[key]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
metaRoute[key] = route[key]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (route.meta != null) {
|
||||||
|
metaRoute.meta = `{ ...(${metaImportName}) || {}), ...${route.meta} }`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.alias != null) {
|
||||||
|
metaRoute.alias = `${route.alias}.concat(${metaImportName}?.alias || [])`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.redirect != null) {
|
||||||
|
metaRoute.redirect = route.redirect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metaRoute
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,441 @@
|
|||||||
|
{
|
||||||
|
"route without file": [
|
||||||
|
{
|
||||||
|
"alias": "["sweet-home"]",
|
||||||
|
"meta": "{"hello":"world"}",
|
||||||
|
"name": ""home"",
|
||||||
|
"path": ""/"",
|
||||||
|
"redirect": undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should allow pages with `:` in their path": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/test:name.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "test:name"",
|
||||||
|
"path": "mockMeta?.path ?? "/test\\:name"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should correctly merge nested routes": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "param-index"",
|
||||||
|
"path": "mockMeta?.path ?? """,
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("layer/pages/param/index/sibling.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "param-index-sibling"",
|
||||||
|
"path": "mockMeta?.path ?? "sibling"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? undefined",
|
||||||
|
"path": "mockMeta?.path ?? """,
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "param-sibling"",
|
||||||
|
"path": "mockMeta?.path ?? "sibling"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/param.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? undefined",
|
||||||
|
"path": "mockMeta?.path ?? "/param"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("layer/pages/wrapper-expose/other/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "wrapper-expose-other"",
|
||||||
|
"path": "mockMeta?.path ?? """,
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/wrapper-expose/other/sibling.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "wrapper-expose-other-sibling"",
|
||||||
|
"path": "mockMeta?.path ?? "sibling"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? undefined",
|
||||||
|
"path": "mockMeta?.path ?? "/wrapper-expose/other"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
|
||||||
|
{
|
||||||
|
"alias": "["sweet-home"].concat(mockMeta?.alias || [])",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "home"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": ""/"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct catch-all route": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "slug"",
|
||||||
|
"path": "mockMeta?.path ?? "/:slug(.*)*"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct dynamic routes": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[slug].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "slug"",
|
||||||
|
"path": "mockMeta?.path ?? "/:slug()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "foo"",
|
||||||
|
"path": "mockMeta?.path ?? """,
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/[[foo]]").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? undefined",
|
||||||
|
"path": "mockMeta?.path ?? "/:foo?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "optional-opt"",
|
||||||
|
"path": "mockMeta?.path ?? "/optional/:opt?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/optional/prefix-[[opt]].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "optional-prefix-opt"",
|
||||||
|
"path": "mockMeta?.path ?? "/optional/prefix-:opt?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/optional/[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "optional-opt-postfix"",
|
||||||
|
"path": "mockMeta?.path ?? "/optional/:opt?-postfix"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "optional-prefix-opt-postfix"",
|
||||||
|
"path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "bar"",
|
||||||
|
"path": "mockMeta?.path ?? "/:bar()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "nonopt-slug"",
|
||||||
|
"path": "mockMeta?.path ?? "/nonopt/:slug()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/opt/[[slug]].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "opt-slug"",
|
||||||
|
"path": "mockMeta?.path ?? "/opt/:slug?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[[sub]]/route-[slug].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "sub-route-slug"",
|
||||||
|
"path": "mockMeta?.path ?? "/:sub?/route-:slug()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct id for catchall (order 1)": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "stories"",
|
||||||
|
"path": "mockMeta?.path ?? "/:stories(.*)*"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "stories-id"",
|
||||||
|
"path": "mockMeta?.path ?? "/stories/:id()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct id for catchall (order 2)": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "stories-id"",
|
||||||
|
"path": "mockMeta?.path ?? "/stories/:id()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "stories"",
|
||||||
|
"path": "mockMeta?.path ?? "/:stories(.*)*"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct route for kebab-case file": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/kebab-case.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "kebab-case"",
|
||||||
|
"path": "mockMeta?.path ?? "/kebab-case"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct route for snake_case file": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/snake_case.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "snake_case"",
|
||||||
|
"path": "mockMeta?.path ?? "/snake_case"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct routes for index pages": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "parent"",
|
||||||
|
"path": "mockMeta?.path ?? "/parent"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/parent/child/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "parent-child"",
|
||||||
|
"path": "mockMeta?.path ?? "/parent/child"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct routes for parent/child": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "parent-child"",
|
||||||
|
"path": "mockMeta?.path ?? "child"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/parent.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "parent"",
|
||||||
|
"path": "mockMeta?.path ?? "/parent"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should handle trailing slashes with index routes": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index-index-all"",
|
||||||
|
"path": "mockMeta?.path ?? "all"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/index/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should not generate colliding route names when hyphens are in file name": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "parent-child"",
|
||||||
|
"path": "mockMeta?.path ?? "/parent/:child()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "parent-child"",
|
||||||
|
"path": "mockMeta?.path ?? "/parent-:child()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should not merge required param as a child of optional param": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[[foo]].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "foo"",
|
||||||
|
"path": "mockMeta?.path ?? "/:foo?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[foo].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "foo"",
|
||||||
|
"path": "mockMeta?.path ?? "/:foo()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should only allow "_" & "." as special character for dynamic route": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[a1_1a].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "a1_1a"",
|
||||||
|
"path": "mockMeta?.path ?? "/:a1_1a()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "b2.2b"",
|
||||||
|
"path": "mockMeta?.path ?? "/:b2.2b()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[b2]_[2b].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "b2_2b"",
|
||||||
|
"path": "mockMeta?.path ?? "/:b2()_:2b()"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "c33c"",
|
||||||
|
"path": "mockMeta?.path ?? "/:c33c?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "d44d"",
|
||||||
|
"path": "mockMeta?.path ?? "/:d44d?"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should properly override route name if definePageMeta name override is defined.": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "home"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should use fallbacks when normalized with `overrideMeta: true`": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
@ -0,0 +1,303 @@
|
|||||||
|
{
|
||||||
|
"route without file": [
|
||||||
|
{
|
||||||
|
"alias": "["sweet-home"]",
|
||||||
|
"meta": "{"hello":"world"}",
|
||||||
|
"name": ""home"",
|
||||||
|
"path": ""/"",
|
||||||
|
"redirect": undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should allow pages with `:` in their path": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/test:name.vue").then(m => m.default || m)",
|
||||||
|
"name": ""test:name"",
|
||||||
|
"path": ""/test\\:name"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should correctly merge nested routes": [
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""param-index"",
|
||||||
|
"path": """",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("layer/pages/param/index/sibling.vue").then(m => m.default || m)",
|
||||||
|
"name": ""param-index-sibling"",
|
||||||
|
"path": ""sibling"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)",
|
||||||
|
"name": "mockMeta?.name",
|
||||||
|
"path": """",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)",
|
||||||
|
"name": ""param-sibling"",
|
||||||
|
"path": ""sibling"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/param.vue").then(m => m.default || m)",
|
||||||
|
"name": "mockMeta?.name",
|
||||||
|
"path": ""/param"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"component": "() => import("layer/pages/wrapper-expose/other/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""wrapper-expose-other"",
|
||||||
|
"path": """",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/wrapper-expose/other/sibling.vue").then(m => m.default || m)",
|
||||||
|
"name": ""wrapper-expose-other-sibling"",
|
||||||
|
"path": ""sibling"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)",
|
||||||
|
"name": "mockMeta?.name",
|
||||||
|
"path": ""/wrapper-expose/other"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
|
||||||
|
{
|
||||||
|
"alias": "["sweet-home"]",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": ""home"",
|
||||||
|
"path": ""/"",
|
||||||
|
"redirect": ""/"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct catch-all route": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)",
|
||||||
|
"name": ""slug"",
|
||||||
|
"path": ""/:slug(.*)*"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""index"",
|
||||||
|
"path": ""/"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct dynamic routes": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""index"",
|
||||||
|
"path": ""/"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[slug].vue").then(m => m.default || m)",
|
||||||
|
"name": ""slug"",
|
||||||
|
"path": ""/:slug()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""foo"",
|
||||||
|
"path": """",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/[[foo]]").then(m => m.default || m)",
|
||||||
|
"name": "mockMeta?.name",
|
||||||
|
"path": ""/:foo?"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)",
|
||||||
|
"name": ""optional-opt"",
|
||||||
|
"path": ""/optional/:opt?"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/optional/prefix-[[opt]].vue").then(m => m.default || m)",
|
||||||
|
"name": ""optional-prefix-opt"",
|
||||||
|
"path": ""/optional/prefix-:opt?"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/optional/[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||||
|
"name": ""optional-opt-postfix"",
|
||||||
|
"path": ""/optional/:opt?-postfix"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||||
|
"name": ""optional-prefix-opt-postfix"",
|
||||||
|
"path": ""/optional/prefix-:opt?-postfix"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""bar"",
|
||||||
|
"path": ""/:bar()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)",
|
||||||
|
"name": ""nonopt-slug"",
|
||||||
|
"path": ""/nonopt/:slug()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/opt/[[slug]].vue").then(m => m.default || m)",
|
||||||
|
"name": ""opt-slug"",
|
||||||
|
"path": ""/opt/:slug?"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[[sub]]/route-[slug].vue").then(m => m.default || m)",
|
||||||
|
"name": ""sub-route-slug"",
|
||||||
|
"path": ""/:sub?/route-:slug()"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct id for catchall (order 1)": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||||
|
"name": ""stories"",
|
||||||
|
"path": ""/:stories(.*)*"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||||
|
"name": ""stories-id"",
|
||||||
|
"path": ""/stories/:id()"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct id for catchall (order 2)": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||||
|
"name": ""stories-id"",
|
||||||
|
"path": ""/stories/:id()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||||
|
"name": ""stories"",
|
||||||
|
"path": ""/:stories(.*)*"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct route for kebab-case file": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/kebab-case.vue").then(m => m.default || m)",
|
||||||
|
"name": ""kebab-case"",
|
||||||
|
"path": ""/kebab-case"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct route for snake_case file": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/snake_case.vue").then(m => m.default || m)",
|
||||||
|
"name": ""snake_case"",
|
||||||
|
"path": ""/snake_case"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct routes for index pages": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""index"",
|
||||||
|
"path": ""/"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""parent"",
|
||||||
|
"path": ""/parent"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/parent/child/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""parent-child"",
|
||||||
|
"path": ""/parent/child"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should generate correct routes for parent/child": [
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)",
|
||||||
|
"name": ""parent-child"",
|
||||||
|
"path": ""child"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/parent.vue").then(m => m.default || m)",
|
||||||
|
"name": ""parent"",
|
||||||
|
"path": ""/parent"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should handle trailing slashes with index routes": [
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)",
|
||||||
|
"name": ""index-index-all"",
|
||||||
|
"path": ""all"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/index/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""index"",
|
||||||
|
"path": ""/"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should not generate colliding route names when hyphens are in file name": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)",
|
||||||
|
"name": ""parent-child"",
|
||||||
|
"path": ""/parent/:child()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)",
|
||||||
|
"name": ""parent-child"",
|
||||||
|
"path": ""/parent-:child()"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should not merge required param as a child of optional param": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[[foo]].vue").then(m => m.default || m)",
|
||||||
|
"name": ""foo"",
|
||||||
|
"path": ""/:foo?"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[foo].vue").then(m => m.default || m)",
|
||||||
|
"name": ""foo"",
|
||||||
|
"path": ""/:foo()"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should only allow "_" & "." as special character for dynamic route": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[a1_1a].vue").then(m => m.default || m)",
|
||||||
|
"name": ""a1_1a"",
|
||||||
|
"path": ""/:a1_1a()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)",
|
||||||
|
"name": ""b2.2b"",
|
||||||
|
"path": ""/:b2.2b()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[b2]_[2b].vue").then(m => m.default || m)",
|
||||||
|
"name": ""b2_2b"",
|
||||||
|
"path": ""/:b2()_:2b()"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)",
|
||||||
|
"name": ""c33c"",
|
||||||
|
"path": ""/:c33c?"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)",
|
||||||
|
"name": ""d44d"",
|
||||||
|
"path": ""/:d44d?"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should properly override route name if definePageMeta name override is defined.": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""home"",
|
||||||
|
"path": ""/"",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"should use fallbacks when normalized with `overrideMeta: true`": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name",
|
||||||
|
"path": ""/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
@ -1,15 +1,28 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import type { NuxtPage } from 'nuxt/schema'
|
import type { NuxtPage } from 'nuxt/schema'
|
||||||
import { generateRoutesFromFiles, pathToNitroGlob } from '../src/pages/utils'
|
import { generateRoutesFromFiles, normalizeRoutes, pathToNitroGlob } 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 layerDir = 'layer/pages'
|
const layerDir = 'layer/pages'
|
||||||
|
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||||
|
|
||||||
|
vi.mock('knitwork', async (original) => {
|
||||||
|
return {
|
||||||
|
...(await original<typeof import('knitwork')>()),
|
||||||
|
'genArrayFromRaw': (val: any) => val,
|
||||||
|
'genSafeVariableName': (..._args: string[]) => {
|
||||||
|
return 'mock'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const tests: Array<{
|
const tests: Array<{
|
||||||
description: string
|
description: string
|
||||||
files: Array<{ path: string; template?: string; }>
|
files?: Array<{ path: string; template?: string; }>
|
||||||
output?: NuxtPage[]
|
output?: NuxtPage[]
|
||||||
|
normalized?: Record<string, any>[]
|
||||||
error?: string
|
error?: string
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
@ -458,30 +471,118 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/'
|
path: '/'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should use fallbacks when normalized with `overrideMeta: true`',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: `${pagesDir}/index.vue`,
|
||||||
|
template: `
|
||||||
|
<script setup lang="ts">
|
||||||
|
const routeName = ref('home')
|
||||||
|
const routeAliases = ref(['sweet-home'])
|
||||||
|
definePageMeta({
|
||||||
|
name: routeName.value,
|
||||||
|
alias: routeAliases.value,
|
||||||
|
hello: 'world',
|
||||||
|
redirect: () => '/'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
name: 'index',
|
||||||
|
path: '/',
|
||||||
|
file: `${pagesDir}/index.vue`,
|
||||||
|
meta: { [DYNAMIC_META_KEY]: new Set(['name', 'alias', 'redirect', 'meta']) },
|
||||||
|
children: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should extract serializable values and override fallback when normalized with `overrideMeta: true`',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: `${pagesDir}/index.vue`,
|
||||||
|
template: `
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
name: 'home',
|
||||||
|
alias: ['sweet-home'],
|
||||||
|
redirect: '/',
|
||||||
|
hello: 'world'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
file: `${pagesDir}/index.vue`,
|
||||||
|
alias: ['sweet-home'],
|
||||||
|
redirect: '/',
|
||||||
|
children: [],
|
||||||
|
meta: { [DYNAMIC_META_KEY]: new Set(['meta']) },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'route without file',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
path: '/',
|
||||||
|
alias: ['sweet-home'],
|
||||||
|
meta: { hello: 'world' },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const normalizedResults: Record<string, any> = {}
|
||||||
|
const normalizedOverrideMetaResults: Record<string, any> = {}
|
||||||
|
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
it(test.description, async () => {
|
it(test.description, async () => {
|
||||||
|
|
||||||
|
let result
|
||||||
|
if (test.files) {
|
||||||
const vfs = Object.fromEntries(
|
const vfs = Object.fromEntries(
|
||||||
test.files.map(file => [file.path, 'template' in file ? file.template : ''])
|
test.files.map(file => [file.path, 'template' in file ? file.template : ''])
|
||||||
) as Record<string, string>
|
) as Record<string, string>
|
||||||
|
|
||||||
let result
|
|
||||||
try {
|
try {
|
||||||
result = await generateRoutesFromFiles(test.files.map(file => ({
|
result = await generateRoutesFromFiles(test.files.map(file => ({
|
||||||
absolutePath: file.path,
|
absolutePath: file.path,
|
||||||
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
||||||
})), true, vfs)
|
})), { shouldExtractBuildMeta: true, vfs })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
expect(error.message).toEqual(test.error)
|
expect(error.message).toEqual(test.error)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
result = test.output ?? []
|
||||||
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
expect(result).toEqual(test.output)
|
expect(result).toEqual(test.output)
|
||||||
|
normalizedResults[test.description] = normalizeRoutes(result, new Set()).routes
|
||||||
|
normalizedOverrideMetaResults[test.description] = normalizeRoutes(result, new Set(), true).routes
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('should consistently normalize routes', async () => {
|
||||||
|
await expect(normalizedResults).toMatchFileSnapshot('./__snapshots__/pages-override-meta-disabled.test.ts.snap')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should consistently normalize routes when overriding meta', async () => {
|
||||||
|
await expect(normalizedOverrideMetaResults).toMatchFileSnapshot('./__snapshots__/pages-override-meta-enabled.test.ts.snap')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('pages:generateRouteKey', () => {
|
describe('pages:generateRouteKey', () => {
|
||||||
|
@ -261,6 +261,15 @@ export default defineUntypedSchema({
|
|||||||
*/
|
*/
|
||||||
inlineRouteRules: false,
|
inlineRouteRules: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow exposing some route metadata defined in `definePageMeta` at build-time to modules (alias, name, path, redirect).
|
||||||
|
*
|
||||||
|
* This only works with static or strings/arrays rather than variables or conditional assignment.
|
||||||
|
*
|
||||||
|
* https://github.com/nuxt/nuxt/issues/24770
|
||||||
|
*/
|
||||||
|
scanPageMeta: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant
|
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant
|
||||||
* performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same
|
* performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same
|
||||||
|
Loading…
Reference in New Issue
Block a user