fix(nuxt): ignore non-reference identifiers when extracting page metadata (#30381)

This commit is contained in:
Matej Černý 2024-12-26 23:14:17 +01:00 committed by GitHub
parent ce0c89c086
commit ba764b212f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 18 deletions

View File

@ -476,7 +476,7 @@ function getPatternIdentifiers (pattern: WithLocations<Node>) {
return identifiers return identifiers
} }
function isNotReferencePosition (node: WithLocations<Node>, parent: WithLocations<Node> | null) { export function isNotReferencePosition (node: WithLocations<Node>, parent: WithLocations<Node> | null) {
if (!parent || node.type !== 'Identifier') { return false } if (!parent || node.type !== 'Identifier') { return false }
switch (parent.type) { switch (parent.type) {

View File

@ -3,7 +3,6 @@ import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import type { StaticImport } from 'mlly' import type { StaticImport } from 'mlly'
import { findExports, findStaticImports, parseStaticImport } from 'mlly' import { findExports, findStaticImports, parseStaticImport } from 'mlly'
import { walk } from 'estree-walker'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { isAbsolute } from 'pathe' import { isAbsolute } from 'pathe'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
@ -12,7 +11,9 @@ import {
ScopeTracker, ScopeTracker,
type ScopeTrackerNode, type ScopeTrackerNode,
getUndeclaredIdentifiersInFunction, getUndeclaredIdentifiersInFunction,
isNotReferencePosition,
parseAndWalk, parseAndWalk,
walk,
withLocations, withLocations,
} from '../../core/utils/parse' } from '../../core/utils/parse'
@ -147,12 +148,26 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
declarationNodes.push(node) 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)) { if (isStaticIdentifier(name)) {
addImport(name) addImport(name)
} else { } else {
const declaration = scopeTracker.getDeclaration(name) 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) processDeclaration(declaration)
} }
} }
@ -160,32 +175,35 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
const scopeTracker = new ScopeTracker() const scopeTracker = new ScopeTracker()
function processDeclaration (node: ScopeTrackerNode | null) { function processDeclaration (scopeTrackerNode: ScopeTrackerNode | null) {
if (node?.type === 'Variable') { if (scopeTrackerNode?.type === 'Variable') {
addDeclaration(node) addDeclaration(scopeTrackerNode)
for (const decl of node.variableNode.declarations) { for (const decl of scopeTrackerNode.variableNode.declarations) {
if (!decl.init) { continue } if (!decl.init) { continue }
walk(decl.init, { walk(decl.init, {
enter: (node) => { enter: (node, parent) => {
if (node.type === 'AwaitExpression') { if (node.type === 'AwaitExpression') {
logger.error(`[nuxt] Await expressions are not supported in definePageMeta. File: '${id}'`) logger.error(`[nuxt] Await expressions are not supported in definePageMeta. File: '${id}'`)
throw new Error('await in definePageMeta') 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 // arrow functions are going to be assigned to a variable
if (node.node.type === 'ArrowFunctionExpression') { return } if (scopeTrackerNode.node.type === 'ArrowFunctionExpression') { return }
const name = node.node.id?.name const name = scopeTrackerNode.node.id?.name
if (!name) { return } if (!name) { return }
addDeclaration(node) addDeclaration(scopeTrackerNode)
const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(node.node) const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(scopeTrackerNode.node)
for (const name of undeclaredIdentifiers) { for (const name of undeclaredIdentifiers) {
addImportOrDeclaration(name) addImportOrDeclaration(name)
} }
@ -203,8 +221,11 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!meta) { return } if (!meta) { return }
walk(meta, { walk(meta, {
enter (node) { enter (node, parent) {
if (node.type !== 'Identifier') { return } if (
isNotReferencePosition(node, parent)
|| node.type !== 'Identifier' // checking for `node.type` to narrow down the type
) { return }
if (isStaticIdentifier(node.name)) { if (isStaticIdentifier(node.name)) {
addImport(node.name) addImport(node.name)

View File

@ -467,4 +467,81 @@ definePageMeta({
expect(wasErrorThrown).toBe(true) expect(wasErrorThrown).toBe(true)
}) })
it('should only add definitions for reference identifiers', () => {
const sfc = `
<script setup lang="ts">
const foo = 'foo'
const bar = { bar: 'bar' }.bar, baz = { baz: 'baz' }.baz, x = { foo }
const test = 'test'
const prop = 'prop'
const num = 1
const val = 'val'
const useVal = () => ({ val: 'val' })
function recursive () {
recursive()
}
definePageMeta({
middleware: [
() => {
console.log(bar, baz)
recursive()
const val = useVal().val
const obj = {
num,
prop: 'prop',
}
const c = class test {
prop = 'prop'
test () {}
}
},
],
})
</script>
`
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"
`)
})
}) })