UNPKG

6.74 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) {
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 }
24
25 static createWithAsset(asset, parentBundle) {
26 let bundle = new Bundle(
27 asset.type,
28 Path.join(asset.options.outDir, asset.generateBundleName()),
29 parentBundle
30 );
31
32 bundle.entryAsset = asset;
33 bundle.addAsset(asset);
34 return bundle;
35 }
36
37 addAsset(asset) {
38 asset.bundles.add(this);
39 this.assets.add(asset);
40 }
41
42 removeAsset(asset) {
43 asset.bundles.delete(this);
44 this.assets.delete(asset);
45 }
46
47 addOffset(asset, line) {
48 this.offsets.set(asset, line);
49 }
50
51 getOffset(asset) {
52 return this.offsets.get(asset) || 0;
53 }
54
55 getSiblingBundle(type) {
56 if (!type || type === this.type) {
57 return this;
58 }
59
60 if (!this.siblingBundlesMap.has(type)) {
61 let bundle = new Bundle(
62 type,
63 Path.join(
64 Path.dirname(this.name),
65 Path.basename(this.name, Path.extname(this.name)) + '.' + type
66 ),
67 this
68 );
69
70 this.childBundles.add(bundle);
71 this.siblingBundles.add(bundle);
72 this.siblingBundlesMap.set(type, bundle);
73 }
74
75 return this.siblingBundlesMap.get(type);
76 }
77
78 createChildBundle(entryAsset) {
79 let bundle = Bundle.createWithAsset(entryAsset, this);
80 this.childBundles.add(bundle);
81 return bundle;
82 }
83
84 createSiblingBundle(entryAsset) {
85 let bundle = this.createChildBundle(entryAsset);
86 this.siblingBundles.add(bundle);
87 return bundle;
88 }
89
90 get isEmpty() {
91 return this.assets.size === 0;
92 }
93
94 getBundleNameMap(contentHash, hashes = new Map()) {
95 let hashedName = this.getHashedBundleName(contentHash);
96 hashes.set(Path.basename(this.name), hashedName);
97 this.name = Path.join(Path.dirname(this.name), hashedName);
98
99 for (let child of this.childBundles.values()) {
100 child.getBundleNameMap(contentHash, hashes);
101 }
102
103 return hashes;
104 }
105
106 getHashedBundleName(contentHash) {
107 // If content hashing is enabled, generate a hash from all assets in the bundle.
108 // Otherwise, use a hash of the filename so it remains consistent across builds.
109 let ext = Path.extname(this.name);
110 let hash = (contentHash
111 ? this.getHash()
112 : Path.basename(this.name, ext)
113 ).slice(-8);
114 let entryAsset = this.entryAsset || this.parentBundle.entryAsset;
115 let name = Path.basename(entryAsset.name, Path.extname(entryAsset.name));
116 let isMainEntry = entryAsset.name === entryAsset.options.mainFile;
117 let isEntry =
118 isMainEntry || Array.from(entryAsset.parentDeps).some(dep => dep.entry);
119
120 // If this is the main entry file, use the output file option as the name if provided.
121 if (isMainEntry && entryAsset.options.outFile) {
122 name = entryAsset.options.outFile;
123 }
124
125 // If this is an entry asset, don't hash. Return a relative path
126 // from the main file so we keep the original file paths.
127 if (isEntry) {
128 return Path.join(
129 Path.relative(
130 Path.dirname(entryAsset.options.mainFile),
131 Path.dirname(entryAsset.name)
132 ),
133 name + ext
134 );
135 }
136
137 // If this is an index file, use the parent directory name instead
138 // which is probably more descriptive.
139 if (name === 'index') {
140 name = Path.basename(Path.dirname(entryAsset.name));
141 }
142
143 // Add the content hash and extension.
144 return name + '.' + hash + ext;
145 }
146
147 async package(bundler, oldHashes, newHashes = new Map()) {
148 if (this.isEmpty) {
149 return newHashes;
150 }
151
152 let hash = this.getHash();
153 newHashes.set(this.name, hash);
154
155 let promises = [];
156 let mappings = [];
157 if (!oldHashes || oldHashes.get(this.name) !== hash) {
158 promises.push(this._package(bundler));
159 }
160
161 for (let bundle of this.childBundles.values()) {
162 if (bundle.type === 'map') {
163 mappings.push(bundle);
164 } else {
165 promises.push(bundle.package(bundler, oldHashes, newHashes));
166 }
167 }
168
169 await Promise.all(promises);
170 for (let bundle of mappings) {
171 await bundle.package(bundler, oldHashes, newHashes);
172 }
173 return newHashes;
174 }
175
176 async _package(bundler) {
177 let Packager = bundler.packagers.get(this.type);
178 let packager = new Packager(this, bundler);
179
180 let startTime = Date.now();
181 await packager.setup();
182 await packager.start();
183
184 let included = new Set();
185 for (let asset of this.assets) {
186 await this._addDeps(asset, packager, included);
187 }
188
189 await packager.end();
190
191 this.bundleTime = Date.now() - startTime;
192 for (let asset of this.assets) {
193 this.bundleTime += asset.buildTime;
194 }
195 }
196
197 async _addDeps(asset, packager, included) {
198 if (!this.assets.has(asset) || included.has(asset)) {
199 return;
200 }
201
202 included.add(asset);
203
204 for (let depAsset of asset.depAssets.values()) {
205 await this._addDeps(depAsset, packager, included);
206 }
207
208 await packager.addAsset(asset);
209 this.addAssetSize(asset, packager.getSize() - this.totalSize);
210 }
211
212 addAssetSize(asset, size) {
213 asset.bundledSize = size;
214 this.totalSize += size;
215 }
216
217 getParents() {
218 let parents = [];
219 let bundle = this;
220
221 while (bundle) {
222 parents.push(bundle);
223 bundle = bundle.parentBundle;
224 }
225
226 return parents;
227 }
228
229 findCommonAncestor(bundle) {
230 // Get a list of parent bundles going up to the root
231 let ourParents = this.getParents();
232 let theirParents = bundle.getParents();
233
234 // Start from the root bundle, and find the first bundle that's different
235 let a = ourParents.pop();
236 let b = theirParents.pop();
237 let last;
238 while (a === b && ourParents.length > 0 && theirParents.length > 0) {
239 last = a;
240 a = ourParents.pop();
241 b = theirParents.pop();
242 }
243
244 if (a === b) {
245 // One bundle descended from the other
246 return a;
247 }
248
249 return last;
250 }
251
252 getHash() {
253 let hash = crypto.createHash('md5');
254 for (let asset of this.assets) {
255 hash.update(asset.hash);
256 }
257
258 return hash.digest('hex');
259 }
260}
261
262module.exports = Bundle;