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
}
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 }
switch (parent.type) {

View File

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

View File

@ -467,4 +467,81 @@ definePageMeta({
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"
`)
})
})