From 6f43ce3bfbc62c1cccf65f88942904107474f3ad Mon Sep 17 00:00:00 2001 From: Ben Worline Date: Tue, 23 May 2023 15:29:25 -0700 Subject: [PATCH] checkpoint --- .../HarmonyImportDependencyParserPlugin.js | 39 +++++------- .../HarmonyImportSpecifierDependency.js | 59 ++++++++++++++++--- lib/javascript/BasicEvaluatedExpression.js | 12 +++- lib/javascript/JavascriptParser.js | 56 +++++++++++++----- .../re-export-namespace/index.js | 48 +++++++++++---- .../re-export-namespace/module1.js | 2 + .../re-export-namespace/module2.js | 2 +- .../re-export-namespace/module3.js | 2 + types.d.ts | 10 +++- 9 files changed, 168 insertions(+), 62 deletions(-) create mode 100644 test/configCases/code-generation/re-export-namespace/module3.js diff --git a/lib/dependencies/HarmonyImportDependencyParserPlugin.js b/lib/dependencies/HarmonyImportDependencyParserPlugin.js index a91fe368b..fb7eadc9d 100644 --- a/lib/dependencies/HarmonyImportDependencyParserPlugin.js +++ b/lib/dependencies/HarmonyImportDependencyParserPlugin.js @@ -195,7 +195,8 @@ module.exports = class HarmonyImportDependencyParserPlugin { settings.name, expr.range, exportPresenceMode, - settings.assertions + settings.assertions, + [] ); dep.referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); @@ -211,19 +212,10 @@ module.exports = class HarmonyImportDependencyParserPlugin { .for(harmonySpecifierTag) .tap( "HarmonyImportDependencyParserPlugin", - (expression, members, membersOptionals) => { + (expression, members, membersOptionals, memberRangeStarts) => { const settings = /** @type {HarmonySettings} */ ( parser.currentTagData ); - - // hack to get test to pass - if ( - JSON.stringify(members.slice(0, 2)) === - JSON.stringify(["m1", "obj1"]) - ) { - membersOptionals[2] = true; - } - const nonOptionalMembers = getNonOptionalPart( members, membersOptionals @@ -236,6 +228,7 @@ module.exports = class HarmonyImportDependencyParserPlugin { ) : expression; const ids = settings.ids.concat(nonOptionalMembers); + const startsWithNamespace = settings.ids.length === 0; const dep = new HarmonyImportSpecifierDependency( settings.source, settings.sourceOrder, @@ -243,7 +236,11 @@ module.exports = class HarmonyImportDependencyParserPlugin { settings.name, expr.range, exportPresenceMode, - settings.assertions + settings.assertions, + memberRangeStarts.slice( + startsWithNamespace ? 1 : 0, // skip one if starting with an explicit namespace + nonOptionalMembers.length + ) ); dep.referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); @@ -258,20 +255,11 @@ module.exports = class HarmonyImportDependencyParserPlugin { .for(harmonySpecifierTag) .tap( "HarmonyImportDependencyParserPlugin", - (expression, members, membersOptionals) => { + (expression, members, membersOptionals, memberRangeStarts) => { const { arguments: args, callee } = expression; const settings = /** @type {HarmonySettings} */ ( parser.currentTagData ); - - // hack to get test to pass - if ( - JSON.stringify(members.slice(0, 2)) === - JSON.stringify(["m1", "obj1"]) - ) { - membersOptionals[2] = true; - } - const nonOptionalMembers = getNonOptionalPart( members, membersOptionals @@ -284,6 +272,7 @@ module.exports = class HarmonyImportDependencyParserPlugin { ) : callee; const ids = settings.ids.concat(nonOptionalMembers); + const startsWithNamespace = settings.ids.length === 0; const dep = new HarmonyImportSpecifierDependency( settings.source, settings.sourceOrder, @@ -291,7 +280,11 @@ module.exports = class HarmonyImportDependencyParserPlugin { settings.name, expr.range, exportPresenceMode, - settings.assertions + settings.assertions, + memberRangeStarts.slice( + startsWithNamespace ? 1 : 0, // skip one if starting with an explicit namespace + nonOptionalMembers.length + ) ); dep.directImport = members.length === 0; dep.call = true; diff --git a/lib/dependencies/HarmonyImportSpecifierDependency.js b/lib/dependencies/HarmonyImportSpecifierDependency.js index 3b7a1d15f..3b8bb61d9 100644 --- a/lib/dependencies/HarmonyImportSpecifierDependency.js +++ b/lib/dependencies/HarmonyImportSpecifierDependency.js @@ -40,12 +40,14 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { name, range, exportPresenceMode, - assertions + assertions, + idRangeStarts ) { super(request, sourceOrder, assertions); this.ids = ids; this.name = name; this.range = range; + this.idRangeStarts = idRangeStarts; this.exportPresenceMode = exportPresenceMode; this.namespaceObjectAsContext = false; this.call = undefined; @@ -247,6 +249,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { write(this.ids); write(this.name); write(this.range); + write(this.idRangeStarts); write(this.exportPresenceMode); write(this.namespaceObjectAsContext); write(this.call); @@ -266,6 +269,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { this.ids = read(); this.name = read(); this.range = read(); + this.idRangeStarts = read(); this.exportPresenceMode = read(); this.namespaceObjectAsContext = read(); this.call = read(); @@ -299,14 +303,53 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen // Skip rendering depending when dependency is conditional if (connection && !connection.isTargetActive(runtime)) return; - const ids = dep.getIds(moduleGraph); - const exportExpr = this._getCodeForIds(dep, source, templateContext, ids); - const range = dep.range; - if (dep.shorthand) { - source.insert(range[1], `: ${exportExpr}`); - } else { - source.replace(range[0], range[1] - 1, exportExpr); + const ids = dep.getIds(moduleGraph); // determine minimal set of IDs. + const trimmedIds = this._trimIdsToThoseImported(ids, moduleGraph, dep); + const exportExpr = this._getCodeForIds( + dep, + source, + templateContext, + trimmedIds + ); + + let [rangeStart, rangeEnd] = dep.range; + if (trimmedIds.length !== ids.length) { + rangeEnd = dep.idRangeStarts.at(trimmedIds.length - ids.length); } + + if (dep.shorthand) { + source.insert(rangeEnd, `: ${exportExpr}`); + } else { + source.replace(rangeStart, rangeEnd - 1, exportExpr); + } + } + + /** + * @summary Determine which IDs in the id chain are actually referring to namespaces or imports, + * and which are deeper member accessors on the imported object. Only the former should be re-rendered. + * @param {string[]} ids ids + * @param {ModuleGraph} moduleGraph moduleGraph + * @param {HarmonyImportSpecifierDependency} dependency dependency + * @returns {string[]} generated code + */ + _trimIdsToThoseImported(ids, moduleGraph, dependency) { + const exportsInfo = moduleGraph.getExportsInfo( + moduleGraph.getModule(dependency) + ); + let currentExportsInfo = /** @type {ExportsInfo=} */ exportsInfo; + for (let i = 0; i < ids.length; i++) { + if (i === 0 && ids[i] === "default") { + continue; // ExportInfo for the next level under default is still at the root ExportsInfo, so don't advance currentExportsInfo + } + const exportInfo = currentExportsInfo.getExportInfo(ids[i]); + const nestedInfo = exportInfo.getNestedExportsInfo(); + if (!nestedInfo) { + // once all nested exports are traversed, the next item is the actual import so stop there + return ids.slice(0, i + 1); + } + currentExportsInfo = nestedInfo; + } + return ids; } /** diff --git a/lib/javascript/BasicEvaluatedExpression.js b/lib/javascript/BasicEvaluatedExpression.js index 2a5258dae..4c0bba660 100644 --- a/lib/javascript/BasicEvaluatedExpression.js +++ b/lib/javascript/BasicEvaluatedExpression.js @@ -70,6 +70,8 @@ class BasicEvaluatedExpression { this.getMembers = undefined; /** @type {() => boolean[]} */ this.getMembersOptionals = undefined; + /** @type {() => number[]} */ + this.getMemberRangeStarts = undefined; /** @type {EsTreeNode} */ this.expression = undefined; } @@ -384,14 +386,22 @@ class BasicEvaluatedExpression { * @param {string | VariableInfoInterface} rootInfo root info * @param {() => string[]} getMembers members * @param {() => boolean[]=} getMembersOptionals optional members + * @param {() => number[]=} getMemberRangeStarts range start of progressively increasing sub-expressions * @returns {this} this */ - setIdentifier(identifier, rootInfo, getMembers, getMembersOptionals) { + setIdentifier( + identifier, + rootInfo, + getMembers, + getMembersOptionals, + getMemberRangeStarts + ) { this.type = TypeIdentifier; this.identifier = identifier; this.rootInfo = rootInfo; this.getMembers = getMembers; this.getMembersOptionals = getMembersOptionals; + this.getMemberRangeStarts = getMemberRangeStarts; this.sideEffects = true; return this; } diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index d02cb75d4..c504510dd 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -57,7 +57,7 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); /** @typedef {import("../Parser").ParserState} ParserState */ /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */ /** @typedef {{declaredScope: ScopeInfo, freeName: string | true, tagInfo: TagInfo | undefined}} VariableInfoInterface */ -/** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => string[], getMembersOptionals: () => boolean[] }} GetInfoResult */ +/** @typedef {{ name: string | VariableInfo, rootInfo: string | VariableInfo, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRangeStarts: () => number[] }} GetInfoResult */ const EMPTY_ARRAY = []; const ALLOWED_MEMBER_TYPES_CALL_EXPRESSION = 0b01; @@ -314,9 +314,15 @@ class JavascriptParser extends Parser { /** @type {HookMap>} */ call: new HookMap(() => new SyncBailHook(["expression"])), /** Something like "a.b()" */ - /** @type {HookMap>} */ + /** @type {HookMap>} */ callMemberChain: new HookMap( - () => new SyncBailHook(["expression", "members", "membersOptionals"]) + () => + new SyncBailHook([ + "expression", + "members", + "membersOptionals", + "memberRangeStarts" + ]) ), /** Something like "a.b().c.d" */ /** @type {HookMap>} */ @@ -348,9 +354,15 @@ class JavascriptParser extends Parser { binaryExpression: new SyncBailHook(["binaryExpression"]), /** @type {HookMap>} */ expression: new HookMap(() => new SyncBailHook(["expression"])), - /** @type {HookMap>} */ + /** @type {HookMap>} */ expressionMemberChain: new HookMap( - () => new SyncBailHook(["expression", "members", "membersOptionals"]) + () => + new SyncBailHook([ + "expression", + "members", + "membersOptionals", + "memberRangeStarts" + ]) ), /** @type {HookMap>} */ unhandledExpressionMemberChain: new HookMap( @@ -1113,7 +1125,8 @@ class JavascriptParser extends Parser { info.name, info.rootInfo, info.getMembers, - info.getMembersOptionals + info.getMembersOptionals, + info.getMemberRangeStarts ) .setRange(expr.range); } @@ -1135,7 +1148,8 @@ class JavascriptParser extends Parser { name: info, rootInfo: info, getMembers: () => [], - getMembersOptionals: () => [] + getMembersOptionals: () => [], + getMemberRangeStarts: () => [] }; } }); @@ -1149,7 +1163,8 @@ class JavascriptParser extends Parser { name: info, rootInfo: info, getMembers: () => [], - getMembersOptionals: () => [] + getMembersOptionals: () => [], + getMemberRangeStarts: () => [] }; } }); @@ -2981,7 +2996,8 @@ class JavascriptParser extends Parser { callee.getMembers(), callee.getMembersOptionals ? callee.getMembersOptionals() - : callee.getMembers().map(() => false) + : callee.getMembers().map(() => false), + callee.getMemberRangeStarts ? callee.getMemberRangeStarts() : [] ); if (result1 === true) return; const result2 = this.callHooksForInfo( @@ -3022,12 +3038,14 @@ class JavascriptParser extends Parser { if (result1 === true) return; const members = exprInfo.getMembers(); const membersOptionals = exprInfo.getMembersOptionals(); + const memberRangeStarts = exprInfo.getMemberRangeStarts(); const result2 = this.callHooksForInfo( this.hooks.expressionMemberChain, exprInfo.rootInfo, expression, members, - membersOptionals + membersOptionals, + memberRangeStarts ); if (result2 === true) return; this.walkMemberExpressionWithExpressionName( @@ -3883,20 +3901,23 @@ class JavascriptParser extends Parser { /** * @param {MemberExpressionNode} expression a member expression - * @returns {{ members: string[], object: ExpressionNode | SuperNode, membersOptionals: boolean[] }} member names (reverse order) and remaining object + * @returns {{ members: string[], object: ExpressionNode | SuperNode, membersOptionals: boolean[], memberRangeStarts: number[] }} member names (reverse order) and remaining object */ extractMemberExpressionChain(expression) { /** @type {AnyNode} */ let expr = expression; const members = []; const membersOptionals = []; + const memberRangeStarts = []; while (expr.type === "MemberExpression") { if (expr.computed) { if (expr.property.type !== "Literal") break; members.push(`${expr.property.value}`); + memberRangeStarts.push(expr.object.range[1]); } else { if (expr.property.type !== "Identifier") break; members.push(expr.property.name); + memberRangeStarts.push(expr.object.range[1]); } membersOptionals.push(expr.optional); expr = expr.object; @@ -3905,6 +3926,7 @@ class JavascriptParser extends Parser { return { members, membersOptionals, + memberRangeStarts, object: expr }; } @@ -3927,8 +3949,8 @@ class JavascriptParser extends Parser { return { info, name }; } - /** @typedef {{ type: "call", call: CallExpressionNode, calleeName: string, rootInfo: string | VariableInfo, getCalleeMembers: () => string[], name: string, getMembers: () => string[], getMembersOptionals: () => boolean[]}} CallExpressionInfo */ - /** @typedef {{ type: "expression", rootInfo: string | VariableInfo, name: string, getMembers: () => string[], getMembersOptionals: () => boolean[]}} ExpressionExpressionInfo */ + /** @typedef {{ type: "call", call: CallExpressionNode, calleeName: string, rootInfo: string | VariableInfo, getCalleeMembers: () => string[], name: string, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRangeStarts: () => number[]}} CallExpressionInfo */ + /** @typedef {{ type: "expression", rootInfo: string | VariableInfo, name: string, getMembers: () => string[], getMembersOptionals: () => boolean[], getMemberRangeStarts: () => number[]}} ExpressionExpressionInfo */ /** * @param {MemberExpressionNode} expression a member expression @@ -3936,7 +3958,7 @@ class JavascriptParser extends Parser { * @returns {CallExpressionInfo | ExpressionExpressionInfo | undefined} expression info */ getMemberExpressionInfo(expression, allowedTypes) { - const { object, members, membersOptionals } = + const { object, members, membersOptionals, memberRangeStarts } = this.extractMemberExpressionChain(expression); switch (object.type) { case "CallExpression": { @@ -3962,7 +3984,8 @@ class JavascriptParser extends Parser { getCalleeMembers: memoize(() => rootMembers.reverse()), name: objectAndMembersToName(`${calleeName}()`, members), getMembers: memoize(() => members.reverse()), - getMembersOptionals: memoize(() => membersOptionals.reverse()) + getMembersOptionals: memoize(() => membersOptionals.reverse()), + getMemberRangeStarts: memoize(() => memberRangeStarts.reverse()) }; } case "Identifier": @@ -3981,7 +4004,8 @@ class JavascriptParser extends Parser { name: objectAndMembersToName(resolvedRoot, members), rootInfo, getMembers: memoize(() => members.reverse()), - getMembersOptionals: memoize(() => membersOptionals.reverse()) + getMembersOptionals: memoize(() => membersOptionals.reverse()), + getMemberRangeStarts: memoize(() => memberRangeStarts.reverse()) }; } } diff --git a/test/configCases/code-generation/re-export-namespace/index.js b/test/configCases/code-generation/re-export-namespace/index.js index eaff51b8d..e5928f19b 100644 --- a/test/configCases/code-generation/re-export-namespace/index.js +++ b/test/configCases/code-generation/re-export-namespace/index.js @@ -1,4 +1,8 @@ -import * as m2 from './module2'; +import def from "./module1"; +import { obj1 } from './module1'; +import * as m_1 from './module1'; +import * as m_2 from './module2'; +import * as m_3 from './module3'; const { expectSourceToContain } = require("../../../helpers/expectSource"); @@ -9,14 +13,29 @@ it("should use/preserve accessor form for import object and namespaces", functio var fs = require("fs"); var source = fs.readFileSync(__filename, "utf-8").toString(); - // Reference the import to generate uses in the source. + // Reference the imports to generate uses in the source. + + def.obj2.unknownProperty = { deep: "trench" }; + expect(def.obj2.unknownProperty.deep).toBe("trench"); + expect(def).toHaveProperty('obj2'); + expect(def.obj2).toHaveProperty('unknownProperty'); const f = false; if (f) { - const a = m2["m1"]["obj1"]["flip"].flap; - const b = m2["m1"]["obj1"].zip["zap"]; - const c = m2["m1"]["obj1"]["ding"].dong(); - const d = m2["m1"]["obj1"].sing["song"](); + const x1 = m_1; + const x2 = obj1; + + const z1 = obj1["plants"]; + const z2 = obj1["funcs"](); + const z3 = m_1["obj1"]["pots"]; + const z4 = m_1["obj1"]["subs"](); + + const a = m_2["m_1"].obj1["flip"].flap; + const b = m_2["m_1"]["obj1"].zip["zap"]; + const c = m_2.m_1.obj1["ding"].dong(); + const d = m_2.m_1["obj1"].sing["song"](); + + const aa = m_3["m_2"].m_1["obj1"]["zoom"]; } /************ DO NOT MATCH BELOW THIS LINE ************/ @@ -24,9 +43,18 @@ it("should use/preserve accessor form for import object and namespaces", functio // Imported objects and import namespaces should use dot notation. Any references to the properties of exports // should be preserved as either quotes or dot notation, depending on the original source. - expectSourceToContain(source, 'const a = _module2__WEBPACK_IMPORTED_MODULE_0__.m1.obj1["flip"].flap;'); - expectSourceToContain(source, 'const b = _module2__WEBPACK_IMPORTED_MODULE_0__.m1.obj1.zip["zap"];'); + expectSourceToContain(source, 'const x1 = _module1__WEBPACK_IMPORTED_MODULE_0__;'); + expectSourceToContain(source, 'const x2 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1;'); - expectSourceToContain(source, 'const c = _module2__WEBPACK_IMPORTED_MODULE_0__.m1.obj1["ding"].dong();'); - expectSourceToContain(source, 'const d = _module2__WEBPACK_IMPORTED_MODULE_0__.m1.obj1.sing["song"]();'); + expectSourceToContain(source, 'const z1 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["plants"];'); + expectSourceToContain(source, 'const z2 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["funcs"]();'); + expectSourceToContain(source, 'const z3 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["pots"];'); + expectSourceToContain(source, 'const z4 = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1["subs"]();'); + + expectSourceToContain(source, 'const a = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1["flip"].flap;'); + expectSourceToContain(source, 'const b = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1.zip["zap"];'); + expectSourceToContain(source, 'const c = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1["ding"].dong();'); + expectSourceToContain(source, 'const d = _module2__WEBPACK_IMPORTED_MODULE_1__.m_1.obj1.sing["song"]();'); + + expectSourceToContain(source, 'const aa = _module3__WEBPACK_IMPORTED_MODULE_2__.m_2.m_1.obj1["zoom"];'); }); diff --git a/test/configCases/code-generation/re-export-namespace/module1.js b/test/configCases/code-generation/re-export-namespace/module1.js index c6c3f7b96..e85ec6643 100644 --- a/test/configCases/code-generation/re-export-namespace/module1.js +++ b/test/configCases/code-generation/re-export-namespace/module1.js @@ -1 +1,3 @@ export const obj1 = {}; + +export default { obj2: {} }; diff --git a/test/configCases/code-generation/re-export-namespace/module2.js b/test/configCases/code-generation/re-export-namespace/module2.js index 5ae3488b7..a91c5e7a0 100644 --- a/test/configCases/code-generation/re-export-namespace/module2.js +++ b/test/configCases/code-generation/re-export-namespace/module2.js @@ -1,2 +1,2 @@ import * as m1 from './module1'; -export { m1 }; +export { m1 as m_1 }; diff --git a/test/configCases/code-generation/re-export-namespace/module3.js b/test/configCases/code-generation/re-export-namespace/module3.js new file mode 100644 index 000000000..cf0e8cd08 --- /dev/null +++ b/test/configCases/code-generation/re-export-namespace/module3.js @@ -0,0 +1,2 @@ +import * as m2 from './module2'; +export { m2 as m_2 }; diff --git a/types.d.ts b/types.d.ts index edf023557..42af39d9a 100644 --- a/types.d.ts +++ b/types.d.ts @@ -487,6 +487,7 @@ declare abstract class BasicEvaluatedExpression { rootInfo: string | VariableInfoInterface; getMembers: () => string[]; getMembersOptionals: () => boolean[]; + getMemberRangeStarts: () => number[]; expression: NodeEstreeIndex; isUnknown(): boolean; isNull(): boolean; @@ -571,7 +572,8 @@ declare abstract class BasicEvaluatedExpression { identifier: string | VariableInfoInterface, rootInfo: string | VariableInfoInterface, getMembers: () => string[], - getMembersOptionals?: () => boolean[] + getMembersOptionals?: () => boolean[], + getMemberRangeStarts?: () => number[] ): BasicEvaluatedExpression; /** @@ -766,6 +768,7 @@ declare interface CallExpressionInfo { name: string; getMembers: () => string[]; getMembersOptionals: () => boolean[]; + getMemberRangeStarts: () => number[]; } declare interface CallbackAsyncQueue { (err?: null | WebpackError, result?: T): any; @@ -3945,6 +3948,7 @@ declare interface ExpressionExpressionInfo { name: string; getMembers: () => string[]; getMembersOptionals: () => boolean[]; + getMemberRangeStarts: () => number[]; } declare interface ExtensionAliasOption { alias: string | string[]; @@ -5284,7 +5288,7 @@ declare class JavascriptParser extends Parser { topLevelAwait: SyncBailHook<[Expression], boolean | void>; call: HookMap>; callMemberChain: HookMap< - SyncBailHook<[CallExpression, string[], boolean[]], boolean | void> + SyncBailHook<[CallExpression, string[], boolean[], number[]], boolean | void> >; memberChainOfCallMemberChain: HookMap< SyncBailHook< @@ -5303,7 +5307,7 @@ declare class JavascriptParser extends Parser { binaryExpression: SyncBailHook<[BinaryExpression], boolean | void>; expression: HookMap>; expressionMemberChain: HookMap< - SyncBailHook<[Expression, string[], boolean[]], boolean | void> + SyncBailHook<[Expression, string[], boolean[], number[]], boolean | void> >; unhandledExpressionMemberChain: HookMap< SyncBailHook<[Expression, string[]], boolean | void>