UNPKG

16.5 kBJavaScriptView Raw
1// @flow strict-local
2
3import type {
4 MutableAsset as IMutableAsset,
5 FilePath,
6 GenerateOutput,
7 Transformer,
8 TransformerResult,
9 PackageName,
10} from '@parcel/types';
11import type {WorkerApi} from '@parcel/workers';
12import type {
13 Asset as AssetValue,
14 AssetRequestDesc,
15 Config,
16 ConfigRequestDesc,
17 ParcelOptions,
18 ReportFn,
19} from './types';
20
21import invariant from 'assert';
22import path from 'path';
23import nullthrows from 'nullthrows';
24import {md5FromObject} from '@parcel/utils';
25import {PluginLogger} from '@parcel/logger';
26import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic';
27
28import ConfigLoader from './ConfigLoader';
29import {createDependency} from './Dependency';
30import ParcelConfig from './ParcelConfig';
31import ResolverRunner from './ResolverRunner';
32import {MutableAsset, assetToInternalAsset} from './public/Asset';
33import InternalAsset, {createAsset} from './InternalAsset';
34import summarizeRequest from './summarizeRequest';
35import PluginOptions from './public/PluginOptions';
36import {PARCEL_VERSION} from './constants';
37
38type GenerateFunc = (input: IMutableAsset) => Promise<GenerateOutput>;
39
40type PostProcessFunc = (
41 Array<InternalAsset>,
42) => Promise<Array<InternalAsset> | null>;
43
44export type TransformationOpts = {|
45 options: ParcelOptions,
46 report: ReportFn,
47 request: AssetRequestDesc,
48 workerApi: WorkerApi,
49|};
50
51type ConfigMap = Map<PackageName, Config>;
52type ConfigRequestAndResult = {|
53 request: ConfigRequestDesc,
54 result: Config,
55|};
56
57export 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 // TODO: these options may not impact all transformations, let transformers decide if they care or not
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 // If the transformer request passed code rather than a filename,
124 // use a hash as the base for the id to ensure it is unique.
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 // TODO: account for impactfulOptions maybe being different per pipeline
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 // A little hacky
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 // TODO: implement loadPlugin in existing plugins that require config
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
438type 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
450type TransformerWithNameAndConfig = {|
451 name: PackageName,
452 plugin: Transformer,
453 config: ?Config,
454|};
455
456async 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 // Load config for the transformer.
478 let config = preloadedConfig;
479 if (transformer.getConfig) {
480 // TODO: deprecate getConfig
481 config = await transformer.getConfig({
482 asset: new MutableAsset(asset),
483 options: pipeline.pluginOptions,
484 resolve,
485 logger,
486 });
487 }
488
489 // If an ast exists on the asset, but we cannot reuse it,
490 // use the previous transform to generate code that we can re-parse.
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 // Parse if there is no AST available from a previous transform.
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 // Transform.
518 let results = await normalizeAssets(
519 // $FlowFixMe
520 await transformer.transform({
521 asset: new MutableAsset(asset),
522 config,
523 options: pipeline.pluginOptions,
524 resolve,
525 logger,
526 }),
527 );
528
529 // Create generate and postProcess functions that can be called later
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 // For Flow
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
569async 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
584function 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 // $FlowFixMe
599 dependencies: [...internalAsset.value.dependencies.values()],
600 includedFiles: result.getIncludedFiles(),
601 // $FlowFixMe
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
612function 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}