Skip to content

Commit c6f85b2

Browse files
fix: prevent TypeError when child compilations have missing assets (#741)
* fix: prevent TypeError when child compilations have missing assets * fix: populate changeset, preserve child compilations via WeakMap, and improve coverage * fix: preserve entrypoint metadata for appended child compilations
1 parent 6046503 commit c6f85b2

3 files changed

Lines changed: 568 additions & 39 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-bundle-analyzer": patch
3+
---
4+
5+
Fix `TypeError: Cannot read property 'forEach' of undefined` when child compilations or entrypoints have undefined or missing assets.

‎src/analyzer.js‎

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,22 @@ function flatten(arr) {
7979
}
8080

8181
/**
82-
* @param {StatsCompilation} bundleStats bundle stats
82+
* @param {StatsCompilation[]} children child stats list
8383
* @param {string} assetName asset name
84-
* @returns {boolean} child asset bundlers
84+
* @returns {StatsCompilation | undefined} child asset compilation
8585
*/
86-
function getChildAssetBundles(bundleStats, assetName) {
87-
return flatten(
88-
(bundleStats.children || /** @type {StatsCompilation} */ ([])).find(
89-
/**
90-
* @param {StatsCompilation} child child stats
91-
* @returns {string[][]} assets by chunk name
92-
*/
93-
(child) => Object.values(child.assetsByChunkName || []),
94-
),
95-
).includes(assetName);
86+
function getChildAssetBundles(children, assetName) {
87+
return children.find((child) => {
88+
if (!child) return false;
89+
if (
90+
Array.isArray(child.assets) &&
91+
child.assets.some((asset) => asset && asset.name === assetName)
92+
) {
93+
return true;
94+
}
95+
const chunkAssets = flatten(Object.values(child.assetsByChunkName || {}));
96+
return chunkAssets.includes(assetName);
97+
});
9698
}
9799

98100
/**
@@ -195,7 +197,7 @@ function getAssetModulesByChunk(statsAsset, modulesByChunk) {
195197
/** @typedef {Record<string, Record<string, boolean>>} ChunkToInitialByEntrypoint */
196198

197199
/**
198-
* @param {StatsCompilation} bundleStats bundle stats
200+
* @param {StatsCompilation | undefined} bundleStats bundle stats
199201
* @returns {ChunkToInitialByEntrypoint} chunk to initial by entrypoint
200202
*/
201203
function getChunkToInitialByEntrypoint(bundleStats) {
@@ -206,8 +208,10 @@ function getChunkToInitialByEntrypoint(bundleStats) {
206208
const chunkToEntrypointInititalMap = {};
207209
for (const entrypoint of Object.values(bundleStats.entrypoints || {})) {
208210
for (const asset of entrypoint.assets || []) {
209-
chunkToEntrypointInititalMap[asset.name] ??= {};
210-
chunkToEntrypointInititalMap[asset.name][
211+
const assetName = typeof asset === "string" ? asset : asset?.name;
212+
if (!assetName) continue;
213+
chunkToEntrypointInititalMap[assetName] ??= {};
214+
chunkToEntrypointInititalMap[assetName][
211215
/** @type {string} */
212216
(entrypoint.name)
213217
] = true;
@@ -255,7 +259,7 @@ function isEntryModule(statsModule) {
255259
*/
256260

257261
/**
258-
* @param {StatsCompilation} bundleStats bundle stats
262+
* @param {StatsCompilation | StatsCompilation[]} bundleStats bundle stats
259263
* @param {string | null} bundleDir bundle dir
260264
* @param {ViewerDataOptions=} opts options
261265
* @returns {ChartData} chart data
@@ -267,34 +271,54 @@ function getViewerData(bundleStats, bundleDir, opts) {
267271
excludeAssets = null,
268272
} = opts || {};
269273

274+
if (!bundleStats) return [];
275+
276+
if (Array.isArray(bundleStats)) {
277+
bundleStats = {
278+
children: bundleStats,
279+
};
280+
}
281+
270282
const isAssetIncluded = createAssetsFilter(excludeAssets);
271283

284+
// Preserve all children from the root bundleStats (e.g. for MultiCompiler or child compilations)
285+
const allChildren = Array.isArray(bundleStats.children)
286+
? [...bundleStats.children]
287+
: [];
288+
/** @type {WeakMap<StatsAsset, StatsCompilation>} */
289+
const childAssetBundles = new WeakMap();
290+
272291
// Sometimes all the information is located in `children` array (e.g. problem in #10)
273292
if (
274293
(bundleStats.assets === null ||
275294
bundleStats.assets === undefined ||
276295
bundleStats.assets.length === 0) &&
277-
bundleStats.children &&
278-
bundleStats.children.length > 0
296+
allChildren.length > 0
279297
) {
280-
const { children } = bundleStats;
281-
[bundleStats] = bundleStats.children;
298+
bundleStats = { ...allChildren[0] };
299+
bundleStats.assets = Array.isArray(bundleStats.assets)
300+
? [...bundleStats.assets]
301+
: [];
302+
282303
// Sometimes if there are additional child chunks produced add them as child assets,
283304
// leave the 1st one as that is considered the 'root' asset.
284-
for (let i = 1; i < children.length; i++) {
285-
for (const asset of children[i].assets || []) {
305+
for (let i = 1; i < allChildren.length; i++) {
306+
const child = allChildren[i];
307+
for (const asset of child?.assets || []) {
308+
childAssetBundles.set(asset, child);
286309
asset.isChild = true;
287-
/** @type {StatsAsset[]} */
288-
(bundleStats.assets).push(asset);
310+
bundleStats.assets.push(asset);
289311
}
290312
}
291-
} else if (bundleStats.children && bundleStats.children.length > 0) {
313+
} else if (allChildren.length > 0) {
314+
bundleStats.assets = [.../** @type {StatsAsset[]} */ (bundleStats.assets)];
315+
292316
// Sometimes if there are additional child chunks produced add them as child assets
293-
for (const child of bundleStats.children) {
294-
for (const asset of child.assets || []) {
317+
for (const child of allChildren) {
318+
for (const asset of child?.assets || []) {
319+
childAssetBundles.set(asset, child);
295320
asset.isChild = true;
296-
/** @type {StatsAsset[]} */
297-
(bundleStats.assets).push(asset);
321+
bundleStats.assets.push(asset);
298322
}
299323
}
300324
}
@@ -395,27 +419,35 @@ function getViewerData(bundleStats, bundleDir, opts) {
395419
}
396420
}
397421

398-
/** @typedef {{ size: number, parsedSize?: number, gzipSize?: number, brotliSize?: number, zstdSize?: number, modules: StatsModule[], tree: Folder }} Asset */
422+
const rootChunkToInitialByEntrypoint =
423+
getChunkToInitialByEntrypoint(bundleStats);
424+
425+
/** @typedef {{ size: number, parsedSize?: number, gzipSize?: number, brotliSize?: number, zstdSize?: number, modules: StatsModule[], tree: Folder, isInitialByEntrypoint: Record<string, boolean> }} Asset */
399426

400427
const assets = bundleStats.assets.reduce((result, statAsset) => {
401428
/** @type {StatsModule[]} */
402429
let assetModules;
430+
/** @type {Record<string, boolean>} */
431+
let isInitialByEntrypoint;
403432

404433
if (statAsset.isChild) {
405434
// Preserve child-compilation matching because child assets use a different module list.
406-
const assetBundles = getChildAssetBundles(bundleStats, statAsset.name);
435+
const assetBundles =
436+
childAssetBundles.get(statAsset) ||
437+
getChildAssetBundles(allChildren, statAsset.name);
407438
/** @type {StatsModule[]} */
408-
const modules = assetBundles
409-
? // @ts-expect-error TODO looks like we have a bug with child compilation parsing, need to add test cases
410-
getBundleModules(assetBundles)
411-
: [];
439+
const modules = assetBundles ? getBundleModules(assetBundles) : [];
412440
assetModules = modules.filter((statModule) =>
413441
assetHasModule(statAsset, statModule),
414442
);
443+
isInitialByEntrypoint =
444+
getChunkToInitialByEntrypoint(assetBundles)[statAsset.name] ?? {};
415445
} else {
416446
assetModules = /** @type {StatsModule[]} */ (
417-
rootAssetModules.get(statAsset)
447+
rootAssetModules.get(statAsset) || []
418448
);
449+
isInitialByEntrypoint =
450+
rootChunkToInitialByEntrypoint[statAsset.name] ?? {};
419451
}
420452

421453
// A module that is a part of more than one asset is one stats object. Every asset that
@@ -426,6 +458,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
426458

427459
const asset = (result[statAsset.name] = /** @type {Asset} */ ({
428460
size: statAsset.size,
461+
isInitialByEntrypoint,
429462
}));
430463
const assetSources =
431464
bundlesSources && Object.hasOwn(bundlesSources, statAsset.name)
@@ -503,8 +536,6 @@ function getViewerData(bundleStats, bundleDir, opts) {
503536
return result;
504537
}, /** @type {Record<string, Asset>} */ ({}));
505538

506-
const chunkToInitialByEntrypoint = getChunkToInitialByEntrypoint(bundleStats);
507-
508539
return Object.entries(assets).map(([filename, asset]) => ({
509540
label: filename,
510541
isAsset: true,
@@ -518,7 +549,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
518549
brotliSize: asset.brotliSize,
519550
zstdSize: asset.zstdSize,
520551
groups: Object.values(asset.tree.children).map((i) => i.toChartData()),
521-
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {},
552+
isInitialByEntrypoint: asset.isInitialByEntrypoint,
522553
}));
523554
}
524555

0 commit comments

Comments
 (0)