Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
bworline committed Oct 1, 2023
1 parent 1f13ff9 commit d4e8485
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 35 deletions.
82 changes: 76 additions & 6 deletions lib/dependencies/CommonJsFullRequireDependency.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ class CommonJsFullRequireDependency extends ModuleDependency {
* @param {string} request the request string
* @param {Range} range location in source code
* @param {string[]} names accessed properties on module
* @param {Range[]=} idRanges ranges for members of ids; the two arrays are right-aligned
*/
constructor(request, range, names) {
constructor(
request,
range,
names,
idRanges /* TODO webpack 6 make this non-optional. It must always be set to properly trim ids. */
) {
super(request);
this.range = range;
this.names = names;
this.idRanges = idRanges;
this.call = false;
this.asiSafe = undefined;
}
Expand Down Expand Up @@ -60,6 +67,7 @@ class CommonJsFullRequireDependency extends ModuleDependency {
serialize(context) {
const { write } = context;
write(this.names);
write(this.idRanges);
write(this.call);
write(this.asiSafe);
super.serialize(context);
Expand All @@ -71,6 +79,7 @@ class CommonJsFullRequireDependency extends ModuleDependency {
deserialize(context) {
const { read } = context;
this.names = read();
this.idRanges = read();
this.call = read();
this.asiSafe = read();
super.deserialize(context);
Expand Down Expand Up @@ -117,23 +126,84 @@ CommonJsFullRequireDependency.Template = class CommonJsFullRequireDependencyTemp
weak: dep.weak,
runtimeRequirements
});

const ids = dep.names;
let trimmedIds = this._trimIdsToThoseImported(ids, moduleGraph, dep);

let [rangeStart, rangeEnd] = dep.range;
if (trimmedIds.length !== ids.length) {
// The array returned from dep.idRanges is right-aligned with the array returned from dep.names.
// Meaning, the two arrays may not always have the same number of elements, but the last element of
// dep.idRanges corresponds to [the expression fragment to the left of] the last element of dep.names.
// Use this to find the correct replacement range based on the number of ids that were trimmed.
const idx =
dep.idRanges === undefined
? -1 /* trigger failure case below */
: dep.idRanges.length + (trimmedIds.length - ids.length);
if (idx < 0 || idx >= dep.idRanges.length) {
// cspell:ignore minifiers
// Should not happen but we can't throw an error here because of backward compatibility with
// external plugins in wp5. Instead, we just disable trimming for now. This may break some minifiers.
trimmedIds = ids;
// TODO webpack 6 remove the "trimmedIds = ids" above and uncomment the following line instead.
// throw new Error("Missing range starts data for id replacement trimming.");
} else {
[rangeStart, rangeEnd] = dep.idRanges[idx];
}
}

if (importedModule) {
const ids = dep.names;
const usedImported = moduleGraph
.getExportsInfo(importedModule)
.getUsedName(ids, runtime);
.getUsedName(trimmedIds, runtime);
if (usedImported) {
const comment = equals(usedImported, ids)
const comment = equals(usedImported, trimmedIds)
? ""
: Template.toNormalComment(propertyAccess(ids)) + " ";
: Template.toNormalComment(propertyAccess(trimmedIds)) + " ";
const access = `${comment}${propertyAccess(usedImported)}`;
requireExpr =
dep.asiSafe === true
? `(${requireExpr}${access})`
: `${requireExpr}${access}`;
}
}
source.replace(dep.range[0], dep.range[1] - 1, requireExpr);
source.replace(rangeStart, rangeEnd - 1, requireExpr);
}

/**
* @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 {CommonJsFullRequireDependency} dependency dependency
* @returns {string[]} generated code
*/
_trimIdsToThoseImported(ids, moduleGraph, dependency) {
let trimmedIds = [];
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]);
if (exportInfo.provided === false) {
// json imports have nested ExportInfo for elements that things that are not actually exported, so check .provided
trimmedIds = ids.slice(0, i);
break;
}
const nestedInfo = exportInfo.getNestedExportsInfo();
if (!nestedInfo) {
// once all nested exports are traversed, the next item is the actual import so stop there
trimmedIds = ids.slice(0, i + 1);
break;
}
currentExportsInfo = nestedInfo;
}
// Never trim to nothing. This can happen for invalid imports (e.g. import { notThere } from "./module", or import { anything } from "./missingModule")
return trimmedIds.length ? trimmedIds : ids;
}
};

