From eed8730688ec9dba6d39c2729c2b9a300b0656de Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 21 Aug 2024 12:38:18 +0100 Subject: [PATCH] fix(nuxt): extract route rules/page meta in 2+ script blocks (#28625) --- packages/nuxt/src/pages/route-rules.ts | 45 +++--- packages/nuxt/src/pages/utils.ts | 167 ++++++++++++----------- packages/nuxt/test/page-metadata.test.ts | 35 +++++ 3 files changed, 144 insertions(+), 103 deletions(-) diff --git a/packages/nuxt/src/pages/route-rules.ts b/packages/nuxt/src/pages/route-rules.ts index 24acf217b1..599e0617c0 100644 --- a/packages/nuxt/src/pages/route-rules.ts +++ b/packages/nuxt/src/pages/route-rules.ts @@ -18,30 +18,33 @@ export async function extractRouteRules (code: string): Promise[^>]*)>(?[\s\S]*?)<\/script[^>]*>/i +const SFC_SCRIPT_RE = /[^>]*)>(?[\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 = {} @@ -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> - const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const - const dynamicProperties = new Set() - 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() - 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 (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 (name) { - dynamicProperties.add('meta') - break - } - } - - if (dynamicProperties.size) { - extractedMeta.meta ??= {} - extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties - } - }, - }) + }, + }) + } metaCache[absolutePath] = extractedMeta return extractedMeta diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts index d1815a2847..a5b30b5862 100644 --- a/packages/nuxt/test/page-metadata.test.ts +++ b/packages/nuxt/test/page-metadata.test.ts @@ -61,6 +61,41 @@ describe('page metadata', () => { `) }) + it('should extract serialisable metadata from files with multiple blocks', async () => { + const meta = await getRouteMeta(` + + + `, 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(`