Skip to content

Commit 39933d3

Browse files
authored
feat: cjs require destructuring assignment tree shaking (#20548)
1 parent 27c13b4 commit 39933d3

9 files changed

Lines changed: 158 additions & 3 deletions

File tree

.changeset/lazy-paws-begin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": feat
3+
---
4+
5+
Added support for destructuring assignment `require` in cjs, allowing for tree shaking.

lib/dependencies/CommonJsImportsParserPlugin.js

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
expressionIsUnsupported,
1515
toConstantDependency
1616
} = require("../javascript/JavascriptParserHelpers");
17+
const traverseDestructuringAssignmentProperties = require("../util/traverseDestructuringAssignmentProperties");
1718
const CommonJsFullRequireDependency = require("./CommonJsFullRequireDependency");
1819
const CommonJsRequireContextDependency = require("./CommonJsRequireContextDependency");
1920
const CommonJsRequireDependency = require("./CommonJsRequireDependency");
@@ -31,6 +32,7 @@ const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency
3132
/** @typedef {import("estree").NewExpression} NewExpression */
3233
/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
3334
/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
35+
/** @typedef {import("../Dependency").RawReferencedExports} RawReferencedExports */
3436
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
3537
/** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
3638
/** @typedef {import("../javascript/JavascriptParser").ImportSource} ImportSource */
@@ -47,6 +49,48 @@ const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency
4749

4850
const PLUGIN_NAME = "CommonJsImportsParserPlugin";
4951