Expand Down
24 changes: 20 additions & 4 deletions lib/dependencies/CommonJsImportsParserPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,16 @@ class CommonJsImportsParserPlugin {
* @param {string[]} calleeMembers callee members
* @param {CallExpression} callExpr call expression
* @param {string[]} members members
* @param {Range[]} memberRanges member ranges
* @returns {boolean | void} true when handled
*/
const chainHandler = (expr, calleeMembers, callExpr, members) => {
const chainHandler = (
expr,
calleeMembers,
callExpr,
members,
memberRanges
) => {
if (callExpr.arguments.length !== 1) return;
const param = parser.evaluateExpression(callExpr.arguments[0]);
if (
Expand All @@ -391,7 +398,8 @@ class CommonJsImportsParserPlugin {
const dep = new CommonJsFullRequireDependency(
/** @type {string} */ (param.string),
/** @type {Range} */ (expr.range),
members
members,
/** @type {Range[]} */ memberRanges
);
dep.asiSafe = !parser.isAsiPosition(
/** @type {Range} */ (expr.range)[0]
Expand All @@ -407,9 +415,16 @@ class CommonJsImportsParserPlugin {
* @param {string[]} calleeMembers callee members
* @param {CallExpression} callExpr call expression
* @param {string[]} members members
* @param {Range[]} memberRanges member ranges
* @returns {boolean | void} true when handled
*/
const callChainHandler = (expr, calleeMembers, callExpr, members) => {
const callChainHandler = (
expr,
calleeMembers,
callExpr,
members,
memberRanges
) => {
if (callExpr.arguments.length !== 1) return;
const param = parser.evaluateExpression(callExpr.arguments[0]);
if (
Expand All @@ -419,7 +434,8 @@ class CommonJsImportsParserPlugin {
const dep = new CommonJsFullRequireDependency(
/** @type {string} */ (param.string),
/** @type {Range} */ (expr.callee.range),
members
members,
/** @type {Range[]} */ memberRanges
);
dep.call = true;
dep.asiSafe = !parser.isAsiPosition(
Expand Down
16 changes: 10 additions & 6 deletions lib/javascript/JavascriptParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,25 +363,27 @@ class JavascriptParser extends Parser {
])
),
/** Something like "a.b().c.d" */
/** @type {HookMap<SyncBailHook<[Expression, string[], CallExpression, string[]], boolean | void>>} */
/** @type {HookMap<SyncBailHook<[Expression, string[], CallExpression, string[], Range[]], boolean | void>>} */
memberChainOfCallMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"calleeMembers",
"callExpression",
"members"
"members",
"memberRanges"
])
),
/** Something like "a.b().c.d()"" */
/** @type {HookMap<SyncBailHook<[CallExpression, string[], CallExpression, string[]], boolean | void>>} */
/** @type {HookMap<SyncBailHook<[CallExpression, string[], CallExpression, string[], Range[]], boolean | void>>} */
callMemberChainOfCallMemberChain: new HookMap(
() =>
new SyncBailHook([
"expression",
"calleeMembers",
"innerCallExpression",
"members"
"members",
"memberRanges"
])
),
/** @type {SyncBailHook<[ChainExpression], boolean | void>} */
Expand Down Expand Up @@ -3274,7 +3276,8 @@ class JavascriptParser extends Parser {
expression,
exprInfo.getCalleeMembers(),
exprInfo.call,
exprInfo.getMembers()
exprInfo.getMembers(),
exprInfo.getMemberRanges()
);
if (result === true) return;
}
Expand Down Expand Up @@ -3365,7 +3368,8 @@ class JavascriptParser extends Parser {
expression,
exprInfo.getCalleeMembers(),
exprInfo.call,
exprInfo.getMembers()
exprInfo.getMembers(),
exprInfo.getMemberRanges()
);
if (result === true) return;
// Fast skip over the member chain as we already called memberChainOfCallMemberChain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ it("should use/preserve accessor form for import object and namespaces", functio

const bb = obj1.up.down?.left.right;

const ww = require('./module1').obj1["bing"]?.bang;
const xx = require('./module1').obj1["pip"].pop();
const yy = require('./module3')["m_2"]["m_1"]["obj1"]["tip"].top();

data.nested.object3["unknownProperty"].depth = "deep";

(obj1)["aaa"].bbb;
Expand All @@ -48,28 +52,32 @@ 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 x1 = module1_namespaceObject;');
expectSourceToContain(source, 'const x2 = obj1;');
expectSourceToContain(source, 'const x1 = module1;');
expectSourceToContain(source, 'const x2 = module1.obj1;');

expectSourceToContain(source, 'const z1 = module1.obj1["plants"];');
expectSourceToContain(source, 'const z2 = module1.obj1["funcs"]();');
expectSourceToContain(source, 'const z3 = module1.obj1["pots"];');
expectSourceToContain(source, 'const z4 = module1.obj1["subs"]();');

expectSourceToContain(source, 'const z1 = obj1["plants"];');
expectSourceToContain(source, 'const z2 = obj1["funcs"]();');
expectSourceToContain(source, 'const z3 = obj1["pots"];');
expectSourceToContain(source, 'const z4 = obj1["subs"]();');
expectSourceToContain(source, 'const a = module2/* m_1.obj1 */.a.obj1["flip"].flap;');
expectSourceToContain(source, 'const b = module2/* m_1.obj1 */.a.obj1.zip["zap"];');
expectSourceToContain(source, 'const c = module2/* m_1.obj1 */.a.obj1["ding"].dong();');
expectSourceToContain(source, 'const d = module2/* m_1.obj1 */.a.obj1.sing["song"]();');

expectSourceToContain(source, 'const a = obj1["flip"].flap;');
expectSourceToContain(source, 'const b = obj1.zip["zap"];');
expectSourceToContain(source, 'const c = obj1["ding"].dong();');
expectSourceToContain(source, 'const d = obj1.sing["song"]();');
expectSourceToContain(source, 'const aa = module3/* m_2.m_1.obj1 */.a.a.obj1["zoom"];');

expectSourceToContain(source, 'const aa = obj1["zoom"];');
expectSourceToContain(source, 'const bb = module1.obj1.up.down?.left.right;');

expectSourceToContain(source, 'const bb = obj1.up.down?.left.right;');
expectSourceToContain(source, 'const ww = (__webpack_require__(/*! ./module1 */ 960).obj1)["bing"]?.bang;');
expectSourceToContain(source, 'const xx = (__webpack_require__(/*! ./module1 */ 960).obj1)["pip"].pop();');
expectSourceToContain(source, 'const yy = (__webpack_require__(/*! ./module3 */ 834)/* .m_2.m_1.obj1 */ .a.a.obj1)["tip"].top();');

expectSourceToContain(source, 'data_namespaceObject.a.a["unknownProperty"].depth = "deep";');

expectSourceToContain(source, '(obj1)["aaa"].bbb;');
expectSourceToContain(source, '(obj1)["ccc"].ddd;');
expectSourceToContain(source, '(obj1["eee"]).fff;');
expectSourceToContain(source, '(obj1["ggg"]).hhh;');
expectSourceToContain(source, '((obj1)["iii"]).jjj;');
expectSourceToContain(source, '(module1.obj1)["aaa"].bbb;');
expectSourceToContain(source, '(module1.obj1)["ccc"].ddd;');
expectSourceToContain(source, '(module1.obj1["eee"]).fff;');
expectSourceToContain(source, '(module1.obj1["ggg"]).hhh;');
expectSourceToContain(source, '((module1.obj1)["iii"]).jjj;');
});
8 changes: 8 additions & 0 deletions test/configCases/code-generation/re-export-namespace/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ it("should use/preserve accessor form for import object and namespaces", functio

const bb = obj1.up.down?.left.right;

const ww = require('./module1').obj1["bing"]?.bang;
const xx = require('./module1').obj1["pip"].pop();
const yy = require('./module3')["m_2"]["m_1"]["obj1"]["tip"].top();

data.nested.object3["unknownProperty"].depth = "deep";

(obj1)["aaa"].bbb;
Expand Down Expand Up @@ -65,6 +69,10 @@ it("should use/preserve accessor form for import object and namespaces", functio

expectSourceToContain(source, 'const bb = _module1__WEBPACK_IMPORTED_MODULE_0__.obj1.up.down?.left.right;');

expectSourceToContain(source, 'const ww = (__webpack_require__(/*! ./module1 */ 960).obj1)["bing"]?.bang;');
expectSourceToContain(source, 'const xx = (__webpack_require__(/*! ./module1 */ 960).obj1)["pip"].pop();');
expectSourceToContain(source, 'const yy = (__webpack_require__(/*! ./module3 */ 834).m_2.m_1.obj1)["tip"].top();');

expectSourceToContain(source, '_data__WEBPACK_IMPORTED_MODULE_3__.nested.object3["unknownProperty"].depth = "deep";');

expectSourceToContain(source, '(_module1__WEBPACK_IMPORTED_MODULE_0__.obj1)["aaa"].bbb;');
Expand Down
4 changes: 2 additions & 2 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5521,13 +5521,13 @@ declare class JavascriptParser extends Parser {
>;
memberChainOfCallMemberChain: HookMap<
SyncBailHook<
[Expression, string[], CallExpression, string[]],
[Expression, string[], CallExpression, string[], [number, number][]],
boolean | void
>
>;
callMemberChainOfCallMemberChain: HookMap<
SyncBailHook<
[CallExpression, string[], CallExpression, string[]],
[CallExpression, string[], CallExpression, string[], [number, number][]],
boolean | void
>
>;
Expand Down

0 comments on commit d4e8485

Please sign in to comment.