1 |
|
2 |
|
3 | import type {
|
4 | MutableAsset as IMutableAsset,
|
5 | FilePath,
|
6 | GenerateOutput,
|
7 | Transformer,
|
8 | TransformerResult,
|
9 | PackageName,
|
10 | } from '@parcel/types';
|
11 | import type {WorkerApi} from '@parcel/workers';
|
12 | import type {
|
13 | Asset as AssetValue,
|
14 | AssetRequestDesc,
|
15 | Config,
|
16 | ConfigRequestDesc,
|
17 | ParcelOptions,
|
18 | ReportFn,
|
19 | } from './types';
|
20 |
|
21 | import invariant from 'assert';
|
22 | import path from 'path';
|
23 | import nullthrows from 'nullthrows';
|
24 | import {md5FromObject} from '@parcel/utils';
|
25 | import {PluginLogger} from '@parcel/logger';
|
26 | import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic';
|
27 |
|
28 | import ConfigLoader from './ConfigLoader';
|
29 | import {createDependency} from './Dependency';
|
30 | import ParcelConfig from './ParcelConfig';
|
31 | import ResolverRunner from './ResolverRunner';
|
32 | import {MutableAsset, assetToInternalAsset} from './public/Asset';
|
33 | import InternalAsset, {createAsset} from './InternalAsset';
|
34 | import summarizeRequest from './summarizeRequest';
|
35 | import PluginOptions from './public/PluginOptions';
|
36 | import {PARCEL_VERSION} from './constants';
|
37 |
|
38 | type GenerateFunc = (input: IMutableAsset) => Promise<GenerateOutput>;
|
39 |
|
40 | type PostProcessFunc = (
|
41 | Array<InternalAsset>,
|
42 | ) => Promise<Array<InternalAsset> | null>;
|
43 |
|
44 | export type TransformationOpts = {|
|
45 | options: ParcelOptions,
|
46 | report: ReportFn,
|
47 | request: AssetRequestDesc,
|
48 | workerApi: WorkerApi,
|
49 | |};
|
50 |
|
51 | type ConfigMap = Map<PackageName, Config>;
|
52 | type ConfigRequestAndResult = {|
|
53 | request: ConfigRequestDesc,
|
54 | result: Config,
|
55 | |};
|
56 |
|
57 | export default class Transformation {
|
58 | request: AssetRequestDesc;
|
59 | configLoader: ConfigLoader;
|
60 | configRequests: Array<ConfigRequestAndResult>;
|
61 | options: ParcelOptions;
|
62 | impactfulOptions: $Shape<ParcelOptions>;
|
63 | workerApi: WorkerApi;
|
64 | parcelConfig: ParcelConfig;
|
65 | report: ReportFn;
|
66 |
|
67 | constructor({report, request, options, workerApi}: TransformationOpts) {
|
68 | this.configRequests = [];
|
69 | this.configLoader = new ConfigLoader(options);
|
70 | this.options = options;
|
71 | this.report = report;
|
72 | this.request = request;
|
73 | this.workerApi = workerApi;
|
74 |
|
75 |
|
76 | let {minify, hot, scopeHoist} = this.options;
|
77 | this.impactfulOptions = {minify, hot, scopeHoist};
|
78 | }
|
79 |
|
80 | async loadConfig(configRequest: ConfigRequestDesc) {
|
81 | let result = await this.configLoader.load(configRequest);
|
82 | this.configRequests.push({request: configRequest, result});
|
83 | return result;
|
84 | }
|
85 |
|
86 | async run(): Promise<{|
|
87 | assets: Array<AssetValue>,
|
88 | configRequests: Array<ConfigRequestAndResult>,
|
89 | |}> {
|
90 | this.report({
|
91 | type: 'buildProgress',
|
92 | phase: 'transforming',
|
93 | filePath: this.request.filePath,
|
94 | });
|
95 |
|
96 | let asset = await this.loadAsset();
|
97 | let pipeline = await this.loadPipeline(
|
98 | this.request.filePath,
|
99 | asset.value.isSource,
|
100 | this.request.pipeline,
|
101 | );
|
102 | let results = await this.runPipelines(pipeline, asset);
|
103 | let assets = results.map(a => a.value);
|
104 |
|
105 | for (let {request, result} of this.configRequests) {
|
106 | let plugin =
|
107 | request.plugin != null &&
|
108 | (await this.parcelConfig.loadPlugin(request.plugin));
|
109 | if (plugin && plugin.preSerializeConfig) {
|
110 | plugin.preSerializeConfig({config: result});
|
111 | }
|
112 | }
|
113 | return {assets, configRequests: this.configRequests};
|
114 | }
|
115 |
|
116 | async loadAsset(): Promise<InternalAsset> {
|
117 | let {filePath, env, code, pipeline, sideEffects} = this.request;
|
118 | let {content, size, hash, isSource} = await summarizeRequest(
|
119 | this.options.inputFS,
|
120 | this.request,
|
121 | );
|
122 |
|
123 |
|
124 |
|
125 | let idBase = code != null ? hash : filePath;
|
126 | return new InternalAsset({
|
127 | idBase,
|
128 | value: createAsset({
|
129 | idBase,
|
130 | filePath,
|
131 | isSource,
|
132 | type: path.extname(filePath).slice(1),
|
133 | hash,
|
134 | pipeline,
|
135 | env,
|
136 | stats: {
|
137 | time: 0,
|
138 | size,
|
139 | },
|
140 | sideEffects,
|
141 | }),
|
142 | options: this.options,
|
143 | content,
|
144 | });
|
145 | }
|
146 |
|
147 | async runPipelines(
|
148 | pipeline: Pipeline,
|
149 | initialAsset: InternalAsset,
|
150 | ): Promise<Array<InternalAsset>> {
|
151 | let initialType = initialAsset.value.type;
|
152 | let initialAssetCacheKey = this.getCacheKey(
|
153 | [initialAsset],
|
154 | pipeline.configs,
|
155 | );
|
156 | let initialCacheEntry = await this.readFromCache(initialAssetCacheKey);
|
157 |
|
158 | let assets =
|
159 | initialCacheEntry || (await this.runPipeline(pipeline, initialAsset));
|
160 | if (!initialCacheEntry) {
|
161 | await this.writeToCache(initialAssetCacheKey, assets, pipeline.configs);
|
162 | }
|
163 |
|
164 | let finalAssets: Array<InternalAsset> = [];
|
165 | for (let asset of assets) {
|
166 | let nextPipeline;
|
167 | if (asset.value.type !== initialType) {
|
168 | nextPipeline = await this.loadNextPipeline({
|
169 | filePath: initialAsset.value.filePath,
|
170 | isSource: asset.value.isSource,
|
171 | nextType: asset.value.type,
|
172 | currentPipeline: pipeline,
|
173 | });
|
174 | }
|
175 |
|
176 | if (nextPipeline) {
|
177 | let nextPipelineAssets = await this.runPipelines(nextPipeline, asset);
|
178 | finalAssets = finalAssets.concat(nextPipelineAssets);
|
179 | } else {
|
180 | finalAssets.push(asset);
|
181 | }
|
182 | }
|
183 |
|
184 | if (!pipeline.postProcess) {
|
185 | return finalAssets;
|
186 | }
|
187 |
|
188 | let processedCacheEntry = await this.readFromCache(
|
189 | this.getCacheKey(finalAssets, pipeline.configs),
|
190 | );
|
191 |
|
192 | invariant(pipeline.postProcess != null);
|
193 | let processedFinalAssets: Array<InternalAsset> =
|
194 | processedCacheEntry ?? (await pipeline.postProcess(finalAssets)) ?? [];
|
195 |
|
196 | if (!processedCacheEntry) {
|
197 | await this.writeToCache(
|
198 | this.getCacheKey(processedFinalAssets, pipeline.configs),
|
199 | processedFinalAssets,
|
200 | pipeline.configs,
|
201 | );
|
202 | }
|
203 |
|
204 | return processedFinalAssets;
|
205 | }
|
206 |
|
207 | async runPipeline(
|
208 | pipeline: Pipeline,
|
209 | initialAsset: InternalAsset,
|
210 | ): Promise<Array<InternalAsset>> {
|
211 | let initialType = initialAsset.value.type;
|
212 | let inputAssets = [initialAsset];
|
213 | let resultingAssets;
|
214 | let finalAssets = [];
|
215 | for (let transformer of pipeline.transformers) {
|
216 | resultingAssets = [];
|
217 | for (let asset of inputAssets) {
|
218 | if (
|
219 | asset.value.type !== initialType &&
|
220 | (await this.loadNextPipeline({
|
221 | filePath: initialAsset.value.filePath,
|
222 | isSource: asset.value.isSource,
|
223 | nextType: asset.value.type,
|
224 | currentPipeline: pipeline,
|
225 | }))
|
226 | ) {
|
227 | finalAssets.push(asset);
|
228 | continue;
|
229 | }
|
230 |
|
231 | try {
|
232 | let transformerResults = await runTransformer(
|
233 | pipeline,
|
234 | asset,
|
235 | transformer.plugin,
|
236 | transformer.name,
|
237 | transformer.config,
|
238 | );
|
239 |
|
240 | for (let result of transformerResults) {
|
241 | resultingAssets.push(asset.createChildAsset(result));
|
242 | }
|
243 | } catch (e) {
|
244 | throw new ThrowableDiagnostic({
|
245 | diagnostic: errorToDiagnostic(e, transformer.name),
|
246 | });
|
247 | }
|
248 | }
|
249 | inputAssets = resultingAssets;
|
250 | }
|
251 |
|
252 | finalAssets = finalAssets.concat(resultingAssets);
|
253 |
|
254 | return Promise.all(
|
255 | finalAssets.map(asset =>
|
256 | finalize(nullthrows(asset), nullthrows(pipeline.generate)),
|
257 | ),
|
258 | );
|
259 | }
|
260 |
|
261 | async readFromCache(cacheKey: string): Promise<null | Array<InternalAsset>> {
|
262 | if (this.options.disableCache || this.request.code != null) {
|
263 | return null;
|
264 | }
|
265 |
|
266 | let cachedAssets = await this.options.cache.get(cacheKey);
|
267 | if (!cachedAssets) {
|
268 | return null;
|
269 | }
|
270 |
|
271 | return cachedAssets.map(
|
272 | (value: AssetValue) =>
|
273 | new InternalAsset({
|
274 | value,
|
275 | options: this.options,
|
276 | }),
|
277 | );
|
278 | }
|
279 |
|
280 | async writeToCache(
|
281 | cacheKey: string,
|
282 | assets: Array<InternalAsset>,
|
283 | configs: ConfigMap,
|
284 | ): Promise<void> {
|
285 | await Promise.all(
|
286 |
|
287 | assets.map(asset =>
|
288 | asset.commit(
|
289 | md5FromObject({
|
290 | impactfulOptions: this.impactfulOptions,
|
291 | configs: getImpactfulConfigInfo(configs),
|
292 | }),
|
293 | ),
|
294 | ),
|
295 | );
|
296 | this.options.cache.set(
|
297 | cacheKey,
|
298 | assets.map(a => a.value),
|
299 | );
|
300 | }
|
301 |
|
302 | getCacheKey(assets: Array<InternalAsset>, configs: ConfigMap): string {
|
303 | let assetsKeyInfo = assets.map(a => ({
|
304 | filePath: a.value.filePath,
|
305 | hash: a.value.hash,
|
306 | }));
|
307 |
|
308 | return md5FromObject({
|
309 | parcelVersion: PARCEL_VERSION,
|
310 | assets: assetsKeyInfo,
|
311 | configs: getImpactfulConfigInfo(configs),
|
312 | env: this.request.env,
|
313 | impactfulOptions: this.impactfulOptions,
|
314 | });
|
315 | }
|
316 |
|
317 | async loadPipeline(
|
318 | filePath: FilePath,
|
319 | isSource: boolean,
|
320 | pipelineName?: ?string,
|
321 | ): Promise<Pipeline> {
|
322 | let configRequest = {
|
323 | filePath,
|
324 | env: this.request.env,
|
325 | isSource,
|
326 | pipeline: pipelineName,
|
327 | meta: {
|
328 | actionType: 'transformation',
|
329 | },
|
330 | };
|
331 | let configs = new Map();
|
332 |
|
333 | let config = await this.loadConfig(configRequest);
|
334 | let result = nullthrows(config.result);
|
335 | let parcelConfig = new ParcelConfig(
|
336 | config.result,
|
337 | this.options.packageManager,
|
338 | );
|
339 |
|
340 | this.parcelConfig = parcelConfig;
|
341 |
|
342 | configs.set('parcel', config);
|
343 |
|
344 | for (let [moduleName] of config.devDeps) {
|
345 | let plugin = await parcelConfig.loadPlugin(moduleName);
|
346 |
|
347 | if (plugin.loadConfig) {
|
348 | let thirdPartyConfig = await this.loadTransformerConfig({
|
349 | filePath,
|
350 | plugin: moduleName,
|
351 | parcelConfigPath: result.filePath,
|
352 | isSource,
|
353 | });
|
354 |
|
355 | configs.set(moduleName, thirdPartyConfig);
|
356 | }
|
357 | }
|
358 |
|
359 | let transformers = await parcelConfig.getTransformers(
|
360 | filePath,
|
361 | pipelineName,
|
362 | );
|
363 | let pipeline = {
|
364 | id: transformers.map(t => t.name).join(':'),
|
365 |
|
366 | transformers: transformers.map(transformer => ({
|
367 | name: transformer.name,
|
368 | config: configs.get(transformer.name)?.result,
|
369 | plugin: transformer.plugin,
|
370 | })),
|
371 | configs,
|
372 | options: this.options,
|
373 | resolverRunner: new ResolverRunner({
|
374 | config: new ParcelConfig(
|
375 | nullthrows(nullthrows(configs.get('parcel')).result),
|
376 | this.options.packageManager,
|
377 | ),
|
378 | options: this.options,
|
379 | }),
|
380 |
|
381 | pluginOptions: new PluginOptions(this.options),
|
382 | workerApi: this.workerApi,
|
383 | };
|
384 |
|
385 | return pipeline;
|
386 | }
|
387 |
|
388 | async loadNextPipeline({
|
389 | filePath,
|
390 | isSource,
|
391 | nextType,
|
392 | currentPipeline,
|
393 | }: {|
|
394 | filePath: string,
|
395 | isSource: boolean,
|
396 | nextType: string,
|
397 | currentPipeline: Pipeline,
|
398 | |}): Promise<?Pipeline> {
|
399 | let nextFilePath =
|
400 | filePath.slice(0, -path.extname(filePath).length) + '.' + nextType;
|
401 | let nextPipeline = await this.loadPipeline(
|
402 | nextFilePath,
|
403 | isSource,
|
404 | this.request.pipeline,
|
405 | );
|
406 |
|
407 | if (nextPipeline.id === currentPipeline.id) {
|
408 | return null;
|
409 | }
|
410 |
|
411 | return nextPipeline;
|
412 | }
|
413 |
|
414 | loadTransformerConfig({
|
415 | filePath,
|
416 | plugin,
|
417 | parcelConfigPath,
|
418 | isSource,
|
419 | }: {|
|
420 | filePath: FilePath,
|
421 | plugin: PackageName,
|
422 | parcelConfigPath: FilePath,
|
423 | isSource: boolean,
|
424 | |}): Promise<Config> {
|
425 | let configRequest = {
|
426 | filePath,
|
427 | env: this.request.env,
|
428 | plugin,
|
429 | isSource,
|
430 | meta: {
|
431 | parcelConfigPath,
|
432 | },
|
433 | };
|
434 | return this.loadConfig(configRequest);
|
435 | }
|
436 | }
|
437 |
|
438 | type Pipeline = {|
|
439 | id: string,
|
440 | transformers: Array<TransformerWithNameAndConfig>,
|
441 | configs: ConfigMap,
|
442 | options: ParcelOptions,
|
443 | pluginOptions: PluginOptions,
|
444 | resolverRunner: ResolverRunner,
|
445 | workerApi: WorkerApi,
|
446 | postProcess?: PostProcessFunc,
|
447 | generate?: GenerateFunc,
|
448 | |};
|
449 |
|
450 | type TransformerWithNameAndConfig = {|
|
451 | name: PackageName,
|
452 | plugin: Transformer,
|
453 | config: ?Config,
|
454 | |};
|
455 |
|
456 | async function runTransformer(
|
457 | pipeline: Pipeline,
|
458 | asset: InternalAsset,
|
459 | transformer: Transformer,
|
460 | transformerName: string,
|
461 | preloadedConfig: ?Config,
|
462 | ): Promise<Array<TransformerResult>> {
|
463 | const logger = new PluginLogger({origin: transformerName});
|
464 |
|
465 | const resolve = async (from: FilePath, to: string): Promise<FilePath> => {
|
466 | return nullthrows(
|
467 | await pipeline.resolverRunner.resolve(
|
468 | createDependency({
|
469 | env: asset.value.env,
|
470 | moduleSpecifier: to,
|
471 | sourcePath: from,
|
472 | }),
|
473 | ),
|
474 | ).filePath;
|
475 | };
|
476 |
|
477 |
|
478 | let config = preloadedConfig;
|
479 | if (transformer.getConfig) {
|
480 |
|
481 | config = await transformer.getConfig({
|
482 | asset: new MutableAsset(asset),
|
483 | options: pipeline.pluginOptions,
|
484 | resolve,
|
485 | logger,
|
486 | });
|
487 | }
|
488 |
|
489 |
|
490 |
|
491 | if (
|
492 | asset.ast &&
|
493 | (!transformer.canReuseAST ||
|
494 | !transformer.canReuseAST({
|
495 | ast: asset.ast,
|
496 | options: pipeline.pluginOptions,
|
497 | logger,
|
498 | })) &&
|
499 | pipeline.generate
|
500 | ) {
|
501 | let output = await pipeline.generate(new MutableAsset(asset));
|
502 | asset.content = output.code;
|
503 | asset.ast = null;
|
504 | }
|
505 |
|
506 |
|
507 | if (!asset.ast && transformer.parse) {
|
508 | asset.ast = await transformer.parse({
|
509 | asset: new MutableAsset(asset),
|
510 | config,
|
511 | options: pipeline.pluginOptions,
|
512 | resolve,
|
513 | logger,
|
514 | });
|
515 | }
|
516 |
|
517 |
|
518 | let results = await normalizeAssets(
|
519 |
|
520 | await transformer.transform({
|
521 | asset: new MutableAsset(asset),
|
522 | config,
|
523 | options: pipeline.pluginOptions,
|
524 | resolve,
|
525 | logger,
|
526 | }),
|
527 | );
|
528 |
|
529 |
|
530 | pipeline.generate = (input: IMutableAsset): Promise<GenerateOutput> => {
|
531 | if (transformer.generate) {
|
532 | return Promise.resolve(
|
533 | transformer.generate({
|
534 | asset: input,
|
535 | config,
|
536 | options: pipeline.pluginOptions,
|
537 | resolve,
|
538 | logger,
|
539 | }),
|
540 | );
|
541 | }
|
542 |
|
543 | throw new Error(
|
544 | 'Asset has an AST but no generate method is available on the transform',
|
545 | );
|
546 | };
|
547 |
|
548 |
|
549 | let postProcess = transformer.postProcess;
|
550 | if (postProcess) {
|
551 | pipeline.postProcess = async (
|
552 | assets: Array<InternalAsset>,
|
553 | ): Promise<Array<InternalAsset> | null> => {
|
554 | let results = await postProcess.call(transformer, {
|
555 | assets: assets.map(asset => new MutableAsset(asset)),
|
556 | config,
|
557 | options: pipeline.pluginOptions,
|
558 | resolve,
|
559 | logger,
|
560 | });
|
561 |
|
562 | return Promise.all(results.map(result => asset.createChildAsset(result)));
|
563 | };
|
564 | }
|
565 |
|
566 | return results;
|
567 | }
|
568 |
|
569 | async function finalize(
|
570 | asset: InternalAsset,
|
571 | generate: GenerateFunc,
|
572 | ): Promise<InternalAsset> {
|
573 | if (asset.ast && generate) {
|
574 | let result = await generate(new MutableAsset(asset));
|
575 | return asset.createChildAsset({
|
576 | type: asset.value.type,
|
577 | uniqueKey: asset.value.uniqueKey,
|
578 | ...result,
|
579 | });
|
580 | }
|
581 | return asset;
|
582 | }
|
583 |
|
584 | function normalizeAssets(
|
585 | results: Array<TransformerResult | MutableAsset>,
|
586 | ): Array<TransformerResult> {
|
587 | return results.map(result => {
|
588 | if (!(result instanceof MutableAsset)) {
|
589 | return result;
|
590 | }
|
591 |
|
592 | let internalAsset = assetToInternalAsset(result);
|
593 | return {
|
594 | type: result.type,
|
595 | content: internalAsset.content,
|
596 | ast: result.ast,
|
597 | map: internalAsset.map,
|
598 |
|
599 | dependencies: [...internalAsset.value.dependencies.values()],
|
600 | includedFiles: result.getIncludedFiles(),
|
601 |
|
602 | env: result.env,
|
603 | isIsolated: result.isIsolated,
|
604 | isInline: result.isInline,
|
605 | pipeline: internalAsset.value.pipeline,
|
606 | meta: result.meta,
|
607 | uniqueKey: internalAsset.value.uniqueKey,
|
608 | };
|
609 | });
|
610 | }
|
611 |
|
612 | function getImpactfulConfigInfo(configs: ConfigMap) {
|
613 | let impactfulConfigInfo = {};
|
614 |
|
615 | for (let [configType, {devDeps, resultHash}] of configs) {
|
616 | let devDepsObject = {};
|
617 |
|
618 | for (let [moduleName, version] of devDeps) {
|
619 | devDepsObject[moduleName] = version;
|
620 | }
|
621 |
|
622 | impactfulConfigInfo[configType] = {
|
623 | devDeps: devDepsObject,
|
624 | resultHash,
|
625 | };
|
626 | }
|
627 |
|
628 | return impactfulConfigInfo;
|
629 | }
|