From c35bda352f308b527532464d10635c61ab254144 Mon Sep 17 00:00:00 2001 From: th0r Date: Thu, 5 Nov 2020 19:12:57 +0300 Subject: [PATCH] Properly parse Webpack 5 entry modules --- src/analyzer.js | 55 ++++++++++++++++++++++++++++++++++++++++------- src/parseUtils.js | 48 ++++++++++++++++++++++++++++++----------- 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/analyzer.js b/src/analyzer.js index e7a84aa5..39bc24a2 100644 --- a/src/analyzer.js +++ b/src/analyzer.js @@ -76,7 +76,7 @@ function getViewerData(bundleStats, bundleDir, opts) { continue; } - bundlesSources[statAsset.name] = bundleInfo.src; + bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc'); _.assign(parsedModules, bundleInfo.modules); } @@ -94,19 +94,48 @@ function getViewerData(bundleStats, bundleDir, opts) { const asset = result[statAsset.name] = _.pick(statAsset, 'size'); if (bundlesSources && _.has(bundlesSources, statAsset.name)) { - asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name]); - asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]); + asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name].src); + asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name].src); } // Picking modules from current bundle script - asset.modules = _(modules) - .filter(statModule => assetHasModule(statAsset, statModule)) - .each(statModule => { - if (parsedModules) { + const assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule)); + + // Adding parsed sources + if (parsedModules) { + const unparsedEntryModules = []; + + for (const statModule of assetModules) { + if (parsedModules[statModule.id]) { statModule.parsedSrc = parsedModules[statModule.id]; + } else if (isEntryModule(statModule)) { + unparsedEntryModules.push(statModule); } - }); + } + + // Webpack 5 changed bundle format and now entry modules are concatenated and located at the end on the it. + // Because of this they basically become a concatenated module, for which we can't even precisely determine its + // parsed source as it's located in the same scope as all Webpack runtime helpers. + if (unparsedEntryModules.length) { + if (unparsedEntryModules.length === 1) { + // So if there is only one entry we consider its parsed source to be all the bundle code excluding code + // from parsed modules. + unparsedEntryModules[0].parsedSrc = bundlesSources[statAsset.name].runtimeSrc; + } else { + // If there are multiple entry points we move all of them under synthetic concatenated module. + _.pullAll(assetModules, unparsedEntryModules); + assetModules.unshift({ + identifier: './entry modules', + name: './entry modules', + modules: unparsedEntryModules, + size: unparsedEntryModules.reduce((totalSize, module) => totalSize + module.size, 0), + parsedSrc: bundlesSources[statAsset.name].runtimeSrc + }); + } + } + } + asset.modules = assetModules; asset.tree = createModulesTree(asset.modules); }, {}); @@ -148,6 +177,8 @@ function getBundleModules(bundleStats) { .compact() .flatten() .uniqBy('id') + // Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5) + .reject(isRuntimeModule) .value(); } @@ -158,6 +189,14 @@ function assetHasModule(statAsset, statModule) { ); } +function isEntryModule(statModule) { + return statModule.depth === 0; +} + +function isRuntimeModule(statModule) { + return statModule.moduleType === 'runtime'; +} + function createModulesTree(modules) { const root = new Folder('.'); diff --git a/src/parseUtils.js b/src/parseUtils.js index 1d775ae3..bce9878c 100644 --- a/src/parseUtils.js +++ b/src/parseUtils.js @@ -45,17 +45,19 @@ function parseBundle(bundlePath) { // ...nor parameters fn.callee.params.length === 0 ) { - // Modules are stored in the very first variable as hash - const {body} = fn.callee.body; - - if ( - body.length && - body[0].type === 'VariableDeclaration' && - body[0].declarations.length && - body[0].declarations[0].type === 'VariableDeclarator' && - body[0].declarations[0].init.type === 'ObjectExpression' - ) { - state.locations = getModulesLocations(body[0].declarations[0].init); + // Modules are stored in the very first variable declaration as hash + const firstVariableDeclaration = fn.callee.body.body.find(node => node.type === 'VariableDeclaration'); + + if (firstVariableDeclaration) { + for (const declaration of firstVariableDeclaration.declarations) { + if (declaration.init) { + state.locations = getModulesLocations(declaration.init); + + if (state.locations) { + break; + } + } + } } } } @@ -66,6 +68,7 @@ function parseBundle(bundlePath) { state.expressionStatementDepth--; }, + AssignmentExpression(node, state) { if (state.locations) return; @@ -82,6 +85,7 @@ function parseBundle(bundlePath) { state.locations = getModulesLocations(right); } }, + CallExpression(node, state, c) { if (state.locations) return; @@ -147,11 +151,31 @@ function parseBundle(bundlePath) { } return { + modules, src: content, - modules + runtimeSrc: getBundleRuntime(content, walkState.locations) }; } +/** + * Returns bundle source except modules + */ +function getBundleRuntime(content, modulesLocations) { + const sortedLocations = _(modulesLocations) + .values() + .sortBy('start'); + + let result = ''; + let lastIndex = 0; + + for (const {start, end} of sortedLocations) { + result += content.slice(lastIndex, start); + lastIndex = end; + } + + return result + content.slice(lastIndex, content.length); +} + function isIIFE(node) { return ( node.type === 'ExpressionStatement' &&