fix(nuxt): extract route rules/page meta in 2+ script blocks (#28625)

This commit is contained in:
Daniel Roe 2024-08-21 12:38:18 +01:00
parent 81774f3a5a
commit eed8730688
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
3 changed files with 144 additions and 103 deletions

View File

@ -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

View File

@ -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

View File

@ -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>