52+
/**
53+
* @param {Expression} expression expression
54+
* @returns {boolean} true, when expression is `require(...)` or `module.require(...)`
55+
*/
56+
const isRequireCallExpression = (expression) => {
57+
if (expression.type !== "CallExpression") return false;
58+
const { callee } = expression;
59+
if (callee.type === "Identifier") {
60+
return callee.name === "require";
61+
}
62+
if (callee.type === "MemberExpression" && !callee.computed) {
63+
const object = callee.object;
64+
const property = callee.property;
65+
return (
66+
object.type === "Identifier" &&
67+
object.name === "module" &&
68+
property.type === "Identifier" &&
69+
property.name === "require"
70+
);
71+
}
72+
return false;
73+
};
74+
75+
/**
76+
* @param {JavascriptParser} parser parser
77+
* @param {CallExpression | NewExpression} expr expression
78+
* @returns {RawReferencedExports | null} referenced exports from destructuring
79+
*/
80+
const getRequireReferencedExportsFromDestructuring = (parser, expr) => {
81+
const referencedPropertiesInDestructuring =
82+
parser.destructuringAssignmentPropertiesFor(expr);
83+
if (!referencedPropertiesInDestructuring) return null;
84+
85+
/** @type {RawReferencedExports} */
86+
const referencedExports = [];
87+
traverseDestructuringAssignmentProperties(
88+
referencedPropertiesInDestructuring,
89+
(stack) => referencedExports.push(stack.map((p) => p.id))
90+
);
91+
return referencedExports;
92+
};
93+
5094
/**
5195
* @param {JavascriptParser} parser parser
5296
* @returns {(expr: Expression) => boolean} handler
@@ -101,10 +145,15 @@ const createRequireCallHandler = (parser, options, getContext) => {
101145
*/
102146
const processRequireItem = (expr, param) => {
103147
if (param.isString()) {
148+
const referencedExports = getRequireReferencedExportsFromDestructuring(
149+
parser,
150+
expr
151+
);
104152
const dep = new CommonJsRequireDependency(
105153
/** @type {string} */ (param.string),
106154
/** @type {Range} */ (param.range),
107-
getContext()
155+
getContext(),
156+
referencedExports
108157
);
109158
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
110159
dep.optional = Boolean(parser.scope.inTry);
@@ -118,14 +167,19 @@ const createRequireCallHandler = (parser, options, getContext) => {
118167
* @returns {boolean | void} true when handled
119168
*/
120169
const processRequireContext = (expr, param) => {
170+
const referencedExports = getRequireReferencedExportsFromDestructuring(
171+
parser,
172+
expr
173+
);
121174
const dep = ContextDependencyHelpers.create(
122175
CommonJsRequireContextDependency,
123176
/** @type {Range} */ (expr.range),
124177
param,
125178
expr,
126179
options,
127180
{
128-
category: "commonjs"
181+
category: "commonjs",
182+
referencedExports
129183
},
130184
parser,
131185
undefined,
@@ -353,6 +407,13 @@ class CommonJsImportsParserPlugin {
353407
*/
354408
apply(parser) {
355409
const options = this.options;
410+
parser.hooks.collectDestructuringAssignmentProperties.tap(
411+
PLUGIN_NAME,
412+
(expr) => {
413+
if (isRequireCallExpression(expr)) return true;
414+
}
415+
);
416+
356417
const getContext = () => {
357418
if (parser.currentTagData) {
358419
const { context } =

lib/dependencies/CommonJsRequireContextDependency.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"use strict";
77

8+
const Dependency = require("../Dependency");
89
const makeSerializable = require("../util/makeSerializable");
910
const ContextDependency = require("./ContextDependency");
1011
const ContextDependencyTemplateAsRequireCall = require("./ContextDependencyTemplateAsRequireCall");
@@ -13,6 +14,10 @@ const ContextDependencyTemplateAsRequireCall = require("./ContextDependencyTempl
1314
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
1415
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
1516
/** @typedef {import("./ContextDependency").ContextDependencyOptions} ContextDependencyOptions */
17+
/** @typedef {import("../Dependency").RawReferencedExports} RawReferencedExports */
18+
/** @typedef {import("../Dependency").ReferencedExports} ReferencedExports */
19+
/** @typedef {import("../ModuleGraph")} ModuleGraph */
20+
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
1621

1722
class CommonJsRequireContextDependency extends ContextDependency {
1823
/**
@@ -35,6 +40,22 @@ class CommonJsRequireContextDependency extends ContextDependency {
3540
return "cjs require context";
3641
}
3742

43+
/**
44+
* Returns list of exports referenced by this dependency
45+
* @param {ModuleGraph} moduleGraph module graph
46+
* @param {RuntimeSpec} runtime the runtime for which the module is analysed
47+
* @returns {ReferencedExports} referenced exports
48+
*/
49+
getReferencedExports(moduleGraph, runtime) {
50+
if (!this.options.referencedExports) {
51+
return Dependency.EXPORTS_OBJECT_REFERENCED;
52+
}
53+
return this.options.referencedExports.map((name) => ({
54+
name,
55+
canMangle: false
56+
}));
57+
}
58+
3859
/**
3960
* @param {ObjectSerializerContext} context context
4061
*/

lib/dependencies/CommonJsRequireDependency.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,31 @@
55

66
"use strict";
77

8+
const Dependency = require("../Dependency");
89
const makeSerializable = require("../util/makeSerializable");
910
const ModuleDependency = require("./ModuleDependency");
1011
const ModuleDependencyTemplateAsId = require("./ModuleDependencyTemplateAsId");
1112

13+
/** @typedef {import("../Dependency").RawReferencedExports} RawReferencedExports */
14+
/** @typedef {import("../Dependency").ReferencedExports} ReferencedExports */
15+
/** @typedef {import("../ModuleGraph")} ModuleGraph */
1216
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
17+
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
18+
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
19+
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
1320

1421
class CommonJsRequireDependency extends ModuleDependency {
1522
/**
1623
* @param {string} request request
1724
* @param {Range=} range location in source code
1825
* @param {string=} context request context
26+
* @param {RawReferencedExports | null=} referencedExports list of referenced exports
1927
*/
20-
constructor(request, range, context) {
28+
constructor(request, range, context, referencedExports = null) {
2129
super(request);
2230
this.range = range;
2331
this._context = context;
32+
this.referencedExports = referencedExports;
2433
}
2534

2635
get type() {
@@ -30,6 +39,38 @@ class CommonJsRequireDependency extends ModuleDependency {
3039
get category() {
3140
return "commonjs";
3241
}
42+
43+
/**
44+
* Returns list of exports referenced by this dependency
45+
* @param {ModuleGraph} moduleGraph module graph
46+
* @param {RuntimeSpec} runtime the runtime for which the module is analysed
47+
* @returns {ReferencedExports} referenced exports
48+
*/
49+
getReferencedExports(moduleGraph, runtime) {
50+
if (!this.referencedExports) return Dependency.EXPORTS_OBJECT_REFERENCED;
51+
return this.referencedExports.map((name) => ({
52+
name,
53+
canMangle: false
54+
}));
55+
}
56+
57+
/**
58+
* @param {ObjectSerializerContext} context context
59+
*/
60+
serialize(context) {
61+
const { write } = context;
62+
write(this.referencedExports);
63+
super.serialize(context);
64+
}
65+
66+
/**
67+
* @param {ObjectDeserializerContext} context context
68+
*/
69+
deserialize(context) {
70+
const { read } = context;
71+
this.referencedExports = read();
72+
super.deserialize(context);
73+
}
3374
}
3475

3576
CommonJsRequireDependency.Template = ModuleDependencyTemplateAsId;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const a = "a/a";
2+
export const b = "a/b";
3+
export const usedExports = __webpack_exports_info__.usedExports;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const a = "b/a";
2+
export const b = "b/b";
3+
export const usedExports = __webpack_exports_info__.usedExports;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
it("should static analyze require destructuring assignment", () => {
2+
const { a, usedExports } = require("./module");
3+
expect(a).toBe("a");
4+
expect(usedExports).toEqual(["a", "usedExports"]);
5+
});
6+
7+
it("should support require context destructuring assignment", () => {
8+
const file = "a";
9+
const { a, usedExports } = require(`./dir/${file}.js`);
10+
expect(a).toBe("a/a");
11+
expect(usedExports).toEqual(["a", "usedExports"]);
12+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports.a = "a";
2+
exports.b = "b";
3+
exports.usedExports = __webpack_exports_info__.usedExports;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"use strict";
2+
3+
module.exports = function filter(config) {
4+
// This test can't run in development mode
5+
return config.mode !== "development";
6+
};

0 commit comments

Comments
 (0)