1 |
|
2 |
|
3 | import type {FilePath, NamedBundle} from '@parcel/types';
|
4 | import type {FileSystem} from '@parcel/fs';
|
5 | import SourceMap from '@parcel/source-map';
|
6 | import nullthrows from 'nullthrows';
|
7 | import path from 'path';
|
8 | import {loadSourceMapUrl} from './';
|
9 |
|
10 | export type AssetStats = {|
|
11 | filePath: string,
|
12 | size: number,
|
13 | originalSize: number,
|
14 | time: number,
|
15 | |};
|
16 |
|
17 | export type BundleStats = {|
|
18 | filePath: string,
|
19 | size: number,
|
20 | time: number,
|
21 | assets: Array<AssetStats>,
|
22 | |};
|
23 |
|
24 | export type BuildMetrics = {|
|
25 | bundles: Array<BundleStats>,
|
26 | |};
|
27 |
|
28 | async function getSourcemapSizes(
|
29 | filePath: FilePath,
|
30 | fs: FileSystem,
|
31 | projectRoot: FilePath,
|
32 | ): Promise<?Map<string, number>> {
|
33 | let bundleContents = await fs.readFile(filePath, 'utf-8');
|
34 | let mapUrlData = await loadSourceMapUrl(fs, filePath, bundleContents);
|
35 | if (!mapUrlData) {
|
36 | return null;
|
37 | }
|
38 |
|
39 | let rawMap = mapUrlData.map;
|
40 | let sourceMap = new SourceMap();
|
41 | sourceMap.addRawMappings(rawMap);
|
42 | let parsedMapData = sourceMap.getMap();
|
43 |
|
44 | if (parsedMapData.mappings.length > 2) {
|
45 | let sources = parsedMapData.sources.map(s =>
|
46 | path.normalize(path.join(projectRoot, s)),
|
47 | );
|
48 | let currLine = 1;
|
49 | let currColumn = 0;
|
50 | let currMappingIndex = 0;
|
51 | let currMapping = parsedMapData.mappings[currMappingIndex];
|
52 | let nextMapping = parsedMapData.mappings[currMappingIndex + 1];
|
53 | let sourceSizes = new Array(sources.length).fill(0);
|
54 | let unknownOrigin: number = 0;
|
55 | for (let i = 0; i < bundleContents.length; i++) {
|
56 | let character = bundleContents[i];
|
57 |
|
58 | while (
|
59 | nextMapping &&
|
60 | nextMapping.generated.line === currLine &&
|
61 | nextMapping.generated.column <= currColumn
|
62 | ) {
|
63 | currMappingIndex++;
|
64 | currMapping = parsedMapData.mappings[currMappingIndex];
|
65 | nextMapping = parsedMapData.mappings[currMappingIndex + 1];
|
66 | }
|
67 |
|
68 | let currentSource = currMapping.source;
|
69 | let charSize = Buffer.byteLength(character, 'utf8');
|
70 | if (
|
71 | currentSource != null &&
|
72 | currMapping.generated.line === currLine &&
|
73 | currMapping.generated.column <= currColumn
|
74 | ) {
|
75 | sourceSizes[currentSource] += charSize;
|
76 | } else {
|
77 | unknownOrigin += charSize;
|
78 | }
|
79 |
|
80 | if (character === '\n') {
|
81 | currColumn = 0;
|
82 | currLine++;
|
83 | } else {
|
84 | currColumn++;
|
85 | }
|
86 | }
|
87 |
|
88 | let sizeMap = new Map();
|
89 | for (let i = 0; i < sourceSizes.length; i++) {
|
90 | sizeMap.set(sources[i], sourceSizes[i]);
|
91 | }
|
92 |
|
93 | sizeMap.set('', unknownOrigin);
|
94 |
|
95 | return sizeMap;
|
96 | }
|
97 | }
|
98 |
|
99 | async function createBundleStats(
|
100 | bundle: NamedBundle,
|
101 | fs: FileSystem,
|
102 | projectRoot: FilePath,
|
103 | ) {
|
104 | let filePath = bundle.filePath;
|
105 | let sourcemapSizes = await getSourcemapSizes(filePath, fs, projectRoot);
|
106 |
|
107 | let assets: Map<string, AssetStats> = new Map();
|
108 | bundle.traverseAssets(asset => {
|
109 | let filePath = path.normalize(asset.filePath);
|
110 | assets.set(filePath, {
|
111 | filePath,
|
112 | size: asset.stats.size,
|
113 | originalSize: asset.stats.size,
|
114 | time: asset.stats.time,
|
115 | });
|
116 | });
|
117 |
|
118 | let assetsReport: Array<AssetStats> = [];
|
119 | if (sourcemapSizes && sourcemapSizes.size) {
|
120 | assetsReport = Array.from(sourcemapSizes.keys()).map((filePath: string) => {
|
121 | let foundSize = sourcemapSizes.get(filePath) || 0;
|
122 | let stats = assets.get(filePath) || {
|
123 | filePath,
|
124 | size: foundSize,
|
125 | originalSize: foundSize,
|
126 | time: 0,
|
127 | };
|
128 |
|
129 | return {
|
130 | ...stats,
|
131 | size: foundSize,
|
132 | };
|
133 | });
|
134 | } else {
|
135 | assetsReport = Array.from(assets.values());
|
136 | }
|
137 |
|
138 | return {
|
139 | filePath: nullthrows(bundle.filePath),
|
140 | size: bundle.stats.size,
|
141 | time: bundle.stats.time,
|
142 | assets: assetsReport.sort((a, b) => b.size - a.size),
|
143 | };
|
144 | }
|
145 |
|
146 | export default async function generateBuildMetrics(
|
147 | bundles: Array<NamedBundle>,
|
148 | fs: FileSystem,
|
149 | projectRoot: FilePath,
|
150 | ): Promise<BuildMetrics> {
|
151 | bundles.sort((a, b) => b.stats.size - a.stats.size).filter(b => !!b.filePath);
|
152 |
|
153 | return {
|
154 | bundles: (
|
155 | await Promise.all(bundles.map(b => createBundleStats(b, fs, projectRoot)))
|
156 | ).filter(e => !!e),
|
157 | };
|
158 | }
|