react-native-bundle-discovery 2.2.0-rc.1 → 2.2.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +3 -10
- package/package.json +2 -2
- package/parseUtils.js +615 -0
- package/webpack.js +221 -0
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@ const { parse, resolve } = require("path");
|
|
|
2
2
|
const { writeFileSync, existsSync } = require("fs");
|
|
3
3
|
const { Buffer } = require("buffer");
|
|
4
4
|
const chalk = require("chalk");
|
|
5
|
-
|
|
5
|
+
const BundleDiscoveryPlugin = require("./webpack").BundleDiscoveryPlugin;
|
|
6
6
|
const NAME = require("./package.json").name;
|
|
7
7
|
|
|
8
8
|
function getDefault(module) {
|
|
@@ -270,13 +270,6 @@ const createProcessModuleFilter =
|
|
|
270
270
|
module.exports = {
|
|
271
271
|
createSerializer,
|
|
272
272
|
createResolveRequest,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
`[${NAME}]: ⚠️'createProcessModuleFilter' is deprecated. Use 'createResolveRequest' instead.`,
|
|
276
|
-
);
|
|
277
|
-
return createProcessModuleFilter;
|
|
278
|
-
},
|
|
279
|
-
get BundleDiscoveryPlugin() {
|
|
280
|
-
return require("./webpack").BundleDiscoveryPlugin;
|
|
281
|
-
},
|
|
273
|
+
createProcessModuleFilter,
|
|
274
|
+
BundleDiscoveryPlugin,
|
|
282
275
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-bundle-discovery",
|
|
3
|
-
"version": "2.2.0-rc.
|
|
3
|
+
"version": "2.2.0-rc.3",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"repository": "git@github.com:retyui/react-native-bundle-discovery.git",
|
|
6
6
|
"author": "David <4661784+retyui@users.noreply.github.com>",
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"
|
|
25
|
+
"*.js"
|
|
26
26
|
]
|
|
27
27
|
}
|
package/parseUtils.js
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
//////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copy of: https://github.com/webpack/webpack-bundle-analyzer/blob/main/src/parseUtils.js
|
|
3
|
+
// Thanks to the Webpack maintainers! 🙏
|
|
4
|
+
//////////////////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** @typedef {import("acorn").Node} Node */
|
|
6
|
+
/** @typedef {import("acorn").CallExpression} CallExpression */
|
|
7
|
+
/** @typedef {import("acorn").ExpressionStatement} ExpressionStatement */
|
|
8
|
+
/** @typedef {import("acorn").Expression} Expression */
|
|
9
|
+
/** @typedef {import("acorn").SpreadElement} SpreadElement */
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const acorn = require("acorn");
|
|
13
|
+
const walk = require("acorn-walk");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Expression} node node
|
|
17
|
+
* @returns {boolean} true when id is numeric, otherwise false
|
|
18
|
+
*/
|
|
19
|
+
function isNumericId(node) {
|
|
20
|
+
return (
|
|
21
|
+
node.type === "Literal" &&
|
|
22
|
+
node.value !== null &&
|
|
23
|
+
node.value !== undefined &&
|
|
24
|
+
Number.isInteger(node.value) &&
|
|
25
|
+
/** @type {number} */ (node.value) >= 0
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Expression | SpreadElement | null} node node
|
|
31
|
+
* @returns {boolean} true when module id, otherwise false
|
|
32
|
+
*/
|
|
33
|
+
function isModuleId(node) {
|
|
34
|
+
return (
|
|
35
|
+
node !== null &&
|
|
36
|
+
node.type === "Literal" &&
|
|
37
|
+
(isNumericId(node) || typeof node.value === "string")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Expression | SpreadElement} node node
|
|
43
|
+
* @returns {boolean} true when module wrapper, otherwise false
|
|
44
|
+
*/
|
|
45
|
+
function isModuleWrapper(node) {
|
|
46
|
+
return (
|
|
47
|
+
// It's an anonymous function expression that wraps module
|
|
48
|
+
((node.type === "FunctionExpression" ||
|
|
49
|
+
node.type === "ArrowFunctionExpression") &&
|
|
50
|
+
!node.id) ||
|
|
51
|
+
// If `DedupePlugin` is used it can be an ID of duplicated module...
|
|
52
|
+
isModuleId(node) ||
|
|
53
|
+
// or an array of shape [<module_id>, ...args]
|
|
54
|
+
(node.type === "ArrayExpression" &&
|
|
55
|
+
node.elements.length > 1 &&
|
|
56
|
+
isModuleId(node.elements[0]))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {Expression | SpreadElement | null} node node
|
|
62
|
+
* @returns {boolean} true when module hash, otherwise false
|
|
63
|
+
*/
|
|
64
|
+
function isModulesHash(node) {
|
|
65
|
+
return (
|
|
66
|
+
node !== null &&
|
|
67
|
+
node.type === "ObjectExpression" &&
|
|
68
|
+
node.properties
|
|
69
|
+
.filter((property) => property.type !== "SpreadElement")
|
|
70
|
+
.map((node) => node.value)
|
|
71
|
+
.every(isModuleWrapper)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Expression | SpreadElement | null} node node
|
|
77
|
+
* @returns {boolean} true when module array, otherwise false
|
|
78
|
+
*/
|
|
79
|
+
function isModulesArray(node) {
|
|
80
|
+
return (
|
|
81
|
+
node !== null &&
|
|
82
|
+
node.type === "ArrayExpression" &&
|
|
83
|
+
node.elements.every(
|
|
84
|
+
(elem) =>
|
|
85
|
+
// Some of array items may be skipped because there is no module with such id
|
|
86
|
+
!elem || isModuleWrapper(elem),
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {Expression | SpreadElement | null} node node
|
|
93
|
+
* @returns {boolean} true when simple modules list, otherwise false
|
|
94
|
+
*/
|
|
95
|
+
function isSimpleModulesList(node) {
|
|
96
|
+
return (
|
|
97
|
+
// Modules are contained in hash. Keys are module ids.
|
|
98
|
+
isModulesHash(node) ||
|
|
99
|
+
// Modules are contained in array. Indexes are module ids.
|
|
100
|
+
isModulesArray(node)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {Expression | SpreadElement | null} node node
|
|
106
|
+
* @returns {boolean} true when optimized modules array, otherwise false
|
|
107
|
+
*/
|
|
108
|
+
function isOptimizedModulesArray(node) {
|
|
109
|
+
// Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
|
|
110
|
+
// https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
|
|
111
|
+
// The `<minimum ID>` + array indexes are module ids
|
|
112
|
+
return (
|
|
113
|
+
node !== null &&
|
|
114
|
+
node.type === "CallExpression" &&
|
|
115
|
+
node.callee.type === "MemberExpression" &&
|
|
116
|
+
// Make sure the object called is `Array(<some number>)`
|
|
117
|
+
node.callee.object.type === "CallExpression" &&
|
|
118
|
+
node.callee.object.callee.type === "Identifier" &&
|
|
119
|
+
node.callee.object.callee.name === "Array" &&
|
|
120
|
+
node.callee.object.arguments.length === 1 &&
|
|
121
|
+
node.callee.object.arguments[0].type !== "SpreadElement" &&
|
|
122
|
+
isNumericId(node.callee.object.arguments[0]) &&
|
|
123
|
+
// Make sure the property X called for `Array(<some number>).X` is `concat`
|
|
124
|
+
node.callee.property.type === "Identifier" &&
|
|
125
|
+
node.callee.property.name === "concat" &&
|
|
126
|
+
// Make sure exactly one array is passed in to `concat`
|
|
127
|
+
node.arguments.length === 1 &&
|
|
128
|
+
isModulesArray(node.arguments[0])
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {Expression | SpreadElement | null} node node
|
|
134
|
+
* @returns {boolean} true when modules list, otherwise false
|
|
135
|
+
*/
|
|
136
|
+
function isModulesList(node) {
|
|
137
|
+
return (
|
|
138
|
+
isSimpleModulesList(node) ||
|
|
139
|
+
// Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
|
|
140
|
+
isOptimizedModulesArray(node)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @typedef {{ start: number, end: number }} Location */
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {Node} node node
|
|
148
|
+
* @returns {Location} location
|
|
149
|
+
*/
|
|
150
|
+
function getModuleLocation(node) {
|
|
151
|
+
return {
|
|
152
|
+
start: node.start,
|
|
153
|
+
end: node.end,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @typedef {Record<number, Location>} ModulesLocations */
|
|
158
|
+
/** @typedef {{ locations: ModulesLocations, hasWebpackRuntime: boolean, isTopLevel: boolean }} Webpack5IIFECandidate */
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {Expression | SpreadElement} node node
|
|
162
|
+
* @returns {ModulesLocations} modules locations
|
|
163
|
+
*/
|
|
164
|
+
function getModulesLocations(node) {
|
|
165
|
+
if (node.type === "ObjectExpression") {
|
|
166
|
+
// Modules hash
|
|
167
|
+
const modulesNodes = node.properties;
|
|
168
|
+
|
|
169
|
+
return modulesNodes.reduce((result, moduleNode) => {
|
|
170
|
+
if (moduleNode.type !== "Property") {
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const moduleId =
|
|
175
|
+
moduleNode.key.type === "Identifier"
|
|
176
|
+
? moduleNode.key.name
|
|
177
|
+
: // @ts-expect-error need verify why we need it, tests not cover it case
|
|
178
|
+
moduleNode.key.value;
|
|
179
|
+
|
|
180
|
+
if (moduleId === "undefined") {
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
result[moduleId] = getModuleLocation(moduleNode.value);
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}, /** @type {ModulesLocations} */ ({}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const isOptimizedArray = node.type === "CallExpression";
|
|
191
|
+
|
|
192
|
+
if (node.type === "ArrayExpression" || isOptimizedArray) {
|
|
193
|
+
// Modules array or optimized array
|
|
194
|
+
const minId =
|
|
195
|
+
isOptimizedArray &&
|
|
196
|
+
node.callee.type === "MemberExpression" &&
|
|
197
|
+
node.callee.object.type === "CallExpression" &&
|
|
198
|
+
node.callee.object.arguments[0].type === "Literal"
|
|
199
|
+
? // Get the [minId] value from the Array() call first argument literal value
|
|
200
|
+
/** @type {number} */ (node.callee.object.arguments[0].value)
|
|
201
|
+
: // `0` for simple array
|
|
202
|
+
0;
|
|
203
|
+
const modulesNodes = isOptimizedArray
|
|
204
|
+
? // The modules reside in the `concat()` function call arguments
|
|
205
|
+
node.arguments[0].type === "ArrayExpression"
|
|
206
|
+
? node.arguments[0].elements
|
|
207
|
+
: []
|
|
208
|
+
: node.elements;
|
|
209
|
+
|
|
210
|
+
return modulesNodes.reduce((result, moduleNode, i) => {
|
|
211
|
+
if (moduleNode) {
|
|
212
|
+
result[i + minId] = getModuleLocation(moduleNode);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return result;
|
|
216
|
+
}, /** @type {ModulesLocations} */ ({}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {ExpressionStatement} node node
|
|
224
|
+
* @returns {CallExpression | null} IIFE call expression
|
|
225
|
+
*/
|
|
226
|
+
function getIIFECallExpression(node) {
|
|
227
|
+
if (node.expression.type === "CallExpression") {
|
|
228
|
+
return node.expression;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
node.expression.type === "UnaryExpression" &&
|
|
233
|
+
node.expression.argument.type === "CallExpression"
|
|
234
|
+
) {
|
|
235
|
+
return node.expression.argument;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {Node} node node
|
|
243
|
+
* @param {string} modulesVariableName modules variable name
|
|
244
|
+
* @returns {boolean} true when the node calls a module wrapper
|
|
245
|
+
*/
|
|
246
|
+
function callsModulesMap(node, modulesVariableName) {
|
|
247
|
+
let callsModule = false;
|
|
248
|
+
|
|
249
|
+
walk.simple(node, {
|
|
250
|
+
CallExpression(callExpression) {
|
|
251
|
+
const { callee } = callExpression;
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
callee.type === "MemberExpression" &&
|
|
255
|
+
((callee.object.type === "Identifier" &&
|
|
256
|
+
callee.object.name === modulesVariableName) ||
|
|
257
|
+
(callee.object.type === "MemberExpression" &&
|
|
258
|
+
callee.object.object.type === "Identifier" &&
|
|
259
|
+
callee.object.object.name === modulesVariableName))
|
|
260
|
+
) {
|
|
261
|
+
callsModule = true;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return callsModule;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {import("acorn").BlockStatement} body body
|
|
271
|
+
* @param {import("acorn").VariableDeclaration} modulesDeclaration modules declaration
|
|
272
|
+
* @param {string} modulesVariableName modules variable name
|
|
273
|
+
* @returns {boolean} true when the candidate contains Webpack module loading
|
|
274
|
+
*/
|
|
275
|
+
function hasWebpackModulesRuntime(
|
|
276
|
+
body,
|
|
277
|
+
modulesDeclaration,
|
|
278
|
+
modulesVariableName,
|
|
279
|
+
) {
|
|
280
|
+
const declarationIndex = body.body.indexOf(modulesDeclaration);
|
|
281
|
+
|
|
282
|
+
return body.body
|
|
283
|
+
.slice(declarationIndex + 1)
|
|
284
|
+
.some((statement) => callsModulesMap(statement, modulesVariableName));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {CallExpression} node node
|
|
289
|
+
* @param {boolean} isTopLevel is top-level IIFE
|
|
290
|
+
* @returns {Webpack5IIFECandidate | null} modules candidate
|
|
291
|
+
*/
|
|
292
|
+
function getWebpack5IIFEModulesCandidate(node, isTopLevel) {
|
|
293
|
+
if (
|
|
294
|
+
node.arguments.length !== 0 ||
|
|
295
|
+
(node.callee.type !== "FunctionExpression" &&
|
|
296
|
+
node.callee.type !== "ArrowFunctionExpression") ||
|
|
297
|
+
node.callee.params.length !== 0 ||
|
|
298
|
+
node.callee.body.type !== "BlockStatement"
|
|
299
|
+
) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const firstVariableDeclaration = node.callee.body.body.find(
|
|
304
|
+
(node) => node.type === "VariableDeclaration",
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (firstVariableDeclaration) {
|
|
308
|
+
for (const declaration of firstVariableDeclaration.declarations) {
|
|
309
|
+
if (declaration.init && isModulesList(declaration.init)) {
|
|
310
|
+
const locations = getModulesLocations(declaration.init);
|
|
311
|
+
|
|
312
|
+
if (Object.keys(locations).length === 0) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
locations,
|
|
318
|
+
hasWebpackRuntime:
|
|
319
|
+
declaration.id.type === "Identifier" &&
|
|
320
|
+
hasWebpackModulesRuntime(
|
|
321
|
+
node.callee.body,
|
|
322
|
+
firstVariableDeclaration,
|
|
323
|
+
declaration.id.name,
|
|
324
|
+
),
|
|
325
|
+
isTopLevel,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @param {Webpack5IIFECandidate[]} candidates candidates
|
|
336
|
+
* @returns {Webpack5IIFECandidate | null} selected candidate
|
|
337
|
+
*/
|
|
338
|
+
function selectWebpack5IIFECandidate(candidates) {
|
|
339
|
+
return (
|
|
340
|
+
candidates.find(
|
|
341
|
+
(candidate) => candidate.hasWebpackRuntime && candidate.isTopLevel,
|
|
342
|
+
) ||
|
|
343
|
+
candidates.find((candidate) => candidate.hasWebpackRuntime) ||
|
|
344
|
+
candidates.find((candidate) => candidate.isTopLevel) ||
|
|
345
|
+
null
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @param {Webpack5IIFECandidate[]} candidates candidates
|
|
351
|
+
* @param {(string | number)[] | undefined} expectedModuleIds expected module ids
|
|
352
|
+
* @returns {ModulesLocations | null} selected modules locations
|
|
353
|
+
*/
|
|
354
|
+
function selectWebpack5IIFEModulesLocations(candidates, expectedModuleIds) {
|
|
355
|
+
if (candidates.length === 0) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (expectedModuleIds?.length) {
|
|
360
|
+
const expectedIds = new Set(expectedModuleIds.map(String));
|
|
361
|
+
const rankedCandidates = candidates
|
|
362
|
+
.map((candidate) => ({
|
|
363
|
+
candidate,
|
|
364
|
+
expectedIdsIntersection: Object.keys(candidate.locations).filter(
|
|
365
|
+
(moduleId) => expectedIds.has(moduleId),
|
|
366
|
+
).length,
|
|
367
|
+
}))
|
|
368
|
+
.toSorted(
|
|
369
|
+
(candidateA, candidateB) =>
|
|
370
|
+
candidateB.expectedIdsIntersection -
|
|
371
|
+
candidateA.expectedIdsIntersection,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
if (rankedCandidates[0].expectedIdsIntersection > 0) {
|
|
375
|
+
const bestCandidates = rankedCandidates
|
|
376
|
+
.filter(
|
|
377
|
+
(rankedCandidate) =>
|
|
378
|
+
rankedCandidate.expectedIdsIntersection ===
|
|
379
|
+
rankedCandidates[0].expectedIdsIntersection,
|
|
380
|
+
)
|
|
381
|
+
.map((rankedCandidate) => rankedCandidate.candidate);
|
|
382
|
+
|
|
383
|
+
return selectWebpack5IIFECandidate(bestCandidates)?.locations || null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return selectWebpack5IIFECandidate(candidates)?.locations || null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {Expression} node node
|
|
392
|
+
* @returns {boolean} true when chunks ids, otherwose false
|
|
393
|
+
*/
|
|
394
|
+
function isChunkIds(node) {
|
|
395
|
+
// Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
|
|
396
|
+
return node.type === "ArrayExpression" && node.elements.every(isModuleId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @param {(Expression | SpreadElement | null)[]} args arguments
|
|
401
|
+
* @returns {boolean} true when async chunk arguments, otherwise false
|
|
402
|
+
*/
|
|
403
|
+
function mayBeAsyncChunkArguments(args) {
|
|
404
|
+
return (
|
|
405
|
+
args.length >= 2 &&
|
|
406
|
+
args[0] !== null &&
|
|
407
|
+
args[0].type !== "SpreadElement" &&
|
|
408
|
+
isChunkIds(args[0])
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Returns bundle source except modules
|
|
414
|
+
* @param {string} content content
|
|
415
|
+
* @param {ModulesLocations | null} modulesLocations modules locations
|
|
416
|
+
* @returns {string} runtime code
|
|
417
|
+
*/
|
|
418
|
+
function getBundleRuntime(content, modulesLocations) {
|
|
419
|
+
const sortedLocations = Object.values(modulesLocations || {}).toSorted(
|
|
420
|
+
(a, b) => a.start - b.start,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
let result = "";
|
|
424
|
+
let lastIndex = 0;
|
|
425
|
+
|
|
426
|
+
for (const { start, end } of sortedLocations) {
|
|
427
|
+
result += content.slice(lastIndex, start);
|
|
428
|
+
lastIndex = end;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return result + content.slice(lastIndex);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @param {CallExpression} node node
|
|
436
|
+
* @returns {boolean} true when is async chunk push expression, otheriwse false
|
|
437
|
+
*/
|
|
438
|
+
function isAsyncChunkPushExpression(node) {
|
|
439
|
+
const { callee, arguments: args } = node;
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
callee.type === "MemberExpression" &&
|
|
443
|
+
callee.property.type === "Identifier" &&
|
|
444
|
+
callee.property.name === "push" &&
|
|
445
|
+
callee.object.type === "AssignmentExpression" &&
|
|
446
|
+
args.length === 1 &&
|
|
447
|
+
args[0].type === "ArrayExpression" &&
|
|
448
|
+
mayBeAsyncChunkArguments(args[0].elements) &&
|
|
449
|
+
isModulesList(args[0].elements[1])
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* @param {CallExpression} node node
|
|
455
|
+
* @returns {boolean} true when is async web worker, otherwise false
|
|
456
|
+
*/
|
|
457
|
+
function isAsyncWebWorkerChunkExpression(node) {
|
|
458
|
+
const { callee, type, arguments: args } = node;
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
type === "CallExpression" &&
|
|
462
|
+
callee.type === "MemberExpression" &&
|
|
463
|
+
args.length === 2 &&
|
|
464
|
+
args[0].type !== "SpreadElement" &&
|
|
465
|
+
isChunkIds(args[0]) &&
|
|
466
|
+
isModulesList(args[1])
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** @typedef {Record<string, string>} Modules */
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @param {string} bundlePath bundle path
|
|
474
|
+
* @param {{ sourceType?: "script" | "module", expectedModuleIds?: (string | number)[] }} opts options
|
|
475
|
+
* @returns {{ modules: Modules, src: string, runtimeSrc: string }} parsed result
|
|
476
|
+
*/
|
|
477
|
+
module.exports.parseBundle = function parseBundle(bundlePath, opts) {
|
|
478
|
+
const { sourceType = "script", expectedModuleIds } = opts || {};
|
|
479
|
+
|
|
480
|
+
const content = fs.readFileSync(bundlePath, "utf8");
|
|
481
|
+
const ast = acorn.parse(content, {
|
|
482
|
+
sourceType,
|
|
483
|
+
ecmaVersion: "latest",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/** @type {Set<CallExpression>} */
|
|
487
|
+
const topLevelIIFECalls = new Set();
|
|
488
|
+
|
|
489
|
+
for (const node of ast.body) {
|
|
490
|
+
if (node.type === "ExpressionStatement") {
|
|
491
|
+
const iifeCall = getIIFECallExpression(node);
|
|
492
|
+
|
|
493
|
+
if (iifeCall) {
|
|
494
|
+
topLevelIIFECalls.add(iifeCall);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** @type {{ locations: ModulesLocations | null, webpack5IIFECandidates: Webpack5IIFECandidate[] }} */
|
|
500
|
+
const walkState = {
|
|
501
|
+
locations: null,
|
|
502
|
+
webpack5IIFECandidates: [],
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
walk.recursive(ast, walkState, {
|
|
506
|
+
AssignmentExpression(node, state) {
|
|
507
|
+
if (state.locations) return;
|
|
508
|
+
|
|
509
|
+
// Modules are stored in exports.modules:
|
|
510
|
+
// exports.modules = {};
|
|
511
|
+
const { left, right } = node;
|
|
512
|
+
|
|
513
|
+
if (
|
|
514
|
+
left &&
|
|
515
|
+
left.type === "MemberExpression" &&
|
|
516
|
+
left.object &&
|
|
517
|
+
left.object.type === "Identifier" &&
|
|
518
|
+
left.object.name === "exports" &&
|
|
519
|
+
left.property &&
|
|
520
|
+
left.property.type === "Identifier" &&
|
|
521
|
+
left.property.name === "modules" &&
|
|
522
|
+
isModulesHash(right)
|
|
523
|
+
) {
|
|
524
|
+
state.locations = getModulesLocations(right);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
CallExpression(node, state, callback) {
|
|
529
|
+
if (state.locations) return;
|
|
530
|
+
|
|
531
|
+
const args = node.arguments;
|
|
532
|
+
const webpack5IIFEModulesCandidate = getWebpack5IIFEModulesCandidate(
|
|
533
|
+
node,
|
|
534
|
+
topLevelIIFECalls.has(node),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (webpack5IIFEModulesCandidate) {
|
|
538
|
+
state.webpack5IIFECandidates.push(webpack5IIFEModulesCandidate);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Main chunk with webpack loader.
|
|
542
|
+
// Modules are stored in first argument:
|
|
543
|
+
// (function (...) {...})(<modules>)
|
|
544
|
+
if (
|
|
545
|
+
node.callee.type === "FunctionExpression" &&
|
|
546
|
+
!node.callee.id &&
|
|
547
|
+
args.length === 1 &&
|
|
548
|
+
isSimpleModulesList(args[0])
|
|
549
|
+
) {
|
|
550
|
+
state.locations = getModulesLocations(args[0]);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Async Webpack < v4 chunk without webpack loader.
|
|
555
|
+
// webpackJsonp([<chunks>], <modules>, ...)
|
|
556
|
+
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
|
|
557
|
+
if (
|
|
558
|
+
node.callee.type === "Identifier" &&
|
|
559
|
+
mayBeAsyncChunkArguments(args) &&
|
|
560
|
+
args[1].type !== "SpreadElement" &&
|
|
561
|
+
isModulesList(args[1])
|
|
562
|
+
) {
|
|
563
|
+
state.locations = getModulesLocations(args[1]);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Async Webpack v4 chunk without webpack loader.
|
|
568
|
+
// (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
|
|
569
|
+
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
|
|
570
|
+
if (
|
|
571
|
+
isAsyncChunkPushExpression(node) &&
|
|
572
|
+
args[0].type === "ArrayExpression" &&
|
|
573
|
+
args[0].elements[1]
|
|
574
|
+
) {
|
|
575
|
+
state.locations = getModulesLocations(args[0].elements[1]);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Webpack v4 WebWorkerChunkTemplatePlugin
|
|
580
|
+
// globalObject.chunkCallbackName([<chunks>],<modules>, ...);
|
|
581
|
+
// Both globalObject and chunkCallbackName can be changed through the config, so we can't check them.
|
|
582
|
+
if (isAsyncWebWorkerChunkExpression(node)) {
|
|
583
|
+
state.locations = getModulesLocations(args[1]);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
|
|
588
|
+
// features (e.g. `umd` library output) can wrap modules list into additional IIFE.
|
|
589
|
+
for (const arg of args) {
|
|
590
|
+
callback(arg, state);
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const modulesLocations =
|
|
596
|
+
walkState.locations ||
|
|
597
|
+
selectWebpack5IIFEModulesLocations(
|
|
598
|
+
walkState.webpack5IIFECandidates,
|
|
599
|
+
expectedModuleIds,
|
|
600
|
+
);
|
|
601
|
+
/** @type {Modules} */
|
|
602
|
+
const modules = {};
|
|
603
|
+
|
|
604
|
+
if (modulesLocations) {
|
|
605
|
+
for (const [id, loc] of Object.entries(modulesLocations)) {
|
|
606
|
+
modules[id] = content.slice(loc.start, loc.end);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
modules,
|
|
612
|
+
src: content,
|
|
613
|
+
runtimeSrc: getBundleRuntime(content, modulesLocations),
|
|
614
|
+
};
|
|
615
|
+
};
|
package/webpack.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
|
|
6
|
+
const { parseBundle } = require("./parseUtils.js");
|
|
7
|
+
const NAME = require("./package.json").name;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Simple Webpack/Rspack plugin
|
|
11
|
+
* to adapt stats object https://webpack.js.org/api/stats/
|
|
12
|
+
* to metro-stats.json format used by `react-native-bundle-discovery`
|
|
13
|
+
*/
|
|
14
|
+
class BundleDiscoveryPlugin {
|
|
15
|
+
constructor({ filename, options, enabled } = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.filename = filename || "metro-stats.json";
|
|
18
|
+
this.enabled = enabled || true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extractSizesFromJsBundle(statAsset, options) {
|
|
22
|
+
const assetFile = path.join(options.output.path, statAsset.name);
|
|
23
|
+
try {
|
|
24
|
+
return parseBundle(assetFile, {
|
|
25
|
+
sourceType: statAsset.info.javascriptModule ? "module" : "script",
|
|
26
|
+
});
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
apply(compiler) {
|
|
33
|
+
if (!this.enabled) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
compiler.hooks.done.tap("GenerateStatsJsonPlugin", (stats) => {
|
|
38
|
+
// Extract the relevant information from the stats object
|
|
39
|
+
const json = stats.toJson({
|
|
40
|
+
version: true,
|
|
41
|
+
builtAt: true,
|
|
42
|
+
modules: true,
|
|
43
|
+
reasons: true,
|
|
44
|
+
assets: true,
|
|
45
|
+
ids: true,
|
|
46
|
+
...this.options,
|
|
47
|
+
all: false,
|
|
48
|
+
source: true,
|
|
49
|
+
});
|
|
50
|
+
// Format the stats into the desired structure
|
|
51
|
+
const statsJson = this.formatStat(
|
|
52
|
+
json,
|
|
53
|
+
// Parse JS bundle to extract modules sizes
|
|
54
|
+
this.extractSizesFromJsBundle(json.assets[0], compiler.options),
|
|
55
|
+
compiler.options,
|
|
56
|
+
);
|
|
57
|
+
// Write the formatted stats to the specified output file
|
|
58
|
+
const outputPath = path.resolve(compiler.options.context, this.filename);
|
|
59
|
+
fs.writeFileSync(outputPath, JSON.stringify(statsJson, null, 2));
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
`${chalk.yellow(`[${NAME}]`)}: Saved stats to ${chalk.green(
|
|
63
|
+
outputPath,
|
|
64
|
+
)}`,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
readSource(filePath) {
|
|
70
|
+
const fallback = { code: "", lineCount: 0, sizeInBytes: 0 };
|
|
71
|
+
try {
|
|
72
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
73
|
+
const lineCount = code.split("\n").length;
|
|
74
|
+
const sizeInBytes = Buffer.byteLength(code, "utf-8");
|
|
75
|
+
return {
|
|
76
|
+
code: this.options?.source === false ? "" : code,
|
|
77
|
+
lineCount,
|
|
78
|
+
sizeInBytes,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getPackageName(modulePath) {
|
|
86
|
+
if (!modulePath) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const parts = modulePath.split("node_modules");
|
|
90
|
+
if (parts.length < 2) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const packagePath = parts[1].split(path.sep).filter(Boolean);
|
|
94
|
+
if (packagePath.length === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (packagePath[0].startsWith("@")) {
|
|
98
|
+
return `${packagePath[0]}/${packagePath[1]}`;
|
|
99
|
+
}
|
|
100
|
+
return packagePath[0];
|
|
101
|
+
}
|
|
102
|
+
getAbsolutePath(packageName, modulePath) {
|
|
103
|
+
if (!packageName) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return modulePath.substring(
|
|
107
|
+
0,
|
|
108
|
+
modulePath.indexOf(packageName) + packageName.length,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
getPackages(statsJson) {
|
|
112
|
+
const packagesMap = statsJson.modules.reduce((allPackages, mdl) => {
|
|
113
|
+
const name = this.getPackageName(mdl.nameForCondition);
|
|
114
|
+
if (!name) {
|
|
115
|
+
return allPackages;
|
|
116
|
+
}
|
|
117
|
+
const absolutePath = this.getAbsolutePath(name, mdl.nameForCondition);
|
|
118
|
+
if (!allPackages.has(absolutePath)) {
|
|
119
|
+
allPackages.set(absolutePath, {
|
|
120
|
+
name,
|
|
121
|
+
absolutePath,
|
|
122
|
+
version: require(path.join(absolutePath, "package.json")).version,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return allPackages;
|
|
126
|
+
}, new Map());
|
|
127
|
+
return Array.from(packagesMap.values());
|
|
128
|
+
}
|
|
129
|
+
getEntryFile(statsJson, rootFolder) {
|
|
130
|
+
const fallbackValue = path.join(rootFolder, "index.js");
|
|
131
|
+
return (
|
|
132
|
+
statsJson.modules.find(
|
|
133
|
+
(mdl) =>
|
|
134
|
+
!mdl.nameForCondition?.includes("node_modules/") &&
|
|
135
|
+
mdl.reasons?.length === 1 &&
|
|
136
|
+
mdl.reasons?.[0].type === "entry",
|
|
137
|
+
)?.nameForCondition || fallbackValue
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
getModuleOutput(mdl, bundleModules) {
|
|
141
|
+
const jsCodeStrFromJSBundle = bundleModules?.modules?.[mdl.id];
|
|
142
|
+
if (jsCodeStrFromJSBundle) {
|
|
143
|
+
const formatted =
|
|
144
|
+
jsCodeStrFromJSBundle.startsWith("function(") &&
|
|
145
|
+
jsCodeStrFromJSBundle.endsWith("}")
|
|
146
|
+
? jsCodeStrFromJSBundle.replace("function(", "function module(")
|
|
147
|
+
: jsCodeStrFromJSBundle;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
code: formatted,
|
|
151
|
+
lineCount: formatted.split("\n").length ?? 0,
|
|
152
|
+
sizeInBytes: Buffer.byteLength(jsCodeStrFromJSBundle, "utf-8"),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
code: mdl.source ?? "",
|
|
157
|
+
lineCount: mdl.source?.split("\n").length ?? 0,
|
|
158
|
+
sizeInBytes: mdl.size ?? 0,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
formatStat(statsJson, bundleModules, compilerOptions) {
|
|
163
|
+
return {
|
|
164
|
+
date: statsJson.builtAt || Date.now(),
|
|
165
|
+
entryPoint: this.getEntryFile(statsJson, compilerOptions.context),
|
|
166
|
+
rootFolder: compilerOptions.context,
|
|
167
|
+
// envs: {},
|
|
168
|
+
transformOptions: {
|
|
169
|
+
// customTransformOptions: {},
|
|
170
|
+
dev: compilerOptions.mode !== "production",
|
|
171
|
+
minify: compilerOptions.optimization?.minimize ?? false,
|
|
172
|
+
platform: compilerOptions.name ?? "ios",
|
|
173
|
+
// type: "module",
|
|
174
|
+
// unstable_transformProfile: "default",
|
|
175
|
+
},
|
|
176
|
+
modules: statsJson.modules
|
|
177
|
+
.map((mdl) => {
|
|
178
|
+
if (mdl.type === "runtime modules") {
|
|
179
|
+
const sourceAndOutput = {
|
|
180
|
+
code: bundleModules.runtimeSrc ?? "",
|
|
181
|
+
lineCount: bundleModules.runtimeSrc?.split("\n").length ?? 0,
|
|
182
|
+
sizeInBytes: Buffer.byteLength(
|
|
183
|
+
bundleModules.runtimeSrc ?? "",
|
|
184
|
+
"utf-8",
|
|
185
|
+
),
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
path: "__prelude__",
|
|
189
|
+
source: sourceAndOutput,
|
|
190
|
+
output: sourceAndOutput,
|
|
191
|
+
dependencies: [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
path: mdl.nameForCondition,
|
|
197
|
+
source: this.readSource(mdl.nameForCondition),
|
|
198
|
+
output: this.getModuleOutput(mdl, bundleModules),
|
|
199
|
+
dependencies:
|
|
200
|
+
mdl.reasons
|
|
201
|
+
?.filter((r) => r.type !== "entry")
|
|
202
|
+
.map((reason) => {
|
|
203
|
+
return {
|
|
204
|
+
absolutePath: path.isAbsolute(reason.resolvedModule)
|
|
205
|
+
? reason.resolvedModule
|
|
206
|
+
: path.resolve(
|
|
207
|
+
compilerOptions.context,
|
|
208
|
+
reason.resolvedModule,
|
|
209
|
+
),
|
|
210
|
+
name: reason.userRequest,
|
|
211
|
+
};
|
|
212
|
+
}) ?? [],
|
|
213
|
+
};
|
|
214
|
+
})
|
|
215
|
+
.filter((e) => e !== null),
|
|
216
|
+
packages: this.getPackages(statsJson),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { BundleDiscoveryPlugin };
|