From ba764b212ff40d811caae9ab9eee0c4e1f896571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20=C4=8Cern=C3=BD?= <112722215+cernymatej@users.noreply.github.com> Date: Thu, 26 Dec 2024 23:14:17 +0100 Subject: [PATCH] fix(nuxt): ignore non-reference identifiers when extracting page metadata (#30381) --- packages/nuxt/src/core/utils/parse.ts | 2 +- packages/nuxt/src/pages/plugins/page-meta.ts | 55 +++++++++----- packages/nuxt/test/page-metadata.test.ts | 77 ++++++++++++++++++++ 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/packages/nuxt/src/core/utils/parse.ts b/packages/nuxt/src/core/utils/parse.ts index c60aefa82a..c063dbcdfa 100644 --- a/packages/nuxt/src/core/utils/parse.ts +++ b/packages/nuxt/src/core/utils/parse.ts @@ -476,7 +476,7 @@ function getPatternIdentifiers (pattern: WithLocations) { return identifiers } -function isNotReferencePosition (node: WithLocations, parent: WithLocations | null) { +export function isNotReferencePosition (node: WithLocations, parent: WithLocations | null) { if (!parent || node.type !== 'Identifier') { return false } switch (parent.type) { diff --git a/packages/nuxt/src/pages/plugins/page-meta.ts b/packages/nuxt/src/pages/plugins/page-meta.ts index ff179fb68d..366db0848f 100644 --- a/packages/nuxt/src/pages/plugins/page-meta.ts +++ b/packages/nuxt/src/pages/plugins/page-meta.ts @@ -3,7 +3,6 @@ import { createUnplugin } from 'unplugin' import { parseQuery, parseURL } from 'ufo' import type { StaticImport } from 'mlly' import { findExports, findStaticImports, parseStaticImport } from 'mlly' -import { walk } from 'estree-walker' import MagicString from 'magic-string' import { isAbsolute } from 'pathe' import { logger } from '@nuxt/kit' @@ -12,7 +11,9 @@ import { ScopeTracker, type ScopeTrackerNode, getUndeclaredIdentifiersInFunction, + isNotReferencePosition, parseAndWalk, + walk, withLocations, } from '../../core/utils/parse' @@ -147,12 +148,26 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp declarationNodes.push(node) } - function addImportOrDeclaration (name: string) { + /** + * Adds an import or a declaration to the extracted code. + * @param name The name of the import or declaration to add. + * @param node The node that is currently being processed. (To detect self-references) + */ + function addImportOrDeclaration (name: string, node?: ScopeTrackerNode) { if (isStaticIdentifier(name)) { addImport(name) } else { const declaration = scopeTracker.getDeclaration(name) - if (declaration) { + /* + Without checking for `declaration !== node`, we would end up in an infinite loop + when, for example, a variable is declared and then used in its own initializer. + (we shouldn't mask the underlying error by throwing a `Maximum call stack size exceeded` error) + + ```ts + const a = { b: a } + ``` + */ + if (declaration && declaration !== node) { processDeclaration(declaration) } } @@ -160,32 +175,35 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp const scopeTracker = new ScopeTracker() - function processDeclaration (node: ScopeTrackerNode | null) { - if (node?.type === 'Variable') { - addDeclaration(node) + function processDeclaration (scopeTrackerNode: ScopeTrackerNode | null) { + if (scopeTrackerNode?.type === 'Variable') { + addDeclaration(scopeTrackerNode) - for (const decl of node.variableNode.declarations) { + for (const decl of scopeTrackerNode.variableNode.declarations) { if (!decl.init) { continue } walk(decl.init, { - enter: (node) => { + enter: (node, parent) => { if (node.type === 'AwaitExpression') { logger.error(`[nuxt] Await expressions are not supported in definePageMeta. File: '${id}'`) throw new Error('await in definePageMeta') } - if (node.type !== 'Identifier') { return } + if ( + isNotReferencePosition(node, parent) + || node.type !== 'Identifier' // checking for `node.type` to narrow down the type + ) { return } - addImportOrDeclaration(node.name) + addImportOrDeclaration(node.name, scopeTrackerNode) }, }) } - } else if (node?.type === 'Function') { + } else if (scopeTrackerNode?.type === 'Function') { // arrow functions are going to be assigned to a variable - if (node.node.type === 'ArrowFunctionExpression') { return } - const name = node.node.id?.name + if (scopeTrackerNode.node.type === 'ArrowFunctionExpression') { return } + const name = scopeTrackerNode.node.id?.name if (!name) { return } - addDeclaration(node) + addDeclaration(scopeTrackerNode) - const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(node.node) + const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(scopeTrackerNode.node) for (const name of undeclaredIdentifiers) { addImportOrDeclaration(name) } @@ -203,8 +221,11 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp if (!meta) { return } walk(meta, { - enter (node) { - if (node.type !== 'Identifier') { return } + enter (node, parent) { + if ( + isNotReferencePosition(node, parent) + || node.type !== 'Identifier' // checking for `node.type` to narrow down the type + ) { return } if (isStaticIdentifier(node.name)) { addImport(node.name) diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts index 0513b97148..e320aecb99 100644 --- a/packages/nuxt/test/page-metadata.test.ts +++ b/packages/nuxt/test/page-metadata.test.ts @@ -467,4 +467,81 @@ definePageMeta({ expect(wasErrorThrown).toBe(true) }) + + it('should only add definitions for reference identifiers', () => { + const sfc = ` + + ` + const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' }) + expect(transformPlugin.transform.call({ + parse: (code: string, opts: any = {}) => Parser.parse(code, { + sourceType: 'module', + ecmaVersion: 'latest', + locations: true, + ...opts, + }), + }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(` + "const foo = 'foo' + const num = 1 + const bar = { bar: 'bar' }.bar, baz = { baz: 'baz' }.baz, x = { foo } + const useVal = () => ({ val: 'val' }) + function recursive () { + recursive() + } + const __nuxt_page_meta = { + middleware: [ + () => { + console.log(bar, baz) + recursive() + + const val = useVal().val + const obj = { + num, + prop: 'prop', + } + + const c = class test { + prop = 'prop' + test () {} + } + }, + ], + } + export default __nuxt_page_meta" + `) + }) })