Skip to content

Commit c35bda3

Browse files
committed
Properly parse Webpack 5 entry modules
1 parent 7bbe89f commit c35bda3

File tree

2 files changed

+83
-20
lines changed

2 files changed

+83
-20
lines changed

src/analyzer.js

+47-8
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function getViewerData(bundleStats, bundleDir, opts) {
7676
continue;
7777
}
7878

79-
bundlesSources[statAsset.name] = bundleInfo.src;
79+
bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
8080
_.assign(parsedModules, bundleInfo.modules);
8181
}
8282

@@ -94,19 +94,48 @@ function getViewerData(bundleStats, bundleDir, opts) {
9494
const asset = result[statAsset.name] = _.pick(statAsset, 'size');
9595

9696
if (bundlesSources && _.has(bundlesSources, statAsset.name)) {
97-
asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name]);
98-
asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]);
97+
asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name].src);
98+
asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name].src);
9999
}
100100

101101
// Picking modules from current bundle script
102-
asset.modules = _(modules)
103-
.filter(statModule => assetHasModule(statAsset, statModule))
104-
.each(statModule => {
105-
if (parsedModules) {
102+
const assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule));
103+
104+
// Adding parsed sources
105+
if (parsedModules) {
106+
const unparsedEntryModules = [];
107+
108+
for (const statModule of assetModules) {
109+
if (parsedModules[statModule.id]) {
106110
statModule.parsedSrc = parsedModules[statModule.id];
111+
} else if (isEntryModule(statModule)) {
112+
unparsedEntryModules.push(statModule);
107113
}
108-
});
114+
}
115+
116+
// Webpack 5 changed bundle format and now entry modules are concatenated and located at the end on the it.
117+
// Because of this they basically become a concatenated module, for which we can't even precisely determine its
118+
// parsed source as it's located in the same scope as all Webpack runtime helpers.
119+
if (unparsedEntryModules.length) {
120+
if (unparsedEntryModules.length === 1) {
121+
// So if there is only one entry we consider its parsed source to be all the bundle code excluding code
122+
// from parsed modules.
123+
unparsedEntryModules[0].parsedSrc = bundlesSources[statAsset.name].runtimeSrc;
124+
} else {
125+
// If there are multiple entry points we move all of them under synthetic concatenated module.
126+
_.pullAll(assetModules, unparsedEntryModules);
127+
assetModules.unshift({
128+
identifier: './entry modules',
129+
name: './entry modules',
130+
modules: unparsedEntryModules,
131+
size: unparsedEntryModules.reduce((totalSize, module) => totalSize + module.size, 0),
132+
parsedSrc: bundlesSources[statAsset.name].runtimeSrc
133+
});
134+
}
135+
}
136+
}
109137

138+
asset.modules = assetModules;
110139
asset.tree = createModulesTree(asset.modules);
111140
}, {});
112141

@@ -148,6 +177,8 @@ function getBundleModules(bundleStats) {
148177
.compact()
149178
.flatten()
150179
.uniqBy('id')
180+
// Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
181+
.reject(isRuntimeModule)
151182
.value();
152183
}
153184

@@ -158,6 +189,14 @@ function assetHasModule(statAsset, statModule) {
158189
);
159190
}
160191

192+
function isEntryModule(statModule) {
193+
return statModule.depth === 0;
194+
}
195+
196+
function isRuntimeModule(statModule) {
197+
return statModule.moduleType === 'runtime';
198+
}
199+
161200
function createModulesTree(modules) {
162201
const root = new Folder('.');
163202

src/parseUtils.js

+36-12
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,19 @@ function parseBundle(bundlePath) {
4545
// ...nor parameters
4646
fn.callee.params.length === 0
4747
) {
48-
// Modules are stored in the very first variable as hash
49-
const {body} = fn.callee.body;
50-
51-
if (
52-
body.length &&
53-
body[0].type === 'VariableDeclaration' &&
54-
body[0].declarations.length &&
55-
body[0].declarations[0].type === 'VariableDeclarator' &&
56-
body[0].declarations[0].init.type === 'ObjectExpression'
57-
) {
58-
state.locations = getModulesLocations(body[0].declarations[0].init);
48+
// Modules are stored in the very first variable declaration as hash
49+
const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === 'VariableDeclaration');
50+
51+
if (firstVariableDeclaration) {
52+
for (const declaration of firstVariableDeclaration.declarations) {
53+
if (declaration.init) {
54+
state.locations = getModulesLocations(declaration.init);
55+
56+
if (state.locations) {
57+
break;
58+
}
59+
}
60+
}
5961
}
6062
}
6163
}
@@ -66,6 +68,7 @@ function parseBundle(bundlePath) {
6668

6769
state.expressionStatementDepth--;
6870
},
71+
6972
AssignmentExpression(node, state) {
7073
if (state.locations) return;
7174

@@ -82,6 +85,7 @@ function parseBundle(bundlePath) {
8285
state.locations = getModulesLocations(right);
8386
}
8487
},
88+
8589
CallExpression(node, state, c) {
8690
if (state.locations) return;
8791

@@ -147,11 +151,31 @@ function parseBundle(bundlePath) {
147151
}
148152

149153
return {
154+
modules,
150155
src: content,
151-
modules
156+
runtimeSrc: getBundleRuntime(content, walkState.locations)
152157
};
153158
}
154159

160+
/**
161+
* Returns bundle source except modules
162+
*/
163+
function getBundleRuntime(content, modulesLocations) {
164+
const sortedLocations = _(modulesLocations)
165+
.values()
166+
.sortBy('start');
167+
168+
let result = '';
169+
let lastIndex = 0;
170+
171+
for (const {start, end} of sortedLocations) {
172+
result += content.slice(lastIndex, start);
173+
lastIndex = end;
174+
}
175+
176+
return result + content.slice(lastIndex, content.length);
177+
}
178+
155179
function isIIFE(node) {
156180
return (
157181
node.type === 'ExpressionStatement' &&

0 commit comments

Comments
 (0)