UNPKG

4.23 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('../lib/tree');
9const { parseBundle } = require('../lib/parseUtils');
10
11const FILENAME_QUERY_REGEXP = /\?.*$/;
12const MULTI_MODULE_REGEXP = /^multi /;
13
14module.exports = {
15 getViewerData,
16 readStatsFromFile
17};
18
19function getViewerData(bundleStats, bundleDir, opts) {
20 const {
21 logger = new Logger()
22 } = opts || {};
23
24 // Sometimes all the information is located in `children` array (e.g. problem in #10)
25 if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
26 bundleStats = bundleStats.children[0];
27 }
28
29 // Picking only `*.js` assets from bundle that has non-empty `chunks` array
30 bundleStats.assets = _.filter(bundleStats.assets, asset => {
31 // Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
32 // See #22
33 asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
34
35 return _.endsWith(asset.name, '.js') && !_.isEmpty(asset.chunks);
36 });
37
38 // Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
39 let bundlesSources = null;
40 let parsedModules = null;
41
42 if (bundleDir) {
43 bundlesSources = {};
44 parsedModules = {};
45
46 for (const statAsset of bundleStats.assets) {
47 const assetFile = path.join(bundleDir, statAsset.name);
48 let bundleInfo;
49
50 try {
51 bundleInfo = parseBundle(assetFile);
52 } catch (err) {
53 bundleInfo = null;
54 }
55
56 if (!bundleInfo) {
57 logger.warn(
58 `\nCouldn't parse bundle asset "${assetFile}".\n` +
59 'Analyzer will use module sizes from stats file.\n'
60 );
61 parsedModules = null;
62 bundlesSources = null;
63 break;
64 }
65
66 bundlesSources[statAsset.name] = bundleInfo.src;
67 _.assign(parsedModules, bundleInfo.modules);
68 }
69 }
70
71 const assets = _.transform(bundleStats.assets, (result, statAsset) => {
72 const asset = result[statAsset.name] = _.pick(statAsset, 'size');
73
74 if (bundlesSources) {
75 asset.parsedSize = bundlesSources[statAsset.name].length;
76 asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]);
77 }
78
79 // Picking modules from current bundle script
80 asset.modules = _(bundleStats.modules)
81 .filter(statModule => assetHasModule(statAsset, statModule))
82 .each(statModule => {
83 if (parsedModules) {
84 statModule.parsedSrc = parsedModules[statModule.id];
85 }
86 });
87
88 asset.tree = createModulesTree(asset.modules);
89 }, {});
90
91 return _.transform(assets, (result, asset, filename) => {
92 result.push({
93 label: filename,
94 // Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
95 // In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
96 // be the size of minified bundle.
97 statSize: asset.tree.size,
98 parsedSize: asset.parsedSize,
99 gzipSize: asset.gzipSize,
100 groups: _.invokeMap(asset.tree.children, 'toChartData')
101 });
102 }, []);
103}
104
105function readStatsFromFile(filename) {
106 return JSON.parse(
107 fs.readFileSync(filename, 'utf8')
108 );
109}
110
111function assetHasModule(statAsset, statModule) {
112 return _.some(statModule.chunks, moduleChunk =>
113 _.includes(statAsset.chunks, moduleChunk)
114 );
115}
116
117function createModulesTree(modules) {
118 const root = new Folder('.');
119
120 _.each(modules, module => {
121 const path = getModulePath(module);
122
123 if (path) {
124 root.addModuleByPath(path, module);
125 }
126 });
127
128 return root;
129}
130
131function getModulePath(module) {
132 if (MULTI_MODULE_REGEXP.test(module.identifier)) {
133 return [module.identifier];
134 }
135
136 const parsedPath = _
137 // Removing loaders from module path: they're joined by `!` and the last part is a raw module path
138 .last(module.name.split('!'))
139 // Splitting module path into parts
140 .split('/')
141 // Removing first `.`
142 .slice(1)
143 // Replacing `~` with `node_modules`
144 .map(part => (part === '~') ? 'node_modules' : part);
145
146 return parsedPath.length ? parsedPath : null;
147}