mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
fix(nuxt): extract route rules/page meta in 2+ script blocks (#28625)
This commit is contained in:
parent
81774f3a5a
commit
eed8730688
@ -18,30 +18,33 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
|
|||||||
}
|
}
|
||||||
if (!ROUTE_RULE_RE.test(code)) { return null }
|
if (!ROUTE_RULE_RE.test(code)) { return null }
|
||||||
|
|
||||||
const script = extractScriptContent(code)
|
|
||||||
code = script?.code || code
|
|
||||||
|
|
||||||
let rule: NitroRouteConfig | null = null
|
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' })
|
code = script?.code || code
|
||||||
walk(parse(js.code, {
|
|
||||||
sourceType: 'module',
|
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||||
ecmaVersion: 'latest',
|
walk(parse(js.code, {
|
||||||
}) as Node, {
|
sourceType: 'module',
|
||||||
enter (_node) {
|
ecmaVersion: 'latest',
|
||||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
}) as Node, {
|
||||||
const node = _node as CallExpression & { start: number, end: number }
|
enter (_node) {
|
||||||
const name = 'name' in node.callee && node.callee.name
|
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||||
if (name === 'defineRouteRules') {
|
const node = _node as CallExpression & { start: number, end: number }
|
||||||
const rulesString = js.code.slice(node.start, node.end)
|
const name = 'name' in node.callee && node.callee.name
|
||||||
try {
|
if (name === 'defineRouteRules') {
|
||||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
const rulesString = js.code.slice(node.start, node.end)
|
||||||
} catch {
|
try {
|
||||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
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
|
ruleCache[code] = rule
|
||||||
return rule
|
return rule
|
||||||
|
@ -168,21 +168,23 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
|
|||||||
return augmentedPages
|
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) {
|
export function extractScriptContent (html: string) {
|
||||||
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
|
const contents: Array<{ loader: 'tsx' | 'ts', code: string }> = []
|
||||||
|
for (const match of html.matchAll(SFC_SCRIPT_RE)) {
|
||||||
if (groups.content) {
|
if (match?.groups?.content) {
|
||||||
return {
|
contents.push({
|
||||||
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
||||||
code: groups.content.trim(),
|
code: match.groups.content.trim(),
|
||||||
} as const
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return contents
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
|
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 DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||||
|
|
||||||
const pageContentsCache: Record<string, string> = {}
|
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] }
|
if (absolutePath in metaCache) { return metaCache[absolutePath] }
|
||||||
|
|
||||||
const loader = getLoader(absolutePath)
|
const loader = getLoader(absolutePath)
|
||||||
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader }
|
const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }]
|
||||||
if (!script) {
|
if (!scriptBlocks) {
|
||||||
metaCache[absolutePath] = {}
|
metaCache[absolutePath] = {}
|
||||||
return {}
|
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 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, {
|
const js = await transform(script.code, { loader: script.loader })
|
||||||
enter (node) {
|
const ast = parse(js.code, {
|
||||||
if (foundMeta) { return }
|
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
|
let foundMeta = false
|
||||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
|
||||||
|
|
||||||
for (const key of extractionKeys) {
|
walk(ast, {
|
||||||
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
enter (node) {
|
||||||
if (!property) { continue }
|
if (foundMeta) { return }
|
||||||
|
|
||||||
if (property.value.type === 'ObjectExpression') {
|
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||||
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') {
|
foundMeta = true
|
||||||
const values: string[] = []
|
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||||
for (const element of property.value.elements) {
|
|
||||||
if (!element) {
|
for (const key of extractionKeys) {
|
||||||
continue
|
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||||
}
|
if (!property) { 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}\`).`)
|
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)
|
dynamicProperties.add(key)
|
||||||
continue
|
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') {
|
for (const property of pageMetaArgument.properties) {
|
||||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
if (property.type !== 'Property') {
|
||||||
dynamicProperties.add(key)
|
continue
|
||||||
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 (name) {
|
||||||
|
dynamicProperties.add('meta')
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
extractedMeta[key] = property.value.value
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const property of pageMetaArgument.properties) {
|
if (dynamicProperties.size) {
|
||||||
if (property.type !== 'Property') {
|
extractedMeta.meta ??= {}
|
||||||
continue
|
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 (name) {
|
|
||||||
dynamicProperties.add('meta')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dynamicProperties.size) {
|
|
||||||
extractedMeta.meta ??= {}
|
|
||||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
metaCache[absolutePath] = extractedMeta
|
metaCache[absolutePath] = extractedMeta
|
||||||
return extractedMeta
|
return extractedMeta
|
||||||
|
@ -61,6 +61,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 () => {
|
it('should extract serialisable metadata in options api', async () => {
|
||||||
const meta = await getRouteMeta(`
|
const meta = await getRouteMeta(`
|
||||||
<script>
|
<script>
|
||||||
|
Loading…
Reference in New Issue
Block a user