mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
fix(nuxt): extract route rules/page meta in 2+ script blocks (#28625)
This commit is contained in:
parent
161a1f10ee
commit
2ccdaa14cb
@ -18,30 +18,33 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
|
||||
}
|
||||
if (!ROUTE_RULE_RE.test(code)) { return null }
|
||||
|
||||
const script = extractScriptContent(code)
|
||||
code = script?.code || code
|
||||
|
||||
let rule: NitroRouteConfig | null = null
|
||||
const contents = extractScriptContent(code)
|
||||
for (const script of contents) {
|
||||
if (rule) { break }
|
||||
|
||||
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
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 === 'defineRouteRules') {
|
||||
const rulesString = js.code.slice(node.start, node.end)
|
||||
try {
|
||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
||||
} catch {
|
||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
||||
code = script?.code || code
|
||||
|
||||
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
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 === 'defineRouteRules') {
|
||||
const rulesString = js.code.slice(node.start, node.end)
|
||||
try {
|
||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
||||
} catch {
|
||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ruleCache[code] = rule
|
||||
return rule
|
||||
|
@ -168,21 +168,23 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
|
||||
return augmentedPages
|
||||
}
|
||||
|
||||
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i
|
||||
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi
|
||||
export function extractScriptContent (html: string) {
|
||||
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
|
||||
|
||||
if (groups.content) {
|
||||
return {
|
||||
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
||||
code: groups.content.trim(),
|
||||
} as const
|
||||
const contents: Array<{ loader: 'tsx' | 'ts', code: string }> = []
|
||||
for (const match of html.matchAll(SFC_SCRIPT_RE)) {
|
||||
if (match?.groups?.content) {
|
||||
contents.push({
|
||||
loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
||||
code: match.groups.content.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return contents
|
||||
}
|
||||
|
||||
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
|
||||
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
|
||||
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||
|
||||
const pageContentsCache: Record<string, string> = {}
|
||||
@ -197,100 +199,101 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
|
||||
if (absolutePath in metaCache) { return metaCache[absolutePath] }
|
||||
|
||||
const loader = getLoader(absolutePath)
|
||||
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader }
|
||||
if (!script) {
|
||||
const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }]
|
||||
if (!scriptBlocks) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
if (!PAGE_META_RE.test(script.code)) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
const js = await transform(script.code, { loader: script.loader })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as unknown as Program
|
||||
|
||||
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
|
||||
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
|
||||
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||
|
||||
let foundMeta = false
|
||||
for (const script of scriptBlocks) {
|
||||
if (!PAGE_META_RE.test(script.code)) {
|
||||
continue
|
||||
}
|
||||
|
||||
walk(ast, {
|
||||
enter (node) {
|
||||
if (foundMeta) { return }
|
||||
const js = await transform(script.code, { loader: script.loader })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as unknown as Program
|
||||
|
||||
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||
|
||||
foundMeta = true
|
||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
let foundMeta = false
|
||||
|
||||
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 }
|
||||
walk(ast, {
|
||||
enter (node) {
|
||||
if (foundMeta) { return }
|
||||
|
||||
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 (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
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}\`).`)
|
||||
foundMeta = true
|
||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
|
||||
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
|
||||
}
|
||||
values.push(element.value)
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
for (const property of pageMetaArgument.properties) {
|
||||
if (property.type !== 'Property') {
|
||||
continue
|
||||
}
|
||||
const isIdentifierOrLiteral = property.key.type === 'Literal' || property.key.type === 'Identifier'
|
||||
if (!isIdentifierOrLiteral) {
|
||||
continue
|
||||
}
|
||||
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
|
||||
if (!(extractionKeys as unknown as string[]).includes(name)) {
|
||||
dynamicProperties.add('meta')
|
||||
break
|
||||
}
|
||||
}
|
||||
extractedMeta[key] = property.value.value
|
||||
}
|
||||
|
||||
for (const property of pageMetaArgument.properties) {
|
||||
if (property.type !== 'Property') {
|
||||
continue
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
const isIdentifierOrLiteral = property.key.type === 'Literal' || property.key.type === 'Identifier'
|
||||
if (!isIdentifierOrLiteral) {
|
||||
continue
|
||||
}
|
||||
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
|
||||
if (!(extractionKeys as unknown as string[]).includes(name)) {
|
||||
dynamicProperties.add('meta')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
metaCache[absolutePath] = extractedMeta
|
||||
return extractedMeta
|
||||
|
@ -58,6 +58,41 @@ describe('page metadata', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
it('should extract serialisable metadata from files with multiple blocks', async () => {
|
||||
const meta = await getRouteMeta(`
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'thing'
|
||||
}
|
||||
</script>
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
name: 'some-custom-name',
|
||||
path: '/some-custom-path',
|
||||
validate: () => true,
|
||||
middleware: [
|
||||
function () {},
|
||||
],
|
||||
otherValue: {
|
||||
foo: 'bar',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
`, filePath)
|
||||
|
||||
expect(meta).toMatchInlineSnapshot(`
|
||||
{
|
||||
"meta": {
|
||||
"__nuxt_dynamic_meta_key": Set {
|
||||
"meta",
|
||||
},
|
||||
},
|
||||
"name": "some-custom-name",
|
||||
"path": "/some-custom-path",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should extract serialisable metadata in options api', async () => {
|
||||
const meta = await getRouteMeta(`
|
||||
<script>
|
||||
|
Loading…
Reference in New Issue
Block a user