UNPKG

18.2 kBJavaScriptView Raw
1const fs = require('./utils/fs');
2const Resolver = require('./Resolver');
3const Parser = require('./Parser');
4const WorkerFarm = require('./WorkerFarm');
5const Path = require('path');
6const Bundle = require('./Bundle');
7const {FSWatcher} = require('chokidar');
8const FSCache = require('./FSCache');
9const HMRServer = require('./HMRServer');
10const Server = require('./Server');
11const {EventEmitter} = require('events');
12const logger = require('./Logger');
13const PackagerRegistry = require('./packagers');
14const localRequire = require('./utils/localRequire');
15const config = require('./utils/config');
16const emoji = require('./utils/emoji');
17const loadEnv = require('./utils/env');
18const PromiseQueue = require('./utils/PromiseQueue');
19const installPackage = require('./utils/installPackage');
20const bundleReport = require('./utils/bundleReport');
21const prettifyTime = require('./utils/prettifyTime');
22
23/**
24 * The Bundler is the main entry point. It resolves and loads assets,
25 * creates the bundle tree, and manages the worker farm, cache, and file watcher.
26 */
27class Bundler extends EventEmitter {
28 constructor(main, options = {}) {
29 super();
30 this.mainFile = Path.resolve(main || '');
31 this.options = this.normalizeOptions(options);
32
33 this.resolver = new Resolver(this.options);
34 this.parser = new Parser(this.options);
35 this.packagers = new PackagerRegistry();
36 this.cache = this.options.cache ? new FSCache(this.options) : null;
37 this.delegate = options.delegate || {};
38 this.bundleLoaders = {};
39
40 const loadersPath = `./builtins/loaders/${
41 options.target === 'node' ? 'node' : 'browser'
42 }/`;
43
44 this.addBundleLoader('wasm', require.resolve(loadersPath + 'wasm-loader'));
45 this.addBundleLoader('css', require.resolve(loadersPath + 'css-loader'));
46 this.addBundleLoader('js', require.resolve(loadersPath + 'js-loader'));
47
48 this.pending = false;
49 this.loadedAssets = new Map();
50 this.watchedAssets = new Map();
51 this.farm = null;
52 this.watcher = null;
53 this.hmr = null;
54 this.bundleHashes = null;
55 this.errored = false;
56 this.buildQueue = new PromiseQueue(this.processAsset.bind(this));
57 this.rebuildTimeout = null;
58
59 logger.setOptions(this.options);
60 }
61
62 normalizeOptions(options) {
63 const isProduction =
64 options.production || process.env.NODE_ENV === 'production';
65 const publicURL = options.publicUrl || options.publicURL || '/';
66 const watch =
67 typeof options.watch === 'boolean' ? options.watch : !isProduction;
68 const target = options.target || 'browser';
69 return {
70 production: isProduction,
71 outDir: Path.resolve(options.outDir || 'dist'),
72 outFile: options.outFile || '',
73 publicURL: publicURL,
74 watch: watch,
75 cache: typeof options.cache === 'boolean' ? options.cache : true,
76 cacheDir: Path.resolve(options.cacheDir || '.cache'),
77 killWorkers:
78 typeof options.killWorkers === 'boolean' ? options.killWorkers : true,
79 minify:
80 typeof options.minify === 'boolean' ? options.minify : isProduction,
81 target: target,
82 hmr:
83 target === 'node'
84 ? false
85 : typeof options.hmr === 'boolean' ? options.hmr : watch,
86 https: options.https || false,
87 logLevel: isNaN(options.logLevel) ? 3 : options.logLevel,
88 mainFile: this.mainFile,
89 hmrPort: options.hmrPort || 0,
90 rootDir: Path.dirname(this.mainFile),
91 sourceMaps:
92 typeof options.sourceMaps === 'boolean' ? options.sourceMaps : true,
93 hmrHostname:
94 options.hmrHostname ||
95 (options.target === 'electron' ? 'localhost' : ''),
96 detailedReport: options.detailedReport || false,
97 autoinstall: (options.autoinstall || false) && !isProduction,
98 contentHash:
99 typeof options.contentHash === 'boolean'
100 ? options.contentHash
101 : isProduction
102 };
103 }
104
105 addAssetType(extension, path) {
106 if (typeof path !== 'string') {
107 throw new Error('Asset type should be a module path.');
108 }
109
110 if (this.farm) {
111 throw new Error('Asset types must be added before bundling.');
112 }
113
114 this.parser.registerExtension(extension, path);
115 }
116
117 addPackager(type, packager) {
118 if (this.farm) {
119 throw new Error('Packagers must be added before bundling.');
120 }
121
122 this.packagers.add(type, packager);
123 }
124
125 addBundleLoader(type, path) {
126 if (typeof path !== 'string') {
127 throw new Error('Bundle loader should be a module path.');
128 }
129
130 if (this.farm) {
131 throw new Error('Bundle loaders must be added before bundling.');
132 }
133
134 this.bundleLoaders[type] = path;
135 }
136
137 async loadPlugins() {
138 let pkg = await config.load(this.mainFile, ['package.json']);
139 if (!pkg) {
140 return;
141 }
142
143 try {
144 let deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
145 for (let dep in deps) {
146 if (dep.startsWith('parcel-plugin-')) {
147 let plugin = await localRequire(dep, this.mainFile);
148 await plugin(this);
149 }
150 }
151 } catch (err) {
152 logger.warn(err);
153 }
154 }
155
156 async bundle() {
157 // If another bundle is already pending, wait for that one to finish and retry.
158 if (this.pending) {
159 return new Promise((resolve, reject) => {
160 this.once('buildEnd', () => {
161 this.bundle().then(resolve, reject);
162 });
163 });
164 }
165
166 let isInitialBundle = !this.mainAsset;
167 let startTime = Date.now();
168 this.pending = true;
169 this.errored = false;
170
171 logger.clear();
172 logger.status(emoji.progress, 'Building...');
173
174 try {
175 // Start worker farm, watcher, etc. if needed
176 await this.start();
177
178 // If this is the initial bundle, ensure the output directory exists, and resolve the main asset.
179 if (isInitialBundle) {
180 await fs.mkdirp(this.options.outDir);
181
182 this.mainAsset = await this.resolveAsset(this.mainFile);
183 this.buildQueue.add(this.mainAsset);
184 }
185
186 // Build the queued assets.
187 let loadedAssets = await this.buildQueue.run();
188
189 // The changed assets are any that don't have a parent bundle yet
190 // plus the ones that were in the build queue.
191 let changedAssets = [...this.findOrphanAssets(), ...loadedAssets];
192
193 // Invalidate bundles
194 for (let asset of this.loadedAssets.values()) {
195 asset.invalidateBundle();
196 }
197
198 // Create a new bundle tree
199 this.mainBundle = this.createBundleTree(this.mainAsset);
200
201 // Generate the final bundle names, and replace references in the built assets.
202 this.bundleNameMap = this.mainBundle.getBundleNameMap(
203 this.options.contentHash
204 );
205
206 for (let asset of changedAssets) {
207 asset.replaceBundleNames(this.bundleNameMap);
208 }
209
210 // Emit an HMR update if this is not the initial bundle.
211 if (this.hmr && !isInitialBundle) {
212 this.hmr.emitUpdate(changedAssets);
213 }
214
215 // Package everything up
216 this.bundleHashes = await this.mainBundle.package(
217 this,
218 this.bundleHashes
219 );
220
221 // Unload any orphaned assets
222 this.unloadOrphanedAssets();
223
224 let buildTime = Date.now() - startTime;
225 let time = prettifyTime(buildTime);
226 logger.status(emoji.success, `Built in ${time}.`, 'green');
227 if (!this.watcher) {
228 bundleReport(this.mainBundle, this.options.detailedReport);
229 }
230
231 this.emit('bundled', this.mainBundle);
232 return this.mainBundle;
233 } catch (err) {
234 this.errored = true;
235 logger.error(err);
236 if (this.hmr) {
237 this.hmr.emitError(err);
238 }
239
240 if (process.env.NODE_ENV === 'production') {
241 process.exitCode = 1;
242 } else if (process.env.NODE_ENV === 'test' && !this.hmr) {
243 throw err;
244 }
245 } finally {
246 this.pending = false;
247 this.emit('buildEnd');
248
249 // If not in watch mode, stop the worker farm so we don't keep the process running.
250 if (!this.watcher && this.options.killWorkers) {
251 this.stop();
252 }
253 }
254 }
255
256 async start() {
257 if (this.farm) {
258 return;
259 }
260
261 await this.loadPlugins();
262 await loadEnv(this.mainFile);
263
264 this.options.extensions = Object.assign({}, this.parser.extensions);
265 this.options.bundleLoaders = this.bundleLoaders;
266 this.options.env = process.env;
267
268 if (this.options.watch) {
269 // FS events on macOS are flakey in the tests, which write lots of files very quickly
270 // See https://github.com/paulmillr/chokidar/issues/612
271 this.watcher = new FSWatcher({
272 useFsEvents: process.env.NODE_ENV !== 'test'
273 });
274
275 this.watcher.on('change', this.onChange.bind(this));
276 }
277
278 if (this.options.hmr) {
279 this.hmr = new HMRServer();
280 this.options.hmrPort = await this.hmr.start(this.options);
281 }
282
283 this.farm = WorkerFarm.getShared(this.options);
284 }
285
286 stop() {
287 if (this.farm) {
288 this.farm.end();
289 }
290
291 if (this.watcher) {
292 this.watcher.close();
293 }
294
295 if (this.hmr) {
296 this.hmr.stop();
297 }
298 }
299
300 async getAsset(name, parent) {
301 let asset = await this.resolveAsset(name, parent);
302 this.buildQueue.add(asset);
303 await this.buildQueue.run();
304 return asset;
305 }
306
307 async resolveAsset(name, parent) {
308 let {path, pkg} = await this.resolver.resolve(name, parent);
309 if (this.loadedAssets.has(path)) {
310 return this.loadedAssets.get(path);
311 }
312
313 let asset = this.parser.getAsset(path, pkg, this.options);
314 this.loadedAssets.set(path, asset);
315
316 this.watch(path, asset);
317 return asset;
318 }
319
320 watch(path, asset) {
321 if (!this.watcher) {
322 return;
323 }
324
325 if (!this.watchedAssets.has(path)) {
326 this.watcher.add(path);
327 this.watchedAssets.set(path, new Set());
328 }
329
330 this.watchedAssets.get(path).add(asset);
331 }
332
333 unwatch(path, asset) {
334 if (!this.watchedAssets.has(path)) {
335 return;
336 }
337
338 let watched = this.watchedAssets.get(path);
339 watched.delete(asset);
340
341 if (watched.size === 0) {
342 this.watchedAssets.delete(path);
343 this.watcher.unwatch(path);
344 }
345 }
346
347 async resolveDep(asset, dep, install = true) {
348 try {
349 return await this.resolveAsset(dep.name, asset.name);
350 } catch (err) {
351 let thrown = err;
352
353 if (thrown.message.indexOf(`Cannot find module '${dep.name}'`) === 0) {
354 // Check if dependency is a local file
355 let isLocalFile = /^[/~.]/.test(dep.name);
356 let fromNodeModules = asset.name.includes(
357 `${Path.sep}node_modules${Path.sep}`
358 );
359
360 // If it's not a local file, attempt to install the dep
361 if (
362 !isLocalFile &&
363 !fromNodeModules &&
364 this.options.autoinstall &&
365 install
366 ) {
367 return await this.installDep(asset, dep);
368 }
369
370 // If the dep is optional, return before we throw
371 if (dep.optional) {
372 return;
373 }
374
375 thrown.message = `Cannot resolve dependency '${dep.name}'`;
376 if (isLocalFile) {
377 const absPath = Path.resolve(Path.dirname(asset.name), dep.name);
378 thrown.message += ` at '${absPath}'`;
379 }
380
381 await this.throwDepError(asset, dep, thrown);
382 }
383
384 throw thrown;
385 }
386 }
387
388 async installDep(asset, dep) {
389 let [moduleName] = this.resolver.getModuleParts(dep.name);
390 try {
391 await installPackage([moduleName], asset.name, {saveDev: false});
392 } catch (err) {
393 await this.throwDepError(asset, dep, err);
394 }
395
396 return await this.resolveDep(asset, dep, false);
397 }
398
399 async throwDepError(asset, dep, err) {
400 // Generate a code frame where the dependency was used
401 if (dep.loc) {
402 await asset.loadIfNeeded();
403 err.loc = dep.loc;
404 err = asset.generateErrorMessage(err);
405 }
406
407 err.fileName = asset.name;
408 throw err;
409 }
410
411 async processAsset(asset, isRebuild) {
412 if (isRebuild) {
413 asset.invalidate();
414 if (this.cache) {
415 this.cache.invalidate(asset.name);
416 }
417 }
418
419 await this.loadAsset(asset);
420 }
421
422 async loadAsset(asset) {
423 if (asset.processed) {
424 return;
425 }
426
427 if (!this.errored) {
428 logger.status(emoji.progress, `Building ${asset.basename}...`);
429 }
430
431 // Mark the asset processed so we don't load it twice
432 asset.processed = true;
433
434 // First try the cache, otherwise load and compile in the background
435 let startTime = Date.now();
436 let processed = this.cache && (await this.cache.read(asset.name));
437 if (!processed || asset.shouldInvalidate(processed.cacheData)) {
438 processed = await this.farm.run(asset.name, asset.package, this.options);
439 if (this.cache) {
440 this.cache.write(asset.name, processed);
441 }
442 }
443
444 asset.buildTime = Date.now() - startTime;
445 asset.generated = processed.generated;
446 asset.hash = processed.hash;
447
448 // Call the delegate to get implicit dependencies
449 let dependencies = processed.dependencies;
450 if (this.delegate.getImplicitDependencies) {
451 let implicitDeps = await this.delegate.getImplicitDependencies(asset);
452 if (implicitDeps) {
453 dependencies = dependencies.concat(implicitDeps);
454 }
455 }
456
457 // Resolve and load asset dependencies
458 let assetDeps = await Promise.all(
459 dependencies.map(async dep => {
460 if (dep.includedInParent) {
461 // This dependency is already included in the parent's generated output,
462 // so no need to load it. We map the name back to the parent asset so
463 // that changing it triggers a recompile of the parent.
464 this.watch(dep.name, asset);
465 } else {
466 let assetDep = await this.resolveDep(asset, dep);
467 if (assetDep) {
468 await this.loadAsset(assetDep);
469 }
470
471 return assetDep;
472 }
473 })
474 );
475
476 // Store resolved assets in their original order
477 dependencies.forEach((dep, i) => {
478 asset.dependencies.set(dep.name, dep);
479 let assetDep = assetDeps[i];
480 if (assetDep) {
481 asset.depAssets.set(dep, assetDep);
482 }
483 });
484 }
485
486 createBundleTree(asset, dep, bundle, parentBundles = new Set()) {
487 if (dep) {
488 asset.parentDeps.add(dep);
489 }
490
491 if (asset.parentBundle) {
492 // If the asset is already in a bundle, it is shared. Move it to the lowest common ancestor.
493 if (asset.parentBundle !== bundle) {
494 let commonBundle = bundle.findCommonAncestor(asset.parentBundle);
495 if (
496 asset.parentBundle !== commonBundle &&
497 asset.parentBundle.type === commonBundle.type
498 ) {
499 this.moveAssetToBundle(asset, commonBundle);
500 return;
501 }
502 } else {
503 return;
504 }
505
506 // Detect circular bundles
507 if (parentBundles.has(asset.parentBundle)) {
508 return;
509 }
510 }
511
512 if (!bundle) {
513 // Create the root bundle if it doesn't exist
514 bundle = Bundle.createWithAsset(asset);
515 } else if (dep && dep.dynamic) {
516 // Create a new bundle for dynamic imports
517 bundle = bundle.createChildBundle(asset);
518 } else if (asset.type && !this.packagers.has(asset.type)) {
519 // No packager is available for this asset type. Create a new bundle with only this asset.
520 bundle.createSiblingBundle(asset);
521 } else {
522 // Add the asset to the common bundle of the asset's type
523 bundle.getSiblingBundle(asset.type).addAsset(asset);
524 }
525
526 // If the asset generated a representation for the parent bundle type, also add it there
527 if (asset.generated[bundle.type] != null) {
528 bundle.addAsset(asset);
529 }
530
531 // Add the asset to sibling bundles for each generated type
532 if (asset.type && asset.generated[asset.type]) {
533 for (let t in asset.generated) {
534 if (asset.generated[t]) {
535 bundle.getSiblingBundle(t).addAsset(asset);
536 }
537 }
538 }
539
540 asset.parentBundle = bundle;
541 parentBundles.add(bundle);
542
543 for (let [dep, assetDep] of asset.depAssets) {
544 this.createBundleTree(assetDep, dep, bundle, parentBundles);
545 }
546
547 parentBundles.delete(bundle);
548 return bundle;
549 }
550
551 moveAssetToBundle(asset, commonBundle) {
552 // Never move the entry asset of a bundle, as it was explicitly requested to be placed in a separate bundle.
553 if (asset.parentBundle.entryAsset === asset) {
554 return;
555 }
556
557 for (let bundle of Array.from(asset.bundles)) {
558 bundle.removeAsset(asset);
559 commonBundle.getSiblingBundle(bundle.type).addAsset(asset);
560 }
561
562 let oldBundle = asset.parentBundle;
563 asset.parentBundle = commonBundle;
564
565 // Move all dependencies as well
566 for (let child of asset.depAssets.values()) {
567 if (child.parentBundle === oldBundle) {
568 this.moveAssetToBundle(child, commonBundle);
569 }
570 }
571 }
572
573 *findOrphanAssets() {
574 for (let asset of this.loadedAssets.values()) {
575 if (!asset.parentBundle) {
576 yield asset;
577 }
578 }
579 }
580
581 unloadOrphanedAssets() {
582 for (let asset of this.findOrphanAssets()) {
583 this.unloadAsset(asset);
584 }
585 }
586
587 unloadAsset(asset) {
588 this.loadedAssets.delete(asset.name);
589 if (this.watcher) {
590 this.unwatch(asset.name, asset);
591
592 // Unwatch all included dependencies that map to this asset
593 for (let dep of asset.dependencies.values()) {
594 if (dep.includedInParent) {
595 this.unwatch(dep.name, asset);
596 }
597 }
598 }
599 }
600
601 async onChange(path) {
602 let assets = this.watchedAssets.get(path);
603 if (!assets || !assets.size) {
604 return;
605 }
606
607 logger.clear();
608 logger.status(emoji.progress, `Building ${Path.basename(path)}...`);
609
610 // Add the asset to the rebuild queue, and reset the timeout.
611 for (let asset of assets) {
612 this.buildQueue.add(asset, true);
613 }
614
615 clearTimeout(this.rebuildTimeout);
616
617 this.rebuildTimeout = setTimeout(async () => {
618 await this.bundle();
619 }, 100);
620 }
621
622 middleware() {
623 this.bundle();
624 return Server.middleware(this);
625 }
626
627 async serve(port = 1234, https = false) {
628 this.server = await Server.serve(this, port, https);
629 this.bundle();
630 return this.server;
631 }
632}
633
634module.exports = Bundler;
635Bundler.Asset = require('./Asset');
636Bundler.Packager = require('./packagers/Packager');