UNPKG

7.47 kBJavaScriptView Raw
1const fs = require('fs');
2const path = require('path');
3
4const _ = require('lodash');
5const gzipSize = require('gzip-size');
6
7const Logger = require('./Logger');
8const Folder = require('./tree/Folder').default;
9const {parseBundle} = require('./parseUtils');
10const {createAssetsFilter} = require('./utils');
11
12const FILENAME_QUERY_REGEXP = /\?.*$/u;
13const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu;
14
15module.exports = {
16 getViewerData,
17 readStatsFromFile
18};
19
20function getViewerData(bundleStats, bundleDir, opts) {
21 const {
22 logger = new Logger(),
23 excludeAssets = null
24 } = opts || {};
25
26 const isAssetIncluded = createAssetsFilter(excludeAssets);
27
28 // Sometimes all the information is located in `children` array (e.g. problem in #10)
29 if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
30 const {children} = bundleStats;
31 bundleStats = bundleStats.children[0];
32 // Sometimes if there are additional child chunks produced add them as child assets,
33 // leave the 1st one as that is considered the 'root' asset.
34 for (let i = 1; i < children.length; i++) {
35 children[i].assets.forEach((asset) => {
36 asset.isChild = true;
37 bundleStats.assets.push(asset);
38 });
39 }
40 } else if (!_.isEmpty(bundleStats.children)) {
41 // Sometimes if there are additional child chunks produced add them as child assets
42 bundleStats.children.forEach((child) => {
43 child.assets.forEach((asset) => {
44 asset.isChild = true;
45 bundleStats.assets.push(asset);
46 });
47 });
48 }
49
50 // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
51 bundleStats.assets = bundleStats.assets.filter(asset => {
52 // Filter out non 'asset' type asset if type is provided (Webpack 5 add a type to indicate asset types)
53 if (asset.type && asset.type !== 'asset') {
54 return false;
55 }
56
57 // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
58 // See #22
59 asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
60
61 return FILENAME_EXTENSIONS.test(asset.name) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
62 });
63
64 // Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
65 let bundlesSources = null;
66 let parsedModules = null;
67
68 if (bundleDir) {
69 bundlesSources = {};
70 parsedModules = {};
71
72 for (const statAsset of bundleStats.assets) {
73 const assetFile = path.join(bundleDir, statAsset.name);
74 let bundleInfo;
75
76 try {
77 bundleInfo = parseBundle(assetFile);
78 } catch (err) {
79 const msg = (err.code === 'ENOENT') ? 'no such file' : err.message;
80 logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`);
81 continue;
82 }
83
84 bundlesSources[statAsset.name] = _.pick(bundleInfo, 'src', 'runtimeSrc');
85 Object.assign(parsedModules, bundleInfo.modules);
86 }
87
88 if (_.isEmpty(bundlesSources)) {
89 bundlesSources = null;
90 parsedModules = null;
91 logger.warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n');
92 }
93 }
94
95 const assets = bundleStats.assets.reduce((result, statAsset) => {
96 // If asset is a childAsset, then calculate appropriate bundle modules by looking through stats.children
97 const assetBundles = statAsset.isChild ? getChildAssetBundles(bundleStats, statAsset.name) : bundleStats;
98 const modules = assetBundles ? getBundleModules(assetBundles) : [];
99 const asset = result[statAsset.name] = _.pick(statAsset, 'size');
100 const assetSources = bundlesSources && _.has(bundlesSources, statAsset.name) ?
101 bundlesSources[statAsset.name] : null;
102
103 if (assetSources) {
104 asset.parsedSize = Buffer.byteLength(assetSources.src);
105 asset.gzipSize = gzipSize.sync(assetSources.src);
106 }
107
108 // Picking modules from current bundle script
109 const assetModules = modules.filter(statModule => assetHasModule(statAsset, statModule));
110
111 // Adding parsed sources
112 if (parsedModules) {
113 const unparsedEntryModules = [];
114
115 for (const statModule of assetModules) {
116 if (parsedModules[statModule.id]) {
117 statModule.parsedSrc = parsedModules[statModule.id];
118 } else if (isEntryModule(statModule)) {
119 unparsedEntryModules.push(statModule);
120 }
121 }
122
123 // Webpack 5 changed bundle format and now entry modules are concatenated and located at the end of it.
124 // Because of this they basically become a concatenated module, for which we can't even precisely determine its
125 // parsed source as it's located in the same scope as all Webpack runtime helpers.
126 if (unparsedEntryModules.length && assetSources) {
127 if (unparsedEntryModules.length === 1) {
128 // So if there is only one entry we consider its parsed source to be all the bundle code excluding code
129 // from parsed modules.
130 unparsedEntryModules[0].parsedSrc = assetSources.runtimeSrc;
131 } else {
132 // If there are multiple entry points we move all of them under synthetic concatenated module.
133 _.pullAll(assetModules, unparsedEntryModules);
134 assetModules.unshift({
135 identifier: './entry modules',
136 name: './entry modules',
137 modules: unparsedEntryModules,
138 size: unparsedEntryModules.reduce((totalSize, module) => totalSize + module.size, 0),
139 parsedSrc: assetSources.runtimeSrc
140 });
141 }
142 }
143 }
144
145 asset.modules = assetModules;
146 asset.tree = createModulesTree(asset.modules);
147 return result;
148 }, {});
149
150 return Object.entries(assets).map(([filename, asset]) => ({
151 label: filename,
152 isAsset: true,
153 // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
154 // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
155 // be the size of minified bundle.
156 // Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
157 statSize: asset.tree.size || asset.size,
158 parsedSize: asset.parsedSize,
159 gzipSize: asset.gzipSize,
160 groups: _.invokeMap(asset.tree.children, 'toChartData')
161 }));
162}
163
164function readStatsFromFile(filename) {
165 return JSON.parse(
166 fs.readFileSync(filename, 'utf8')
167 );
168}
169
170function getChildAssetBundles(bundleStats, assetName) {
171 return (bundleStats.children || []).find((c) =>
172 _(c.assetsByChunkName)
173 .values()
174 .flatten()
175 .includes(assetName)
176 );
177}
178
179function getBundleModules(bundleStats) {
180 return _(bundleStats.chunks)
181 .map('modules')
182 .concat(bundleStats.modules)
183 .compact()
184 .flatten()
185 .uniqBy('id')
186 // Filtering out Webpack's runtime modules as they don't have ids and can't be parsed (introduced in Webpack 5)
187 .reject(isRuntimeModule)
188 .value();
189}
190
191function assetHasModule(statAsset, statModule) {
192 // Checking if this module is the part of asset chunks
193 return statModule.chunks.some(moduleChunk =>
194 statAsset.chunks.includes(moduleChunk)
195 );
196}
197
198function isEntryModule(statModule) {
199 return statModule.depth === 0;
200}
201
202function isRuntimeModule(statModule) {
203 return statModule.moduleType === 'runtime';
204}
205
206function createModulesTree(modules) {
207 const root = new Folder('.');
208
209 modules.forEach(module => root.addModule(module));
210 root.mergeNestedFolders();
211
212 return root;
213}