UNPKG

13.2 kBJavaScriptView Raw
1// @flow
2
3import type {
4 Blob,
5 FilePath,
6 BundleResult,
7 Bundle as BundleType,
8 BundleGraph as BundleGraphType,
9 Stats,
10} from '@parcel/types';
11import type SourceMap from '@parcel/source-map';
12import type WorkerFarm from '@parcel/workers';
13import type {Bundle as InternalBundle, ParcelOptions, ReportFn} from './types';
14import type ParcelConfig from './ParcelConfig';
15import type InternalBundleGraph from './BundleGraph';
16import type {FileSystem, FileOptions} from '@parcel/fs';
17
18import {md5FromObject, md5FromString, blobToStream} from '@parcel/utils';
19import {PluginLogger} from '@parcel/logger';
20import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic';
21import {Readable} from 'stream';
22import nullthrows from 'nullthrows';
23import path from 'path';
24import url from 'url';
25
26import {NamedBundle, bundleToInternalBundle} from './public/Bundle';
27import BundleGraph, {
28 bundleGraphToInternalBundleGraph,
29} from './public/BundleGraph';
30import PluginOptions from './public/PluginOptions';
31import {PARCEL_VERSION} from './constants';
32
33type Opts = {|
34 config: ParcelConfig,
35 farm?: WorkerFarm,
36 options: ParcelOptions,
37 report: ReportFn,
38|};
39
40export default class PackagerRunner {
41 config: ParcelConfig;
42 options: ParcelOptions;
43 farm: ?WorkerFarm;
44 pluginOptions: PluginOptions;
45 distDir: FilePath;
46 distExists: Set<FilePath>;
47 report: ReportFn;
48 writeBundleFromWorker: ({|
49 bundle: InternalBundle,
50 bundleGraphReference: number,
51 config: ParcelConfig,
52 cacheKey: string,
53 options: ParcelOptions,
54 |}) => Promise<Stats>;
55
56 constructor({config, farm, options, report}: Opts) {
57 this.config = config;
58 this.options = options;
59 this.pluginOptions = new PluginOptions(this.options);
60
61 this.farm = farm;
62 this.report = report;
63 this.writeBundleFromWorker = farm
64 ? farm.createHandle('runPackage')
65 : () => {
66 throw new Error(
67 'Cannot call PackagerRunner.writeBundleFromWorker() in a worker',
68 );
69 };
70 }
71
72 async writeBundles(bundleGraph: InternalBundleGraph) {
73 let farm = nullthrows(this.farm);
74 let {ref, dispose} = await farm.createSharedReference(bundleGraph);
75
76 let promises = [];
77 for (let bundle of bundleGraph.getBundles()) {
78 // skip inline bundles, they will be processed via the parent bundle
79 if (bundle.isInline) {
80 continue;
81 }
82
83 promises.push(
84 this.writeBundle(bundle, bundleGraph, ref).then(stats => {
85 bundle.stats = stats;
86 }),
87 );
88 }
89
90 await Promise.all(promises);
91 await dispose();
92 }
93
94 async writeBundle(
95 bundle: InternalBundle,
96 bundleGraph: InternalBundleGraph,
97 bundleGraphReference: number,
98 ) {
99 let start = Date.now();
100
101 let cacheKey = await this.getCacheKey(bundle, bundleGraph);
102 let {size} =
103 (await this.writeBundleFromCache({bundle, bundleGraph, cacheKey})) ||
104 (await this.writeBundleFromWorker({
105 bundle,
106 cacheKey,
107 bundleGraphReference,
108 options: this.options,
109 config: this.config,
110 }));
111
112 return {
113 time: Date.now() - start,
114 size,
115 };
116 }
117
118 async writeBundleFromCache({
119 bundle,
120 bundleGraph,
121 cacheKey,
122 }: {|
123 bundle: InternalBundle,
124 bundleGraph: InternalBundleGraph,
125 cacheKey: string,
126 |}) {
127 if (this.options.disableCache) {
128 return;
129 }
130
131 let cacheResult = await this.readFromCache(cacheKey);
132 if (cacheResult == null) {
133 return;
134 }
135
136 let {contents, map} = cacheResult;
137 let {size} = await this.writeToDist({
138 bundle,
139 bundleGraph,
140 contents,
141 map,
142 });
143
144 return {size};
145 }
146
147 async packageAndWriteBundle(
148 bundle: InternalBundle,
149 bundleGraph: InternalBundleGraph,
150 cacheKey: string,
151 ) {
152 let start = Date.now();
153
154 let {contents, map} = await this.getBundleResult(
155 bundle,
156 bundleGraph,
157 cacheKey,
158 );
159 let {size} = await this.writeToDist({
160 bundle,
161 bundleGraph,
162 contents,
163 map,
164 });
165
166 return {
167 time: Date.now() - start,
168 size,
169 };
170 }
171
172 async getBundleResult(
173 bundle: InternalBundle,
174 bundleGraph: InternalBundleGraph,
175 cacheKey: ?string,
176 ): Promise<{|contents: Blob, map: ?(Readable | string)|}> {
177 let result;
178 if (!cacheKey && !this.options.disableCache) {
179 cacheKey = await this.getCacheKey(bundle, bundleGraph);
180 let cacheResult = await this.readFromCache(cacheKey);
181
182 if (cacheResult) {
183 // NOTE: Returning a new object for flow
184 return {
185 contents: cacheResult.contents,
186 map: cacheResult.map,
187 };
188 }
189 }
190
191 let packaged = await this.package(bundle, bundleGraph);
192 let res = await this.optimize(
193 bundle,
194 bundleGraph,
195 packaged.contents,
196 packaged.map,
197 );
198
199 let map = res.map ? await this.generateSourceMap(bundle, res.map) : null;
200 result = {
201 contents: res.contents,
202 map,
203 };
204
205 if (cacheKey != null) {
206 await this.writeToCache(cacheKey, result.contents, map);
207
208 if (result.contents instanceof Readable) {
209 return {
210 contents: this.options.cache.getStream(getContentKey(cacheKey)),
211 map: result.map,
212 };
213 }
214 }
215
216 return result;
217 }
218
219 async package(
220 internalBundle: InternalBundle,
221 bundleGraph: InternalBundleGraph,
222 ): Promise<BundleResult> {
223 let bundle = new NamedBundle(internalBundle, bundleGraph, this.options);
224 this.report({
225 type: 'buildProgress',
226 phase: 'packaging',
227 bundle,
228 });
229
230 let packager = await this.config.getPackager(bundle.filePath);
231 try {
232 return await packager.plugin.package({
233 bundle,
234 bundleGraph: new BundleGraph(bundleGraph, this.options),
235 getSourceMapReference: map => {
236 return bundle.isInline ||
237 (bundle.target.sourceMap && bundle.target.sourceMap.inline)
238 ? this.generateSourceMap(bundleToInternalBundle(bundle), map)
239 : path.basename(bundle.filePath) + '.map';
240 },
241 options: this.pluginOptions,
242 logger: new PluginLogger({origin: packager.name}),
243 getInlineBundleContents: (
244 bundle: BundleType,
245 bundleGraph: BundleGraphType,
246 ) => {
247 if (!bundle.isInline) {
248 throw new Error(
249 'Bundle is not inline and unable to retrieve contents',
250 );
251 }
252
253 return this.getBundleResult(
254 bundleToInternalBundle(bundle),
255 bundleGraphToInternalBundleGraph(bundleGraph),
256 );
257 },
258 });
259 } catch (e) {
260 throw new ThrowableDiagnostic({
261 diagnostic: errorToDiagnostic(e, packager.name),
262 });
263 }
264 }
265
266 async optimize(
267 internalBundle: InternalBundle,
268 bundleGraph: InternalBundleGraph,
269 contents: Blob,
270 map?: ?SourceMap,
271 ): Promise<BundleResult> {
272 let bundle = new NamedBundle(internalBundle, bundleGraph, this.options);
273 let optimizers = await this.config.getOptimizers(
274 bundle.filePath,
275 internalBundle.pipeline,
276 );
277 if (!optimizers.length) {
278 return {contents, map};
279 }
280
281 this.report({
282 type: 'buildProgress',
283 phase: 'optimizing',
284 bundle,
285 });
286
287 let optimized = {contents, map};
288 for (let optimizer of optimizers) {
289 try {
290 optimized = await optimizer.plugin.optimize({
291 bundle,
292 contents: optimized.contents,
293 map: optimized.map,
294 options: this.pluginOptions,
295 logger: new PluginLogger({origin: optimizer.name}),
296 });
297 } catch (e) {
298 throw new ThrowableDiagnostic({
299 diagnostic: errorToDiagnostic(e, optimizer.name),
300 });
301 }
302 }
303
304 return optimized;
305 }
306
307 generateSourceMap(bundle: InternalBundle, map: SourceMap): Promise<string> {
308 // sourceRoot should be a relative path between outDir and rootDir for node.js targets
309 let filePath = nullthrows(bundle.filePath);
310 let sourceRoot: string = path.relative(
311 path.dirname(filePath),
312 this.options.projectRoot,
313 );
314 let inlineSources = false;
315
316 if (bundle.target) {
317 if (
318 bundle.target.sourceMap &&
319 bundle.target.sourceMap.sourceRoot !== undefined
320 ) {
321 sourceRoot = bundle.target.sourceMap.sourceRoot;
322 } else if (
323 bundle.target.env.context === 'browser' &&
324 this.options.mode !== 'production'
325 ) {
326 sourceRoot = '/__parcel_source_root';
327 }
328
329 if (
330 bundle.target.sourceMap &&
331 bundle.target.sourceMap.inlineSources !== undefined
332 ) {
333 inlineSources = bundle.target.sourceMap.inlineSources;
334 } else if (bundle.target.env.context !== 'node') {
335 // inlining should only happen in production for browser targets by default
336 inlineSources = this.options.mode === 'production';
337 }
338 }
339
340 let mapFilename = filePath + '.map';
341 return map.stringify({
342 file: path.basename(mapFilename),
343 fs: this.options.inputFS,
344 rootDir: this.options.projectRoot,
345 sourceRoot: !inlineSources
346 ? url.format(url.parse(sourceRoot + '/'))
347 : undefined,
348 inlineSources,
349 inlineMap:
350 bundle.isInline ||
351 (bundle.target.sourceMap && bundle.target.sourceMap.inline),
352 });
353 }
354
355 getCacheKey(
356 bundle: InternalBundle,
357 bundleGraph: InternalBundleGraph,
358 ): string {
359 let filePath = nullthrows(bundle.filePath);
360 // TODO: include packagers and optimizers used in inline bundles as well
361 let packager = this.config.getPackagerName(filePath);
362 let optimizers = this.config.getOptimizerNames(filePath);
363 let deps = Promise.all(
364 [packager, ...optimizers].map(async pkg => {
365 let {pkg: resolvedPkg} = await this.options.packageManager.resolve(
366 `${pkg}/package.json`,
367 `${this.config.filePath}/index`,
368 );
369
370 let version = nullthrows(resolvedPkg).version;
371 return [pkg, version];
372 }),
373 );
374
375 // TODO: add third party configs to the cache key
376 let {minify, scopeHoist, sourceMaps} = this.options;
377 return md5FromObject({
378 parcelVersion: PARCEL_VERSION,
379 deps,
380 opts: {minify, scopeHoist, sourceMaps},
381 hash: bundleGraph.getHash(bundle),
382 });
383 }
384
385 async readFromCache(
386 cacheKey: string,
387 ): Promise<?{|
388 contents: Readable,
389 map: ?Readable,
390 |}> {
391 let contentKey = getContentKey(cacheKey);
392 let mapKey = getMapKey(cacheKey);
393
394 let contentExists = await this.options.cache.blobExists(contentKey);
395 if (!contentExists) {
396 return null;
397 }
398
399 let mapExists = await this.options.cache.blobExists(mapKey);
400
401 return {
402 contents: this.options.cache.getStream(contentKey),
403 map: mapExists ? this.options.cache.getStream(mapKey) : null,
404 };
405 }
406
407 async writeToDist({
408 bundle,
409 bundleGraph,
410 contents,
411 map,
412 }: {|
413 bundle: InternalBundle,
414 bundleGraph: InternalBundleGraph,
415 contents: Blob,
416 map: ?(Readable | string),
417 |}) {
418 let {inputFS, outputFS} = this.options;
419 let filePath = nullthrows(bundle.filePath);
420 let dir = path.dirname(filePath);
421 await outputFS.mkdirp(dir); // ? Got rid of dist exists, is this an expensive operation
422
423 // Use the file mode from the entry asset as the file mode for the bundle.
424 // Don't do this for browser builds, as the executable bit in particular is unnecessary.
425 let publicBundle = new NamedBundle(bundle, bundleGraph, this.options);
426 let writeOptions = publicBundle.env.isBrowser()
427 ? undefined
428 : {
429 mode: (
430 await inputFS.stat(nullthrows(publicBundle.getMainEntry()).filePath)
431 ).mode,
432 };
433
434 let size;
435 if (contents instanceof Readable) {
436 size = await writeFileStream(outputFS, filePath, contents, writeOptions);
437 } else {
438 await outputFS.writeFile(filePath, contents, writeOptions);
439 size = contents.length;
440 }
441
442 if (map != null) {
443 if (map instanceof Readable) {
444 await writeFileStream(outputFS, filePath + '.map', map);
445 } else {
446 await outputFS.writeFile(filePath + '.map', map);
447 }
448 }
449
450 return {size};
451 }
452
453 async writeToCache(cacheKey: string, contents: Blob, map: ?Blob) {
454 let contentKey = getContentKey(cacheKey);
455
456 await this.options.cache.setStream(contentKey, blobToStream(contents));
457
458 if (map != null) {
459 let mapKey = getMapKey(cacheKey);
460 await this.options.cache.setStream(mapKey, blobToStream(map));
461 }
462 }
463}
464
465function writeFileStream(
466 fs: FileSystem,
467 filePath: FilePath,
468 stream: Readable,
469 options: ?FileOptions,
470): Promise<number> {
471 return new Promise((resolve, reject) => {
472 let fsStream = fs.createWriteStream(filePath, options);
473 stream
474 .pipe(fsStream)
475 // $FlowFixMe
476 .on('finish', () => resolve(fsStream.bytesWritten))
477 .on('error', reject);
478 });
479}
480
481function getContentKey(cacheKey: string) {
482 return md5FromString(`${cacheKey}:content`);
483}
484
485function getMapKey(cacheKey: string) {
486 return md5FromString(`${cacheKey}:map`);
487}