Skip to content

Commit a5ab3a2

Browse files
fix: populate changeset, preserve child compilations via WeakMap, and improve coverage
1 parent 2056f4c commit a5ab3a2

3 files changed

Lines changed: 182 additions & 6 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: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function flatten(arr) {
8484
* @returns {StatsCompilation | undefined} child asset compilation
8585
*/
8686
function getChildAssetBundles(children, assetName) {
87-
return (children || []).find((child) => {
87+
return children.find((child) => {
8888
if (!child) return false;
8989
if (
9090
Array.isArray(child.assets) &&
@@ -285,6 +285,8 @@ function getViewerData(bundleStats, bundleDir, opts) {
285285
const allChildren = Array.isArray(bundleStats.children)
286286
? [...bundleStats.children]
287287
: [];
288+
/** @type {WeakMap<StatsAsset, StatsCompilation>} */
289+
const childAssetBundles = new WeakMap();
288290

289291
// Sometimes all the information is located in `children` array (e.g. problem in #10)
290292
if (
@@ -301,19 +303,20 @@ function getViewerData(bundleStats, bundleDir, opts) {
301303
// Sometimes if there are additional child chunks produced add them as child assets,
302304
// leave the 1st one as that is considered the 'root' asset.
303305
for (let i = 1; i < allChildren.length; i++) {
304-
for (const asset of allChildren[i]?.assets || []) {
306+
const child = allChildren[i];
307+
for (const asset of child?.assets || []) {
308+
childAssetBundles.set(asset, child);
305309
asset.isChild = true;
306310
bundleStats.assets.push(asset);
307311
}
308312
}
309313
} else if (allChildren.length > 0) {
310-
bundleStats.assets = Array.isArray(bundleStats.assets)
311-
? [...bundleStats.assets]
312-
: [];
314+
bundleStats.assets = [.../** @type {StatsAsset[]} */ (bundleStats.assets)];
313315

314316
// Sometimes if there are additional child chunks produced add them as child assets
315317
for (const child of allChildren) {
316318
for (const asset of child?.assets || []) {
319+
childAssetBundles.set(asset, child);
317320
asset.isChild = true;
318321
bundleStats.assets.push(asset);
319322
}
@@ -424,7 +427,9 @@ function getViewerData(bundleStats, bundleDir, opts) {
424427

425428
if (statAsset.isChild) {
426429
// Preserve child-compilation matching because child assets use a different module list.
427-
const assetBundles = getChildAssetBundles(allChildren, statAsset.name);
430+
const assetBundles =
431+
childAssetBundles.get(statAsset) ||
432+
getChildAssetBundles(allChildren, statAsset.name);
428433
/** @type {StatsModule[]} */
429434
const modules = assetBundles ? getBundleModules(assetBundles) : [];
430435
assetModules = modules.filter((statModule) =>

‎test/analyzerUtils.js‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,170 @@ describe("getViewerData", () => {
329329
const chartData = getViewerData(stats);
330330
expect(chartData[0].isInitialByEntrypoint).toEqual({ main: true });
331331
});
332+
333+
it("handles entrypoint assets with empty or null items", () => {
334+
const stats = {
335+
assets: [
336+
{
337+
name: "bundle.js",
338+
size: 100,
339+
chunks: [1],
340+
},
341+
],
342+
chunks: [
343+
{
344+
id: 1,
345+
modules: [
346+
{
347+
id: 1,
348+
identifier: "./bundle.js",
349+
name: "./bundle.js",
350+
size: 50,
351+
chunks: [1],
352+
},
353+
],
354+
},
355+
],
356+
entrypoints: {
357+
main: {
358+
name: "main",
359+
assets: [null, { name: "" }, "bundle.js"],
360+
},
361+
},
362+
};
363+
364+
const chartData = getViewerData(stats);
365+
expect(chartData[0].isInitialByEntrypoint).toEqual({ main: true });
366+
});
367+
368+
it("handles null or undefined entries in children array", () => {
369+
const stats = {
370+
children: [
371+
null,
372+
{
373+
name: "child",
374+
assets: [
375+
{
376+
name: "bundle.js",
377+
size: 100,
378+
chunks: [1],
379+
},
380+
],
381+
chunks: [
382+
{
383+
id: 1,
384+
modules: [
385+
{
386+
id: 1,
387+
identifier: "./bundle.js",
388+
name: "./bundle.js",
389+
size: 50,
390+
chunks: [1],
391+
},
392+
],
393+
},
394+
],
395+
},
396+
],
397+
};
398+
399+
const chartData = getViewerData(stats);
400+
expect(chartData).toHaveLength(1);
401+
expect(chartData[0].label).toBe("bundle.js");
402+
});
403+
404+
it("resolves pre-marked isChild assets via getChildAssetBundles fallback", () => {
405+
const stats = {
406+
assets: [
407+
{
408+
name: "fallback.js",
409+
size: 100,
410+
chunks: [1],
411+
isChild: true,
412+
},
413+
],
414+
children: [
415+
null,
416+
{
417+
name: "empty-child",
418+
assets: [],
419+
// no assetsByChunkName to exercise empty fallback
420+
},
421+
{
422+
name: "child",
423+
assets: [
424+
{
425+
name: "fallback.js",
426+
size: 100,
427+
chunks: [1],
428+
},
429+
],
430+
chunks: [
431+
{
432+
id: 1,
433+
modules: [
434+
{
435+
id: 1,
436+
identifier: "./fallback-module.js",
437+
name: "./fallback-module.js",
438+
size: 50,
439+
chunks: [1],
440+
},
441+
],
442+
},
443+
],
444+
},
445+
],
446+
};
447+
448+
const chartData = getViewerData(stats);
449+
expect(chartData).toHaveLength(1);
450+
expect(chartData[0].label).toBe("fallback.js");
451+
expect(chartData[0].groups[0].label).toBe("fallback-module.js");
452+
});
453+
454+
it("handles null or undefined bundleStats gracefully", () => {
455+
expect(getViewerData(null)).toEqual([]);
456+
expect(getViewerData(undefined)).toEqual([]);
457+
});
458+
459+
it("resolves child assets by assetsByChunkName when assets array is not present", () => {
460+
const stats = {
461+
assets: [
462+
{
463+
name: "chunk-asset.js",
464+
size: 100,
465+
chunks: [1],
466+
isChild: true,
467+
},
468+
],
469+
children: [
470+
{
471+
name: "legacy-child",
472+
assetsByChunkName: {
473+
main: ["chunk-asset.js"],
474+
},
475+
chunks: [
476+
{
477+
id: 1,
478+
modules: [
479+
{
480+
id: 1,
481+
identifier: "./chunk-mod.js",
482+
name: "./chunk-mod.js",
483+
size: 50,
484+
chunks: [1],
485+
},
486+
],
487+
},
488+
],
489+
},
490+
],
491+
};
492+
493+
const chartData = getViewerData(stats);
494+
expect(chartData).toHaveLength(1);
495+
expect(chartData[0].label).toBe("chunk-asset.js");
496+
expect(chartData[0].groups[0].label).toBe("chunk-mod.js");
497+
});
332498
});

0 commit comments

Comments
 (0)