UNPKG

8.27 kBJavaScriptView Raw
1const Path = require('path');
2const crypto = require('crypto');
3
4/**
5 * A Bundle represents an output file, containing multiple assets. Bundles can have
6 * child bundles, which are bundles that are loaded dynamically from this bundle.
7 * Child bundles are also produced when importing an asset of a different type from
8 * the bundle, e.g. importing a CSS file from JS.
9 */
10class Bundle {
11 constructor(type, name, parent, options = {}) {
12 this.type = type;
13 this.name = name;
14 this.parentBundle = parent;
15 this.entryAsset = null;
16 this.assets = new Set();
17 this.childBundles = new Set();
18 this.siblingBundles = new Set();
19 this.siblingBundlesMap = new Map();
20 this.offsets = new Map();
21 this.totalSize = 0;
22 this.bundleTime = 0;
23 this.isolated = options.isolated;
24 }
25
26 static createWithAsset(asset, parentBundle, options) {
27 let bundle = new Bundle(
28 asset.type,
29 Path.join(asset.options.outDir, asset.generateBundleName()),
30 parentBundle,
31 options
32 );
33
34 bundle.entryAsset = asset;
35 bundle.addAsset(asset);
36 return bundle;
37 }
38
39 addAsset(asset) {
40 asset.bundles.add(this);
41 this.assets.add(asset);
42 if (
43 this.type != 'map' &&
44 this.type == asset.type &&
45 asset.options.sourceMaps &&
46 asset.sourceMaps
47 ) {
48 this.getSiblingBundle('map').addAsset(asset);
49 }
50 }
51
52 removeAsset(asset) {
53 asset.bundles.delete(this);
54 this.assets.delete(asset);
55 }
56
57 addOffset(asset, line, column = 0) {
58 this.offsets.set(asset, [line, column]);
59 }
60
61 getOffset(asset) {
62 return this.offsets.get(asset) || [0, 0];
63 }
64
65 getSiblingBundle(type) {
66 if (!type || type === this.type) {
67 return this;
68 }
69
70 if (!this.siblingBundlesMap.has(type)) {
71 let bundle = new Bundle(
72 type,
73 Path.join(
74 Path.dirname(this.name),
75 // keep the original extension for source map files, so we have
76 // .js.map instead of just .map
77 type === 'map'
78 ? Path.basename(this.name) + '.' + type
79 : Path.basename(this.name, Path.extname(this.name)) + '.' + type
80 ),
81 this
82 );
83
84 this.childBundles.add(bundle);
85 this.siblingBundles.add(bundle);
86 this.siblingBundlesMap.set(type, bundle);
87 }
88
89 return this.siblingBundlesMap.get(type);
90 }
91
92 createChildBundle(entryAsset, options = {}) {
93 let bundle = Bundle.createWithAsset(entryAsset, this, options);
94 this.childBundles.add(bundle);
95 return bundle;
96 }
97
98 createSiblingBundle(entryAsset, options = {}) {
99 let bundle = this.createChildBundle(entryAsset, options);
100 this.siblingBundles.add(bundle);
101 return bundle;
102 }
103
104 get isEmpty() {
105 return this.assets.size === 0;
106 }
107
108 getBundleNameMap(contentHash, hashes = new Map()) {
109 if (this.name) {
110 let hashedName = this.getHashedBundleName(contentHash);
111 hashes.set(Path.basename(this.name), hashedName);
112 this.name = Path.join(Path.dirname(this.name), hashedName);
113 }
114
115 for (let child of this.childBundles.values()) {
116 child.getBundleNameMap(contentHash, hashes);
117 }
118
119 return hashes;
120 }
121
122 getHashedBundleName(contentHash) {
123 // If content hashing is enabled, generate a hash from all assets in the bundle.
124 // Otherwise, use a hash of the filename so it remains consistent across builds.
125
126 if (this.type == 'map') {
127 return this.parentBundle.getHashedBundleName(contentHash) + '.map';
128 }
129
130 let basename = Path.basename(this.name);
131
132 let ext = Path.extname(basename);
133 let hash = (contentHash
134 ? this.getHash()
135 : Path.basename(this.name, ext)
136 ).slice(-8);
137 let entryAsset = this;
138 while (!entryAsset.entryAsset && entryAsset.parentBundle) {
139 entryAsset = entryAsset.parentBundle;
140 }
141 entryAsset = entryAsset.entryAsset;
142 let name = Path.basename(entryAsset.name, Path.extname(entryAsset.name));
143 let isMainEntry = entryAsset.options.entryFiles[0] === entryAsset.name;
144 let isEntry =
145 entryAsset.options.entryFiles.includes(entryAsset.name) ||
146 Array.from(entryAsset.parentDeps).some(dep => dep.entry);
147
148 // If this is the main entry file, use the output file option as the name if provided.
149 if (isMainEntry && entryAsset.options.outFile) {
150 let extname = Path.extname(entryAsset.options.outFile);
151 if (extname) {
152 ext = this.entryAsset ? extname : ext;
153 name = Path.basename(entryAsset.options.outFile, extname);
154 } else {
155 name = entryAsset.options.outFile;
156 }
157 }
158
159 // If this is an entry asset, don't hash. Return a relative path
160 // from the main file so we keep the original file paths.
161 if (isEntry) {
162 return Path.join(
163 Path.relative(
164 entryAsset.options.rootDir,
165 Path.dirname(entryAsset.name)
166 ),
167 name + ext
168 ).replace(/\.\.(\/|\\)/g, '__$1');
169 }
170
171 // If this is an index file, use the parent directory name instead
172 // which is probably more descriptive.
173 if (name === 'index') {
174 name = Path.basename(Path.dirname(entryAsset.name));
175 }
176
177 // Add the content hash and extension.
178 return name + '.' + hash + ext;
179 }
180
181 async package(bundler, oldHashes, newHashes = new Map()) {
182 let promises = [];
183 let mappings = [];
184
185 if (!this.isEmpty) {
186 let hash = this.getHash();
187 newHashes.set(this.name, hash);
188
189 if (!oldHashes || oldHashes.get(this.name) !== hash) {
190 promises.push(this._package(bundler));
191 }
192 }
193
194 for (let bundle of this.childBundles.values()) {
195 if (bundle.type === 'map') {
196 mappings.push(bundle);
197 } else {
198 promises.push(bundle.package(bundler, oldHashes, newHashes));
199 }
200 }
201
202 await Promise.all(promises);
203 for (let bundle of mappings) {
204 await bundle.package(bundler, oldHashes, newHashes);
205 }
206 return newHashes;
207 }
208
209 async _package(bundler) {
210 let Packager = bundler.packagers.get(this.type);
211 let packager = new Packager(this, bundler);
212
213 let startTime = Date.now();
214 await packager.setup();
215 await packager.start();
216
217 let included = new Set();
218 for (let asset of this.assets) {
219 await this._addDeps(asset, packager, included);
220 }
221
222 await packager.end();
223
224 this.totalSize = packager.getSize();
225
226 let assetArray = Array.from(this.assets);
227 let assetStartTime =
228 this.type === 'map'
229 ? 0
230 : assetArray.sort((a, b) => a.startTime - b.startTime)[0].startTime;
231 let assetEndTime =
232 this.type === 'map'
233 ? 0
234 : assetArray.sort((a, b) => b.endTime - a.endTime)[0].endTime;
235 let packagingTime = Date.now() - startTime;
236 this.bundleTime = assetEndTime - assetStartTime + packagingTime;
237 }
238
239 async _addDeps(asset, packager, included) {
240 if (!this.assets.has(asset) || included.has(asset)) {
241 return;
242 }
243
244 included.add(asset);
245
246 for (let depAsset of asset.depAssets.values()) {
247 await this._addDeps(depAsset, packager, included);
248 }
249
250 await packager.addAsset(asset);
251
252 const assetSize = packager.getSize() - this.totalSize;
253 if (assetSize > 0) {
254 this.addAssetSize(asset, assetSize);
255 }
256 }
257
258 addAssetSize(asset, size) {
259 asset.bundledSize = size;
260 this.totalSize += size;
261 }
262
263 getParents() {
264 let parents = [];
265 let bundle = this;
266
267 while (bundle) {
268 parents.push(bundle);
269 bundle = bundle.parentBundle;
270 }
271
272 return parents;
273 }
274
275 findCommonAncestor(bundle) {
276 // Get a list of parent bundles going up to the root
277 let ourParents = this.getParents();
278 let theirParents = bundle.getParents();
279
280 // Start from the root bundle, and find the first bundle that's different
281 let a = ourParents.pop();
282 let b = theirParents.pop();
283 let last;
284 while (a === b && ourParents.length > 0 && theirParents.length > 0) {
285 last = a;
286 a = ourParents.pop();
287 b = theirParents.pop();
288 }
289
290 if (a === b) {
291 // One bundle descended from the other
292 return a;
293 }
294
295 return last;
296 }
297
298 getHash() {
299 let hash = crypto.createHash('md5');
300 for (let asset of this.assets) {
301 hash.update(asset.hash);
302 }
303
304 return hash.digest('hex');
305 }
306}
307
308module.exports = Bundle;