From cbfba9a150bde770c35639ba2ab39c5306b4ded2 Mon Sep 17 00:00:00 2001 From: Gengkun Date: Thu, 14 Aug 2025 02:36:56 +0800 Subject: [PATCH] fix: distinguish free variable and tagged variable (#19795) --- lib/JavascriptMetaInfoPlugin.js | 3 +- lib/javascript/JavascriptParser.js | 106 ++++++++++++++---- lib/optimize/InnerGraph.js | 9 +- .../parsing/top-level-declarations/a.js | 1 + .../parsing/top-level-declarations/b.js | 1 + .../parsing/top-level-declarations/c.js | 1 + .../parsing/top-level-declarations/d.js | 1 + .../parsing/top-level-declarations/index.js | 15 +++ .../top-level-declarations/test.config.js | 23 ++++ .../top-level-declarations/test.filter.js | 5 + .../top-level-declarations/webpack.config.js | 44 ++++++++ types.d.ts | 25 ++++- 12 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 test/configCases/parsing/top-level-declarations/a.js create mode 100644 test/configCases/parsing/top-level-declarations/b.js create mode 100644 test/configCases/parsing/top-level-declarations/c.js create mode 100644 test/configCases/parsing/top-level-declarations/d.js create mode 100644 test/configCases/parsing/top-level-declarations/index.js create mode 100644 test/configCases/parsing/top-level-declarations/test.config.js create mode 100644 test/configCases/parsing/top-level-declarations/test.filter.js create mode 100644 test/configCases/parsing/top-level-declarations/webpack.config.js diff --git a/lib/JavascriptMetaInfoPlugin.js b/lib/JavascriptMetaInfoPlugin.js index e3d9ffa8b..606dca5e3 100644 --- a/lib/JavascriptMetaInfoPlugin.js +++ b/lib/JavascriptMetaInfoPlugin.js @@ -54,8 +54,7 @@ class JavascriptMetaInfoPlugin { topLevelDeclarations = buildInfo.topLevelDeclarations = new Set(); } for (const name of parser.scope.definitions.asSet()) { - const freeInfo = parser.getFreeInfoFromVariable(name); - if (freeInfo === undefined) { + if (parser.isVariableDefined(name)) { topLevelDeclarations.add(name); } } diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index 23a8cad13..2c9874a3e 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -258,17 +258,42 @@ const getImportAttributes = (node) => { return result; }; +/** @typedef {typeof VariableInfoFlags.Evaluated | typeof VariableInfoFlags.Free | typeof VariableInfoFlags.Normal | typeof VariableInfoFlags.Tagged} VariableInfoFlagsType */ + +const VariableInfoFlags = Object.freeze({ + Evaluated: 0b000, + Free: 0b001, + Normal: 0b010, + Tagged: 0b100 +}); + class VariableInfo { /** * @param {ScopeInfo} declaredScope scope in which the variable is declared - * @param {string | true | undefined} freeName which free name the variable aliases, or true when none + * @param {string | undefined} name which name the variable use, defined name or free name or tagged name + * @param {VariableInfoFlagsType} flags how the variable is created * @param {TagInfo | undefined} tagInfo info about tags */ - constructor(declaredScope, freeName, tagInfo) { + constructor(declaredScope, name, flags, tagInfo) { this.declaredScope = declaredScope; - this.freeName = freeName; + this.name = name; + this.flags = flags; this.tagInfo = tagInfo; } + + /** + * @returns {boolean} the variable is free or not + */ + isFree() { + return (this.flags & VariableInfoFlags.Free) > 0; + } + + /** + * @returns {boolean} the variable is tagged by tagVariable or not + */ + isTagged() { + return (this.flags & VariableInfoFlags.Tagged) > 0; + } } /** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */ @@ -1426,7 +1451,7 @@ class JavascriptParser extends Parser { const info = this.getVariableInfo(/** @type {Identifier} */ (expr).name); if ( typeof info === "string" || - (info instanceof VariableInfo && typeof info.freeName === "string") + (info instanceof VariableInfo && (info.isFree() || info.isTagged())) ) { return { name: info, @@ -1441,7 +1466,7 @@ class JavascriptParser extends Parser { const info = this.getVariableInfo("this"); if ( typeof info === "string" || - (info instanceof VariableInfo && typeof info.freeName === "string") + (info instanceof VariableInfo && (info.isFree() || info.isTagged())) ) { return { name: info, @@ -4053,13 +4078,13 @@ class JavascriptParser extends Parser { } tagInfo = tagInfo.next; } - if (info.freeName === true) { + if (!info.isFree() && !info.isTagged()) { if (defined !== undefined) { return defined(); } return; } - name = info.freeName; + name = info.name; } const hook = hookMap.get(name); if (hook !== undefined) { @@ -4818,25 +4843,31 @@ class JavascriptParser extends Parser { * @param {string} name name * @param {Tag} tag tag info * @param {TagData=} data data + * @param {VariableInfoFlagsType=} flags flags */ - tagVariable(name, tag, data) { + tagVariable(name, tag, data, flags = VariableInfoFlags.Tagged) { const oldInfo = this.scope.definitions.get(name); /** @type {VariableInfo} */ let newInfo; if (oldInfo === undefined) { - newInfo = new VariableInfo(this.scope, name, { + newInfo = new VariableInfo(this.scope, name, flags, { tag, data, next: undefined }); } else if (oldInfo instanceof VariableInfo) { - newInfo = new VariableInfo(oldInfo.declaredScope, oldInfo.freeName, { - tag, - data, - next: oldInfo.tagInfo - }); + newInfo = new VariableInfo( + oldInfo.declaredScope, + oldInfo.name, + /** @type {VariableInfoFlagsType} */ (oldInfo.flags | flags), + { + tag, + data, + next: oldInfo.tagInfo + } + ); } else { - newInfo = new VariableInfo(oldInfo, true, { + newInfo = new VariableInfo(oldInfo, name, flags, { tag, data, next: undefined @@ -4875,7 +4906,7 @@ class JavascriptParser extends Parser { const info = this.scope.definitions.get(name); if (info === undefined) return false; if (info instanceof VariableInfo) { - return info.freeName === true; + return !info.isFree(); } return true; } @@ -4904,7 +4935,12 @@ class JavascriptParser extends Parser { } else { this.scope.definitions.set( name, - new VariableInfo(this.scope, variableInfo, undefined) + new VariableInfo( + this.scope, + variableInfo, + VariableInfoFlags.Free, + undefined + ) ); } } else { @@ -4917,7 +4953,12 @@ class JavascriptParser extends Parser { * @returns {VariableInfo} variable info */ evaluatedVariable(tagInfo) { - return new VariableInfo(this.scope, undefined, tagInfo); + return new VariableInfo( + this.scope, + undefined, + VariableInfoFlags.Evaluated, + tagInfo + ); } /** @@ -5002,9 +5043,27 @@ class JavascriptParser extends Parser { getFreeInfoFromVariable(varName) { const info = this.getVariableInfo(varName); let name; - if (info instanceof VariableInfo) { - name = info.freeName; - if (typeof name !== "string") return; + if (info instanceof VariableInfo && info.name) { + if (!info.isFree()) return; + name = info.name; + } else if (typeof info !== "string") { + return; + } else { + name = info; + } + return { info, name }; + } + + /** + * @param {string} varName variable name + * @returns {{name: string, info: VariableInfo | string} | undefined} name of the free variable and variable info for that + */ + getNameInfoFromVariable(varName) { + const info = this.getVariableInfo(varName); + let name; + if (info instanceof VariableInfo && info.name) { + if (!info.isFree() && !info.isTagged()) return; + name = info.name; } else if (typeof info !== "string") { return; } else { @@ -5035,7 +5094,7 @@ class JavascriptParser extends Parser { } const rootName = getRootName(callee); if (!rootName) return; - const result = this.getFreeInfoFromVariable(rootName); + const result = this.getNameInfoFromVariable(rootName); if (!result) return; const { info: rootInfo, name: resolvedRoot } = result; const calleeName = objectAndMembersToName(resolvedRoot, rootMembers); @@ -5058,7 +5117,7 @@ class JavascriptParser extends Parser { const rootName = getRootName(object); if (!rootName) return; - const result = this.getFreeInfoFromVariable(rootName); + const result = this.getNameInfoFromVariable(rootName); if (!result) return; const { info: rootInfo, name: resolvedRoot } = result; return { @@ -5151,4 +5210,5 @@ module.exports.ALLOWED_MEMBER_TYPES_CALL_EXPRESSION = module.exports.ALLOWED_MEMBER_TYPES_EXPRESSION = ALLOWED_MEMBER_TYPES_EXPRESSION; module.exports.VariableInfo = VariableInfo; +module.exports.VariableInfoFlags = VariableInfoFlags; module.exports.getImportAttributes = getImportAttributes; diff --git a/lib/optimize/InnerGraph.js b/lib/optimize/InnerGraph.js index 78350f746..ee37d3dc8 100644 --- a/lib/optimize/InnerGraph.js +++ b/lib/optimize/InnerGraph.js @@ -6,6 +6,7 @@ "use strict"; const { UsageState } = require("../ExportsInfo"); +const JavascriptParser = require("../javascript/JavascriptParser"); /** @typedef {import("estree").Node} AnyNode */ /** @typedef {import("../Dependency")} Dependency */ @@ -15,7 +16,6 @@ const { UsageState } = require("../ExportsInfo"); /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */ /** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */ /** @typedef {import("../Parser").ParserState} ParserState */ -/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ /** @typedef {Map | true | undefined>} InnerGraph */ @@ -348,7 +348,12 @@ module.exports.tagTopLevelSymbol = (parser, name) => { } const fn = new TopLevelSymbol(name); - parser.tagVariable(name, topLevelSymbolTag, fn); + parser.tagVariable( + name, + topLevelSymbolTag, + fn, + JavascriptParser.VariableInfoFlags.Normal + ); return fn; }; diff --git a/test/configCases/parsing/top-level-declarations/a.js b/test/configCases/parsing/top-level-declarations/a.js new file mode 100644 index 000000000..cc798ff50 --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/a.js @@ -0,0 +1 @@ +export const a = 1; diff --git a/test/configCases/parsing/top-level-declarations/b.js b/test/configCases/parsing/top-level-declarations/b.js new file mode 100644 index 000000000..a8f13bb4f --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/b.js @@ -0,0 +1 @@ +exports.b = 2; diff --git a/test/configCases/parsing/top-level-declarations/c.js b/test/configCases/parsing/top-level-declarations/c.js new file mode 100644 index 000000000..5f0cabef8 --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/c.js @@ -0,0 +1 @@ +export const c = 3; diff --git a/test/configCases/parsing/top-level-declarations/d.js b/test/configCases/parsing/top-level-declarations/d.js new file mode 100644 index 000000000..88dcd403a --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/d.js @@ -0,0 +1 @@ +export const d = 4; diff --git a/test/configCases/parsing/top-level-declarations/index.js b/test/configCases/parsing/top-level-declarations/index.js new file mode 100644 index 000000000..a445240db --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/index.js @@ -0,0 +1,15 @@ +import { a } from "./a"; +import { createRequire } from "module"; + +const myRequire = createRequire(import.meta.url); +const { b } = myRequire("./b"); +const c = new URL("./c.js", import.meta.url); +const audioContext = new AudioContext(); +const d = audioContext.audioWorklet.addModule(new URL("./d.js", import.meta.url)); + +it("should have correct top level declarations", async () => { + await d; + expect(a).toBe(1); + expect(b).toBe(2); + expect(c.pathname.endsWith(".js")).toBe(true); +}) \ No newline at end of file diff --git a/test/configCases/parsing/top-level-declarations/test.config.js b/test/configCases/parsing/top-level-declarations/test.config.js new file mode 100644 index 000000000..3f5997f5d --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/test.config.js @@ -0,0 +1,23 @@ +"use strict"; + +let outputDirectory; + +module.exports = { + moduleScope(scope) { + const FakeWorker = require("../../../helpers/createFakeWorker")({ + outputDirectory + }); + + scope.AudioContext = class AudioContext { + constructor() { + this.audioWorklet = { + addModule: (url) => Promise.resolve(FakeWorker.bind(null, url)) + }; + } + }; + }, + findBundle(i, options) { + outputDirectory = options.output.path; + return ["main.js"]; + } +}; diff --git a/test/configCases/parsing/top-level-declarations/test.filter.js b/test/configCases/parsing/top-level-declarations/test.filter.js new file mode 100644 index 000000000..fc9b5e2ce --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/test.filter.js @@ -0,0 +1,5 @@ +"use strict"; + +const supportsWorker = require("../../../helpers/supportsWorker"); + +module.exports = () => supportsWorker(); diff --git a/test/configCases/parsing/top-level-declarations/webpack.config.js b/test/configCases/parsing/top-level-declarations/webpack.config.js new file mode 100644 index 000000000..4323879f8 --- /dev/null +++ b/test/configCases/parsing/top-level-declarations/webpack.config.js @@ -0,0 +1,44 @@ +"use strict"; + +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + output: { + filename: "[name].js" + }, + module: { + parser: { + javascript: { + createRequire: true, + worker: ["*audioContext.audioWorklet.addModule()"] + } + } + }, + plugins: [ + function testPlugin(compiler) { + compiler.hooks.finishMake.tap("test", (compilation) => { + for (const module of compilation.modules) { + const name = module.nameForCondition(); + const topLevelDeclarations = + module.buildInfo && module.buildInfo.topLevelDeclarations; + if ( + name && + name.includes("top-level-declarations/index.js") && + topLevelDeclarations + ) { + const expectedTopLevelDeclarations = new Set([ + "a", + "createRequire", + "myRequire", + "b", + "c", + "audioContext", + "d" + ]); + expect(topLevelDeclarations).toEqual(expectedTopLevelDeclarations); + } + } + }); + } + ] +}; diff --git a/types.d.ts b/types.d.ts index 44efdded1..d4cd08ae1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -7716,7 +7716,12 @@ declare class JavascriptParser extends ParserClass { unsetAsiPosition(pos: number): void; isStatementLevelExpression(expr: Expression): boolean; getTagData(name: string, tag: symbol): undefined | TagData; - tagVariable(name: string, tag: symbol, data?: TagData): void; + tagVariable( + name: string, + tag: symbol, + data?: TagData, + flags?: 0 | 1 | 2 | 4 + ): void; defineVariable(name: string): void; undefineVariable(name: string): void; isVariableDefined(name: string): boolean; @@ -7794,6 +7799,9 @@ declare class JavascriptParser extends ParserClass { getFreeInfoFromVariable( varName: string ): undefined | { name: string; info: string | VariableInfo }; + getNameInfoFromVariable( + varName: string + ): undefined | { name: string; info: string | VariableInfo }; getMemberExpressionInfo( expression: | ImportExpressionImport @@ -7842,6 +7850,12 @@ declare class JavascriptParser extends ParserClass { static ALLOWED_MEMBER_TYPES_CALL_EXPRESSION: 1; static ALLOWED_MEMBER_TYPES_EXPRESSION: 2; static VariableInfo: typeof VariableInfo; + static VariableInfoFlags: Readonly<{ + Evaluated: 0; + Free: 1; + Normal: 2; + Tagged: 4; + }>; static getImportAttributes: ( node: | ImportDeclarationJavascriptParser @@ -16949,13 +16963,18 @@ declare interface Values { declare class VariableInfo { constructor( declaredScope: ScopeInfo, - freeName?: string | true, + name: undefined | string, + flags: VariableInfoFlagsType, tagInfo?: TagInfo ); declaredScope: ScopeInfo; - freeName?: string | true; + name?: string; + flags: VariableInfoFlagsType; tagInfo?: TagInfo; + isFree(): boolean; + isTagged(): boolean; } +type VariableInfoFlagsType = 0 | 1 | 2 | 4; declare interface VirtualModuleConfig { /** * - The module type