1 |
|
2 |
|
3 | import type {
|
4 | Blob,
|
5 | FilePath,
|
6 | BundleResult,
|
7 | Bundle as BundleType,
|
8 | BundleGraph as BundleGraphType,
|
9 | Stats,
|
10 | } from '@parcel/types';
|
11 | import type SourceMap from '@parcel/source-map';
|
12 | import type WorkerFarm from '@parcel/workers';
|
13 | import type {Bundle as InternalBundle, ParcelOptions, ReportFn} from './types';
|
14 | import type ParcelConfig from './ParcelConfig';
|
15 | import type InternalBundleGraph from './BundleGraph';
|
16 | import type {FileSystem, FileOptions} from '@parcel/fs';
|
17 |
|
18 | import {md5FromObject, md5FromString, blobToStream} from '@parcel/utils';
|
19 | import {PluginLogger} from '@parcel/logger';
|
20 | import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic';
|
21 | import {Readable} from 'stream';
|
22 | import nullthrows from 'nullthrows';
|
23 | import path from 'path';
|
24 | import url from 'url';
|
25 |
|
26 | import {NamedBundle, bundleToInternalBundle} from './public/Bundle';
|
27 | import BundleGraph, {
|
28 | bundleGraphToInternalBundleGraph,
|
29 | } from './public/BundleGraph';
|
30 | import PluginOptions from './public/PluginOptions';
|
31 | import {PARCEL_VERSION} from './constants';
|
32 |
|
33 | type Opts = {|
|
34 | config: ParcelConfig,
|
35 | farm?: WorkerFarm,
|
36 | options: ParcelOptions,
|
37 | report: ReportFn,
|
38 | |};
|
39 |
|
40 | export 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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);
|
422 |
|
423 |
|
424 |
|
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 |
|
465 | function 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 |
|
476 | .on('finish', () => resolve(fsStream.bytesWritten))
|
477 | .on('error', reject);
|
478 | });
|
479 | }
|
480 |
|
481 | function getContentKey(cacheKey: string) {
|
482 | return md5FromString(`${cacheKey}:content`);
|
483 | }
|
484 |
|
485 | function getMapKey(cacheKey: string) {
|
486 | return md5FromString(`${cacheKey}:map`);
|
487 | }
|