mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 23:22:02 +00:00
fix(nuxt)!: use parser to generate page metadata (#8536)
This commit is contained in:
parent
f485c143f5
commit
491d02f6ca
@ -50,6 +50,7 @@
|
|||||||
"defu": "^6.1.0",
|
"defu": "^6.1.0",
|
||||||
"destr": "^1.2.0",
|
"destr": "^1.2.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
|
"estree-walker": "^3.0.1",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"globby": "^13.1.2",
|
"globby": "^13.1.2",
|
||||||
"h3": "^0.8.6",
|
"h3": "^0.8.6",
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
import { pathToFileURL } from 'node:url'
|
|
||||||
import { createUnplugin } from 'unplugin'
|
|
||||||
import { parseQuery, parseURL, withQuery } from 'ufo'
|
|
||||||
import { findStaticImports, findExports } from 'mlly'
|
|
||||||
import MagicString from 'magic-string'
|
|
||||||
import { isAbsolute } from 'pathe'
|
|
||||||
|
|
||||||
export interface TransformMacroPluginOptions {
|
|
||||||
macros: Record<string, string>
|
|
||||||
dev?: boolean
|
|
||||||
sourcemap?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TransformMacroPlugin = createUnplugin((options: TransformMacroPluginOptions) => {
|
|
||||||
return {
|
|
||||||
name: 'nuxt:pages-macros-transform',
|
|
||||||
enforce: 'post',
|
|
||||||
transformInclude (id) {
|
|
||||||
if (!id || id.startsWith('\x00')) { return false }
|
|
||||||
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
|
||||||
return pathname.endsWith('.vue') || !!parseQuery(search).macro
|
|
||||||
},
|
|
||||||
transform (code, id) {
|
|
||||||
const s = new MagicString(code)
|
|
||||||
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
|
||||||
|
|
||||||
function result () {
|
|
||||||
if (s.hasChanged()) {
|
|
||||||
return {
|
|
||||||
code: s.toString(),
|
|
||||||
map: options.sourcemap
|
|
||||||
? s.generateMap({ source: id, includeContent: true })
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tree-shake out any runtime references to the macro.
|
|
||||||
// We do this first as it applies to all files, not just those with the query
|
|
||||||
for (const macro in options.macros) {
|
|
||||||
const match = code.match(new RegExp(`\\b${macro}\\s*\\(\\s*`))
|
|
||||||
if (match?.[0]) {
|
|
||||||
s.overwrite(match.index!, match.index! + match[0].length, `/*#__PURE__*/ false && ${match[0]}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parseQuery(search).macro) {
|
|
||||||
return result()
|
|
||||||
}
|
|
||||||
|
|
||||||
const imports = findStaticImports(code)
|
|
||||||
|
|
||||||
// Purge all imports bringing side effects, such as CSS imports
|
|
||||||
for (const entry of imports) {
|
|
||||||
if (!entry.imports) {
|
|
||||||
s.remove(entry.start, entry.end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [webpack] Re-export any imports from script blocks in the components
|
|
||||||
// with workaround for vue-loader bug: https://github.com/vuejs/vue-loader/pull/1911
|
|
||||||
const scriptImport = imports.find(i => parseQuery(i.specifier.replace('?macro=true', '')).type === 'script')
|
|
||||||
if (scriptImport) {
|
|
||||||
// https://github.com/vuejs/vue-loader/pull/1911
|
|
||||||
// https://github.com/vitejs/vite/issues/8473
|
|
||||||
const url = isAbsolute(scriptImport.specifier) ? pathToFileURL(scriptImport.specifier).href : scriptImport.specifier
|
|
||||||
const parsed = parseURL(decodeURIComponent(url).replace('?macro=true', ''))
|
|
||||||
const specifier = withQuery(parsed.pathname, { macro: 'true', ...parseQuery(parsed.search) })
|
|
||||||
s.overwrite(0, code.length, `export { meta } from "${specifier}"`)
|
|
||||||
return result()
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentExports = findExports(code)
|
|
||||||
for (const match of currentExports) {
|
|
||||||
if (match.type !== 'default') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (match.specifier && match._type === 'named') {
|
|
||||||
// [webpack] Export named exports rather than the default (component)
|
|
||||||
s.overwrite(match.start, match.end, `export {${Object.values(options.macros).join(', ')}} from "${match.specifier}"`)
|
|
||||||
return result()
|
|
||||||
} else if (!options.dev) {
|
|
||||||
// ensure we tree-shake any _other_ default exports out of the macro script
|
|
||||||
s.overwrite(match.start, match.end, '/*#__PURE__*/ false &&')
|
|
||||||
s.append('\nexport default {}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const macro in options.macros) {
|
|
||||||
// Skip already-processed macros
|
|
||||||
if (currentExports.some(e => e.name === options.macros[macro])) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const { 0: match, index = 0 } = code.match(new RegExp(`\\b${macro}\\s*\\(\\s*`)) || {} as RegExpMatchArray
|
|
||||||
const macroContent = match ? extractObject(code.slice(index + match.length)) : 'undefined'
|
|
||||||
|
|
||||||
s.append(`\nexport const ${options.macros[macro]} = ${macroContent}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const starts = {
|
|
||||||
'{': '}',
|
|
||||||
'[': ']',
|
|
||||||
'(': ')',
|
|
||||||
'<': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUOTE_RE = /["']/
|
|
||||||
|
|
||||||
function extractObject (code: string) {
|
|
||||||
// Strip comments
|
|
||||||
code = code.replace(/^\s*\/\/.*$/gm, '')
|
|
||||||
|
|
||||||
const stack: string[] = []
|
|
||||||
let result = ''
|
|
||||||
do {
|
|
||||||
if (stack[0] === code[0] && result.slice(-1) !== '\\') {
|
|
||||||
stack.shift()
|
|
||||||
} else if (code[0] in starts && !QUOTE_RE.test(stack[0])) {
|
|
||||||
stack.unshift(starts[code[0] as keyof typeof starts])
|
|
||||||
}
|
|
||||||
result += code[0]
|
|
||||||
code = code.slice(1)
|
|
||||||
} while (stack.length && code.length)
|
|
||||||
return result
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ import type { NuxtApp, NuxtPage } from '@nuxt/schema'
|
|||||||
import { joinURL } from 'ufo'
|
import { joinURL } from 'ufo'
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
import { resolvePagesRoutes, normalizeRoutes } from './utils'
|
import { resolvePagesRoutes, normalizeRoutes } from './utils'
|
||||||
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'
|
import { PageMetaPlugin, PageMetaPluginOptions } from './page-meta'
|
||||||
|
|
||||||
export default defineNuxtModule({
|
export default defineNuxtModule({
|
||||||
meta: {
|
meta: {
|
||||||
@ -64,7 +64,9 @@ export default defineNuxtModule({
|
|||||||
|
|
||||||
const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join('|')})/`)
|
const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join('|')})/`)
|
||||||
if (event !== 'change' && path.match(pathPattern)) {
|
if (event !== 'change' && path.match(pathPattern)) {
|
||||||
await updateTemplates()
|
await updateTemplates({
|
||||||
|
filter: template => template.filename === 'routes.mjs'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -114,15 +116,15 @@ export default defineNuxtModule({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Extract macros from pages
|
// Extract macros from pages
|
||||||
const macroOptions: TransformMacroPluginOptions = {
|
const pageMetaOptions: PageMetaPluginOptions = {
|
||||||
dev: nuxt.options.dev,
|
dev: nuxt.options.dev,
|
||||||
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
|
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
|
||||||
macros: {
|
dirs: nuxt.options._layers.map(
|
||||||
definePageMeta: 'meta'
|
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
addVitePlugin(PageMetaPlugin.vite(pageMetaOptions))
|
||||||
addVitePlugin(TransformMacroPlugin.vite(macroOptions))
|
addWebpackPlugin(PageMetaPlugin.webpack(pageMetaOptions))
|
||||||
addWebpackPlugin(TransformMacroPlugin.webpack(macroOptions))
|
|
||||||
|
|
||||||
// Add router plugin
|
// Add router plugin
|
||||||
addPlugin(resolve(runtimeDir, 'router'))
|
addPlugin(resolve(runtimeDir, 'router'))
|
||||||
|
160
packages/nuxt/src/pages/page-meta.ts
Normal file
160
packages/nuxt/src/pages/page-meta.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import { parseQuery, parseURL, stringifyQuery } from 'ufo'
|
||||||
|
import { findStaticImports, findExports, StaticImport, parseStaticImport } from 'mlly'
|
||||||
|
import type { CallExpression, Expression } from 'estree'
|
||||||
|
import { walk } from 'estree-walker'
|
||||||
|
import MagicString from 'magic-string'
|
||||||
|
import { isAbsolute, normalize } from 'pathe'
|
||||||
|
|
||||||
|
export interface PageMetaPluginOptions {
|
||||||
|
dirs: Array<string | RegExp>
|
||||||
|
dev?: boolean
|
||||||
|
sourcemap?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageMetaPlugin = createUnplugin((options: PageMetaPluginOptions) => {
|
||||||
|
return {
|
||||||
|
name: 'nuxt:pages-macros-transform',
|
||||||
|
enforce: 'post',
|
||||||
|
transformInclude (id) {
|
||||||
|
const query = parseMacroQuery(id)
|
||||||
|
id = normalize(id)
|
||||||
|
|
||||||
|
const isPagesDir = options.dirs.some(dir => typeof dir === 'string' ? id.startsWith(dir) : dir.test(id))
|
||||||
|
if (!isPagesDir && !query.macro) { return false }
|
||||||
|
|
||||||
|
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
|
return /\.(m?[jt]sx?|vue)/.test(pathname)
|
||||||
|
},
|
||||||
|
transform (code, id) {
|
||||||
|
const query = parseMacroQuery(id)
|
||||||
|
if (query.type && query.type !== 'script') { return }
|
||||||
|
|
||||||
|
const s = new MagicString(code)
|
||||||
|
function result () {
|
||||||
|
if (s.hasChanged()) {
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: options.sourcemap
|
||||||
|
? s.generateMap({ source: id, includeContent: true })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMacro = code.match(/\bdefinePageMeta\s*\(\s*/)
|
||||||
|
|
||||||
|
// Remove any references to the macro from our pages
|
||||||
|
if (!query.macro) {
|
||||||
|
if (hasMacro) {
|
||||||
|
walk(this.parse(code, {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}), {
|
||||||
|
enter (_node) {
|
||||||
|
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||||
|
const node = _node as CallExpression & { start: number, end: number }
|
||||||
|
const name = 'name' in node.callee && node.callee.name
|
||||||
|
if (name === 'definePageMeta') {
|
||||||
|
s.overwrite(node.start, node.end, 'false && {}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result()
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = findStaticImports(code)
|
||||||
|
|
||||||
|
// [vite] Re-export any script imports
|
||||||
|
const scriptImport = imports.find(i => parseMacroQuery(i.specifier).type === 'script')
|
||||||
|
if (scriptImport) {
|
||||||
|
const specifier = rewriteQuery(scriptImport.specifier)
|
||||||
|
s.overwrite(0, code.length, `export { default } from ${JSON.stringify(specifier)}`)
|
||||||
|
return result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [webpack] Re-export any exports from script blocks in the components
|
||||||
|
const currentExports = findExports(code)
|
||||||
|
for (const match of currentExports) {
|
||||||
|
if (match.type !== 'default' || !match.specifier) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const specifier = rewriteQuery(match.specifier)
|
||||||
|
s.overwrite(0, code.length, `export { default } from ${JSON.stringify(specifier)}`)
|
||||||
|
return result()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMacro && !code.includes('export { default }') && !code.includes('__nuxt_page_meta')) {
|
||||||
|
s.overwrite(0, code.length, 'export default {}')
|
||||||
|
return result()
|
||||||
|
}
|
||||||
|
|
||||||
|
const importMap = new Map<string, StaticImport>()
|
||||||
|
for (const i of imports) {
|
||||||
|
const parsed = parseStaticImport(i)
|
||||||
|
for (const name of [
|
||||||
|
parsed.defaultImport,
|
||||||
|
...Object.keys(parsed.namedImports || {}),
|
||||||
|
parsed.namespacedImport
|
||||||
|
].filter(Boolean) as string[]) {
|
||||||
|
importMap.set(name, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(this.parse(code, {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}), {
|
||||||
|
enter (_node) {
|
||||||
|
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||||
|
const node = _node as CallExpression & { start: number, end: number }
|
||||||
|
const name = 'name' in node.callee && node.callee.name
|
||||||
|
if (name !== 'definePageMeta') { return }
|
||||||
|
|
||||||
|
const meta = node.arguments[0] as Expression & { start: number, end: number }
|
||||||
|
|
||||||
|
let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || '{}'}\nexport default __nuxt_page_meta`
|
||||||
|
|
||||||
|
walk(meta, {
|
||||||
|
enter (_node) {
|
||||||
|
if (_node.type === 'CallExpression') {
|
||||||
|
const node = _node as CallExpression & { start: number, end: number }
|
||||||
|
const name = 'name' in node.callee && node.callee.name
|
||||||
|
if (name && importMap.has(name)) {
|
||||||
|
contents = importMap.get(name)!.code + '\n' + contents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.overwrite(0, code.length, contents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {
|
||||||
|
s.overwrite(0, code.length, 'export default {}')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://github.com/vuejs/vue-loader/pull/1911
|
||||||
|
// https://github.com/vitejs/vite/issues/8473
|
||||||
|
function rewriteQuery (id: string) {
|
||||||
|
const query = stringifyQuery({ macro: 'true', ...parseMacroQuery(id) })
|
||||||
|
return id.replace(/\?.+$/, '?' + query)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMacroQuery (id: string) {
|
||||||
|
const { search } = parseURL(decodeURIComponent(isAbsolute(id) ? pathToFileURL(id).href : id).replace(/\?macro=true$/, ''))
|
||||||
|
const query = parseQuery(search)
|
||||||
|
if (id.includes('?macro=true')) {
|
||||||
|
return { macro: 'true', ...query }
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
@ -231,7 +231,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
|
|||||||
routes: genArrayFromRaw(routes.map((route) => {
|
routes: genArrayFromRaw(routes.map((route) => {
|
||||||
const file = normalize(route.file)
|
const file = normalize(route.file)
|
||||||
const metaImportName = genSafeVariableName(file) + 'Meta'
|
const metaImportName = genSafeVariableName(file) + 'Meta'
|
||||||
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'meta', as: metaImportName }]))
|
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'default', as: metaImportName }]))
|
||||||
|
|
||||||
let aliasCode = `${metaImportName}?.alias || []`
|
let aliasCode = `${metaImportName}?.alias || []`
|
||||||
if (Array.isArray(route.alias) && route.alias.length) {
|
if (Array.isArray(route.alias) && route.alias.length) {
|
||||||
|
@ -24,7 +24,7 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
|
|||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
transformInclude (id) {
|
transformInclude (id) {
|
||||||
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
return !pathname.match(/node_modules\/nuxt3?\//) && pathname.match(/\.(m?[jt]sx?|vue)/) && parseQuery(search).type !== 'style'
|
return !pathname.match(/node_modules\/nuxt3?\//) && pathname.match(/\.(m?[jt]sx?|vue)/) && parseQuery(search).type !== 'style' && !parseQuery(search).macro
|
||||||
},
|
},
|
||||||
transform (code, id) {
|
transform (code, id) {
|
||||||
if (!KEYED_FUNCTIONS_RE.test(code)) { return }
|
if (!KEYED_FUNCTIONS_RE.test(code)) { return }
|
||||||
|
@ -425,6 +425,7 @@ importers:
|
|||||||
defu: ^6.1.0
|
defu: ^6.1.0
|
||||||
destr: ^1.2.0
|
destr: ^1.2.0
|
||||||
escape-string-regexp: ^5.0.0
|
escape-string-regexp: ^5.0.0
|
||||||
|
estree-walker: ^3.0.1
|
||||||
fs-extra: ^10.1.0
|
fs-extra: ^10.1.0
|
||||||
globby: ^13.1.2
|
globby: ^13.1.2
|
||||||
h3: ^0.8.6
|
h3: ^0.8.6
|
||||||
@ -469,6 +470,7 @@ importers:
|
|||||||
defu: 6.1.0
|
defu: 6.1.0
|
||||||
destr: 1.2.0
|
destr: 1.2.0
|
||||||
escape-string-regexp: 5.0.0
|
escape-string-regexp: 5.0.0
|
||||||
|
estree-walker: 3.0.1
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
globby: 13.1.2
|
globby: 13.1.2
|
||||||
h3: 0.8.6
|
h3: 0.8.6
|
||||||
|
@ -600,8 +600,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || process.env.TEST_WITH_WEBPACK)('inl
|
|||||||
'{--scoped:"scoped"}', // <style lang=css>
|
'{--scoped:"scoped"}', // <style lang=css>
|
||||||
'{--postcss:"postcss"}', // <style lang=postcss>
|
'{--postcss:"postcss"}', // <style lang=postcss>
|
||||||
'{--global:"global"', // entryfile dependency
|
'{--global:"global"', // entryfile dependency
|
||||||
'{--plugin:"plugin"}', // plugin dependency
|
'{--plugin:"plugin"}' // plugin dependency
|
||||||
'{--functional:"functional"}' // functional component with css import
|
|
||||||
]) {
|
]) {
|
||||||
expect(html).toContain(style)
|
expect(html).toContain(style)
|
||||||
}
|
}
|
||||||
@ -609,7 +608,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || process.env.TEST_WITH_WEBPACK)('inl
|
|||||||
|
|
||||||
it('only renders prefetch for entry styles', async () => {
|
it('only renders prefetch for entry styles', async () => {
|
||||||
const html: string = await $fetch('/styles')
|
const html: string = await $fetch('/styles')
|
||||||
expect(html.match(/<link [^>]*href="[^"]*\.css">/)?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
|
expect(html.match(/<link [^>]*href="[^"]*\.css">/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"<link rel=\\"prefetch\\" as=\\"style\\" href=\\"/_nuxt/entry.css\\">",
|
"<link rel=\\"prefetch\\" as=\\"style\\" href=\\"/_nuxt/entry.css\\">",
|
||||||
]
|
]
|
||||||
|
7
test/fixtures/basic/pages/index.vue
vendored
7
test/fixtures/basic/pages/index.vue
vendored
@ -20,16 +20,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { setupDevtoolsPlugin } from '@vue/devtools-api'
|
import { setupDevtoolsPlugin } from '@vue/devtools-api'
|
||||||
import { useRuntimeConfig } from '#imports'
|
import { useRuntimeConfig } from '#imports'
|
||||||
|
|
||||||
setupDevtoolsPlugin({}, () => {})
|
setupDevtoolsPlugin({}, () => {}) as any
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
alias: '/some-alias'
|
alias: '/some-alias',
|
||||||
|
other: ref('test')
|
||||||
})
|
})
|
||||||
|
|
||||||
// reset title template example
|
// reset title template example
|
||||||
|
Loading…
Reference in New Issue
Block a user