mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +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
|
||||
|
||||
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) => {
|
||||
if (event === 'change') { return }
|
||||
|
||||
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({
|
||||
filter: template => template.filename === 'routes.mjs'
|
||||
})
|
||||
@ -398,7 +404,7 @@ export default defineNuxtModule({
|
||||
filename: 'routes.mjs',
|
||||
getContents ({ app }) {
|
||||
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')
|
||||
}
|
||||
})
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import fs from 'node:fs'
|
||||
import { extname, normalize, relative, resolve } from 'pathe'
|
||||
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
|
||||
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')
|
||||
}
|
||||
|
||||
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[] = []
|
||||
|
||||
for (const file of files) {
|
||||
@ -101,12 +110,9 @@ export async function generateRoutesFromFiles (files: ScannedFile[], shouldExtra
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldExtractBuildMeta && vfs) {
|
||||
const fileContent = file.absolutePath in vfs ? vfs[file.absolutePath] : fs.readFileSync(file.absolutePath, 'utf-8')
|
||||
const overrideRouteName = await getRouteName(fileContent)
|
||||
if (overrideRouteName) {
|
||||
route.name = overrideRouteName
|
||||
}
|
||||
if (options.shouldExtractBuildMeta && options.vfs) {
|
||||
const fileContent = file.absolutePath in options.vfs ? options.vfs[file.absolutePath] : fs.readFileSync(file.absolutePath, 'utf-8')
|
||||
Object.assign(route, await getRouteMeta(fileContent, file.absolutePath))
|
||||
}
|
||||
|
||||
parent.push(route)
|
||||
@ -127,26 +133,96 @@ export function extractScriptContent (html: string) {
|
||||
}
|
||||
|
||||
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/
|
||||
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||
|
||||
async function getRouteName (file: string) {
|
||||
const script = extractScriptContent(file)
|
||||
if (!script) { return null }
|
||||
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
|
||||
async function getRouteMeta (contents: string, absolutePath?: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
|
||||
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 ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest'
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true
|
||||
}) 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 }
|
||||
if (!pageMetaAST) {
|
||||
metaCache[contents] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
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 }
|
||||
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
|
||||
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 {
|
||||
@ -301,26 +377,42 @@ function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<s
|
||||
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 {
|
||||
imports: metaImports,
|
||||
routes: genArrayFromRaw(routes.map((page) => {
|
||||
const route: Record<Exclude<keyof NuxtPage, 'file'>, string> & { component?: string } = Object.create(null)
|
||||
for (const [key, value] of Object.entries(page)) {
|
||||
if (key !== 'file' && (Array.isArray(value) ? value.length : value)) {
|
||||
route[key as Exclude<keyof NuxtPage, 'file'>] = JSON.stringify(value)
|
||||
const markedDynamic = page.meta?.[DYNAMIC_META_KEY] ?? new Set()
|
||||
const metaFiltered: Record<string, any> = {}
|
||||
let skipMeta = true
|
||||
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) {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
@ -328,20 +420,56 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
|
||||
const metaImportName = genSafeVariableName(filename(file) + hash(file)) + 'Meta'
|
||||
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'default', as: metaImportName }]))
|
||||
|
||||
let aliasCode = `${metaImportName}?.alias || []`
|
||||
const alias = toArray(page.alias).filter(Boolean)
|
||||
if (alias.length) {
|
||||
aliasCode = `${JSON.stringify(alias)}.concat(${aliasCode})`
|
||||
const metaRoute: NormalizedRoute = {
|
||||
name: `${metaImportName}?.name ?? ${route.name}`,
|
||||
path: `${metaImportName}?.path ?? ${route.path}`,
|
||||
meta: `${metaImportName} || {}`,
|
||||
alias: `${metaImportName}?.alias || []`,
|
||||
redirect: `${metaImportName}?.redirect`,
|
||||
component: genDynamicImport(file, { interopDefault: true })
|
||||
}
|
||||
|
||||
route.name = `${metaImportName}?.name ?? ${page.name ? JSON.stringify(page.name) : 'undefined'}`
|
||||
route.path = `${metaImportName}?.path ?? ${JSON.stringify(page.path)}`
|
||||
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 })
|
||||
if (route.children != null) {
|
||||
metaRoute.children = route.children
|
||||
}
|
||||
|
||||
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 { generateRoutesFromFiles, pathToNitroGlob } from '../src/pages/utils'
|
||||
import { generateRoutesFromFiles, normalizeRoutes, pathToNitroGlob } from '../src/pages/utils'
|
||||
import { generateRouteKey } from '../src/pages/runtime/utils'
|
||||
|
||||
describe('pages:generateRoutesFromFiles', () => {
|
||||
const pagesDir = '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<{
|
||||
description: string
|
||||
files: Array<{ path: string; template?: string; }>
|
||||
files?: Array<{ path: string; template?: string; }>
|
||||
output?: NuxtPage[]
|
||||
normalized?: Record<string, any>[]
|
||||
error?: string
|
||||
}> = [
|
||||
{
|
||||
@ -458,30 +471,118 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
name: 'index',
|
||||
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) {
|
||||
it(test.description, async () => {
|
||||
const vfs = Object.fromEntries(
|
||||
test.files.map(file => [file.path, 'template' in file ? file.template : ''])
|
||||
) as Record<string, string>
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await generateRoutesFromFiles(test.files.map(file => ({
|
||||
absolutePath: file.path,
|
||||
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
||||
})), true, vfs)
|
||||
} catch (error: any) {
|
||||
expect(error.message).toEqual(test.error)
|
||||
if (test.files) {
|
||||
const vfs = Object.fromEntries(
|
||||
test.files.map(file => [file.path, 'template' in file ? file.template : ''])
|
||||
) as Record<string, string>
|
||||
|
||||
try {
|
||||
result = await generateRoutesFromFiles(test.files.map(file => ({
|
||||
absolutePath: file.path,
|
||||
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
||||
})), { shouldExtractBuildMeta: true, vfs })
|
||||
} catch (error: any) {
|
||||
expect(error.message).toEqual(test.error)
|
||||
}
|
||||
} else {
|
||||
result = test.output ?? []
|
||||
}
|
||||
|
||||
if (result) {
|
||||
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', () => {
|
||||
|
@ -261,6 +261,15 @@ export default defineUntypedSchema({
|
||||
*/
|
||||
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
|
||||
* performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same
|
||||
|
Loading…
Reference in New Issue
Block a user