Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-child-compilations-type-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-bundle-analyzer": patch
---

Fix `TypeError: Cannot read property 'forEach' of undefined` when child compilations or entrypoints have undefined or missing assets.
109 changes: 70 additions & 39 deletions src/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,22 @@ function flatten(arr) {
}

/**
* @param {StatsCompilation} bundleStats bundle stats
* @param {StatsCompilation[]} children child stats list
* @param {string} assetName asset name
* @returns {boolean} child asset bundlers
* @returns {StatsCompilation | undefined} child asset compilation
*/
function getChildAssetBundles(bundleStats, assetName) {
return flatten(
(bundleStats.children || /** @type {StatsCompilation} */ ([])).find(
/**
* @param {StatsCompilation} child child stats
* @returns {string[][]} assets by chunk name
*/
(child) => Object.values(child.assetsByChunkName || []),
),
).includes(assetName);
function getChildAssetBundles(children, assetName) {
return children.find((child) => {
if (!child) return false;
if (
Array.isArray(child.assets) &&
child.assets.some((asset) => asset && asset.name === assetName)
) {
return true;
}
const chunkAssets = flatten(Object.values(child.assetsByChunkName || {}));
return chunkAssets.includes(assetName);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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

/**
* @param {StatsCompilation} bundleStats bundle stats
* @param {StatsCompilation | undefined} bundleStats bundle stats
* @returns {ChunkToInitialByEntrypoint} chunk to initial by entrypoint
*/
function getChunkToInitialByEntrypoint(bundleStats) {
Expand All @@ -206,8 +208,10 @@ function getChunkToInitialByEntrypoint(bundleStats) {
const chunkToEntrypointInititalMap = {};
for (const entrypoint of Object.values(bundleStats.entrypoints || {})) {
for (const asset of entrypoint.assets || []) {
chunkToEntrypointInititalMap[asset.name] ??= {};
chunkToEntrypointInititalMap[asset.name][
const assetName = typeof asset === "string" ? asset : asset?.name;
if (!assetName) continue;
chunkToEntrypointInititalMap[assetName] ??= {};
chunkToEntrypointInititalMap[assetName][
/** @type {string} */
(entrypoint.name)
] = true;
Expand Down Expand Up @@ -255,7 +259,7 @@ function isEntryModule(statsModule) {
*/

/**
* @param {StatsCompilation} bundleStats bundle stats
* @param {StatsCompilation | StatsCompilation[]} bundleStats bundle stats
* @param {string | null} bundleDir bundle dir
* @param {ViewerDataOptions=} opts options
* @returns {ChartData} chart data
Expand All @@ -267,34 +271,54 @@ function getViewerData(bundleStats, bundleDir, opts) {
excludeAssets = null,
} = opts || {};

if (!bundleStats) return [];

if (Array.isArray(bundleStats)) {
bundleStats = {
children: bundleStats,
};
}

const isAssetIncluded = createAssetsFilter(excludeAssets);

// Preserve all children from the root bundleStats (e.g. for MultiCompiler or child compilations)
const allChildren = Array.isArray(bundleStats.children)
? [...bundleStats.children]
: [];
/** @type {WeakMap<StatsAsset, StatsCompilation>} */
const childAssetBundles = new WeakMap();

// Sometimes all the information is located in `children` array (e.g. problem in #10)
if (
(bundleStats.assets === null ||
bundleStats.assets === undefined ||
bundleStats.assets.length === 0) &&
bundleStats.children &&
bundleStats.children.length > 0
allChildren.length > 0
) {
const { children } = bundleStats;
[bundleStats] = bundleStats.children;
bundleStats = { ...allChildren[0] };
bundleStats.assets = Array.isArray(bundleStats.assets)
? [...bundleStats.assets]
: [];

// Sometimes if there are additional child chunks produced add them as child assets,
// leave the 1st one as that is considered the 'root' asset.
for (let i = 1; i < children.length; i++) {
for (const asset of children[i].assets || []) {
for (let i = 1; i < allChildren.length; i++) {
const child = allChildren[i];
Comment thread
valscion marked this conversation as resolved.
for (const asset of child?.assets || []) {
childAssetBundles.set(asset, child);
asset.isChild = true;
/** @type {StatsAsset[]} */
(bundleStats.assets).push(asset);
bundleStats.assets.push(asset);
}
}
} else if (bundleStats.children && bundleStats.children.length > 0) {
} else if (allChildren.length > 0) {
bundleStats.assets = [.../** @type {StatsAsset[]} */ (bundleStats.assets)];

// Sometimes if there are additional child chunks produced add them as child assets
for (const child of bundleStats.children) {
for (const asset of child.assets || []) {
for (const child of allChildren) {
for (const asset of child?.assets || []) {
childAssetBundles.set(asset, child);
asset.isChild = true;
/** @type {StatsAsset[]} */
(bundleStats.assets).push(asset);
bundleStats.assets.push(asset);
}
}
}
Expand Down Expand Up @@ -395,27 +419,35 @@ function getViewerData(bundleStats, bundleDir, opts) {
}
}

/** @typedef {{ size: number, parsedSize?: number, gzipSize?: number, brotliSize?: number, zstdSize?: number, modules: StatsModule[], tree: Folder }} Asset */
const rootChunkToInitialByEntrypoint =
getChunkToInitialByEntrypoint(bundleStats);

/** @typedef {{ size: number, parsedSize?: number, gzipSize?: number, brotliSize?: number, zstdSize?: number, modules: StatsModule[], tree: Folder, isInitialByEntrypoint: Record<string, boolean> }} Asset */

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

if (statAsset.isChild) {
// Preserve child-compilation matching because child assets use a different module list.
const assetBundles = getChildAssetBundles(bundleStats, statAsset.name);
const assetBundles =
childAssetBundles.get(statAsset) ||
getChildAssetBundles(allChildren, statAsset.name);
/** @type {StatsModule[]} */
const modules = assetBundles
? // @ts-expect-error TODO looks like we have a bug with child compilation parsing, need to add test cases
getBundleModules(assetBundles)
: [];
const modules = assetBundles ? getBundleModules(assetBundles) : [];
assetModules = modules.filter((statModule) =>
assetHasModule(statAsset, statModule),
);
isInitialByEntrypoint =
getChunkToInitialByEntrypoint(assetBundles)[statAsset.name] ?? {};
} else {
assetModules = /** @type {StatsModule[]} */ (
rootAssetModules.get(statAsset)
rootAssetModules.get(statAsset) || []
);
isInitialByEntrypoint =
rootChunkToInitialByEntrypoint[statAsset.name] ?? {};
}

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

const asset = (result[statAsset.name] = /** @type {Asset} */ ({
size: statAsset.size,
isInitialByEntrypoint,
}));
const assetSources =
bundlesSources && Object.hasOwn(bundlesSources, statAsset.name)
Expand Down Expand Up @@ -503,8 +536,6 @@ function getViewerData(bundleStats, bundleDir, opts) {
return result;
}, /** @type {Record<string, Asset>} */ ({}));

const chunkToInitialByEntrypoint = getChunkToInitialByEntrypoint(bundleStats);

return Object.entries(assets).map(([filename, asset]) => ({
label: filename,
isAsset: true,
Expand All @@ -518,7 +549,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
brotliSize: asset.brotliSize,
zstdSize: asset.zstdSize,
groups: Object.values(asset.tree.children).map((i) => i.toChartData()),
isInitialByEntrypoint: chunkToInitialByEntrypoint[filename] ?? {},
isInitialByEntrypoint: asset.isInitialByEntrypoint,
}));
}

Expand Down
Loading
Loading