UNPKG

@parcel/core

Version:
759 lines (686 loc) • 21.2 kB
// @flow strict-local import type { FilePath, GenerateOutput, Transformer, TransformerResult, PackageName, ResolveOptions, SemverRange, } from '@parcel/types'; import type {WorkerApi} from '@parcel/workers'; import type { Asset as AssetValue, TransformationRequest, Config, DevDepRequest, DevDepRequestRef, ParcelOptions, InternalDevDepOptions, Invalidations, } from './types'; import type {LoadedPlugin} from './ParcelConfig'; import path from 'path'; import {Readable} from 'stream'; import nullthrows from 'nullthrows'; import logger, {PluginLogger} from '@parcel/logger'; import ThrowableDiagnostic, { anyToDiagnostic, errorToDiagnostic, escapeMarkdown, md, type Diagnostic, } from '@parcel/diagnostic'; import {SOURCEMAP_EXTENSIONS} from '@parcel/utils'; import {hashString} from '@parcel/rust'; import {createDependency} from './Dependency'; import ParcelConfig from './ParcelConfig'; // TODO: eventually call path request as sub requests import {ResolverRunner} from './requests/PathRequest'; import { Asset, MutableAsset, mutableAssetToUncommittedAsset, } from './public/Asset'; import UncommittedAsset from './UncommittedAsset'; import {createAsset} from './assetUtils'; import summarizeRequest from './summarizeRequest'; import PluginOptions from './public/PluginOptions'; import {optionsProxy} from './utils'; import {createConfig} from './InternalConfig'; import { loadPluginConfig, getConfigRequests, type ConfigRequest, } from './requests/ConfigRequest'; import { createDevDependency, invalidateDevDeps, getWorkerDevDepRequests, } from './requests/DevDepRequest'; import { type ProjectPath, fromProjectPath, fromProjectPathRelative, toProjectPathUnsafe, toProjectPath, } from './projectPath'; import {invalidateOnFileCreateToInternal, createInvalidations} from './utils'; import invariant from 'assert'; import {tracer, PluginTracer} from '@parcel/profiler'; type GenerateFunc = (input: UncommittedAsset) => Promise<GenerateOutput>; type PostProcessFunc = ( Array<UncommittedAsset>, ) => Promise<Array<UncommittedAsset> | null>; export type TransformationOpts = {| options: ParcelOptions, config: ParcelConfig, request: TransformationRequest, workerApi: WorkerApi, |}; export type TransformationResult = {| assets?: Array<AssetValue>, error?: Array<Diagnostic>, configRequests: Array<ConfigRequest>, invalidations: Invalidations, devDepRequests: Array<DevDepRequest | DevDepRequestRef>, |}; export default class Transformation { request: TransformationRequest; configs: Map<string, Config>; devDepRequests: Map<string, DevDepRequest | DevDepRequestRef>; pluginDevDeps: Array<InternalDevDepOptions>; options: ParcelOptions; pluginOptions: PluginOptions; workerApi: WorkerApi; parcelConfig: ParcelConfig; invalidations: Invalidations; resolverRunner: ResolverRunner; constructor({request, options, config, workerApi}: TransformationOpts) { this.configs = new Map(); this.parcelConfig = config; this.options = options; this.request = request; this.workerApi = workerApi; this.invalidations = createInvalidations(); this.devDepRequests = new Map(); this.pluginDevDeps = []; this.resolverRunner = new ResolverRunner({ config, options, previousDevDeps: request.devDeps, }); this.pluginOptions = new PluginOptions( optionsProxy( this.options, option => { this.invalidations.invalidateOnOptionChange.add(option); }, devDep => { this.pluginDevDeps.push(devDep); }, ), ); } async run(): Promise<TransformationResult> { let asset = await this.loadAsset(); let existing; if (!asset.mapBuffer && SOURCEMAP_EXTENSIONS.has(asset.value.type)) { // Load existing sourcemaps, this automatically runs the source contents extraction try { existing = await asset.loadExistingSourcemap(); } catch (err) { logger.verbose([ { origin: '@parcel/core', message: md`Could not load existing source map for ${fromProjectPathRelative( asset.value.filePath, )}`, }, { origin: '@parcel/core', message: escapeMarkdown(err.message), }, ]); } } if ( existing == null && // Don't buffer an entire stream into memory since it may not need sourceContent, // e.g. large binary files !(asset.content instanceof Readable) ) { // If no existing sourcemap was found, initialize asset.sourceContent // with the original contents. This will be used when the transformer // calls setMap to ensure the source content is in the sourcemap. asset.sourceContent = await asset.getCode(); } invalidateDevDeps( this.request.invalidDevDeps, this.options, this.parcelConfig, ); let pipeline = await this.loadPipeline( this.request.filePath, asset.value.isSource, asset.value.pipeline, ); let assets, error; try { let results = await this.runPipelines(pipeline, asset); await Promise.all(results.map(asset => asset.commit())); assets = results.map(a => a.value); } catch (e) { error = e; } let configRequests = getConfigRequests([ ...this.configs.values(), ...this.resolverRunner.configs.values(), ]); let devDepRequests = getWorkerDevDepRequests([ ...this.devDepRequests.values(), ...this.resolverRunner.devDepRequests.values(), ]); // $FlowFixMe because of $$raw return { $$raw: true, assets, configRequests, // When throwing an error, this (de)serialization is done automatically by the WorkerFarm error: error ? anyToDiagnostic(error) : undefined, invalidations: this.invalidations, devDepRequests, }; } async loadAsset(): Promise<UncommittedAsset> { let { filePath, env, code, pipeline, isSource: isSourceOverride, sideEffects, query, } = this.request; let { content, size, isSource: summarizedIsSource, } = await summarizeRequest(this.options.inputFS, { filePath: fromProjectPath(this.options.projectRoot, filePath), code, }); // Prefer `isSource` originating from the AssetRequest. let isSource = isSourceOverride ?? summarizedIsSource; // If the transformer request passed code, use a hash in addition // to the filename as the base for the id to ensure it is unique. let idBase = fromProjectPathRelative(filePath); if (code != null) { idBase += hashString(code); } return new UncommittedAsset({ idBase, value: createAsset(this.options.projectRoot, { idBase, filePath, isSource, type: path.extname(fromProjectPathRelative(filePath)).slice(1), pipeline, env, query, stats: { time: 0, size, }, sideEffects, }), options: this.options, content, invalidations: this.invalidations, }); } async runPipelines( pipeline: Pipeline, initialAsset: UncommittedAsset, ): Promise<Array<UncommittedAsset>> { let initialType = initialAsset.value.type; let assets: Array<UncommittedAsset>; try { assets = await this.runPipeline(pipeline, initialAsset); } finally { // Add dev dep requests for each transformer for (let transformer of pipeline.transformers) { await this.addDevDependency({ specifier: transformer.name, resolveFrom: transformer.resolveFrom, range: transformer.range, }); } // Add dev dep requests for dependencies of transformer plugins // (via proxied packageManager.require calls). for (let devDep of this.pluginDevDeps) { await this.addDevDependency(devDep); } } let finalAssets: Array<UncommittedAsset> = []; for (let asset of assets) { let nextPipeline; if (asset.value.type !== initialType) { nextPipeline = await this.loadNextPipeline({ filePath: initialAsset.value.filePath, isSource: asset.value.isSource, newType: asset.value.type, newPipeline: asset.value.pipeline, currentPipeline: pipeline, }); } if (nextPipeline) { let nextPipelineAssets = await this.runPipelines(nextPipeline, asset); finalAssets = finalAssets.concat(nextPipelineAssets); } else { finalAssets.push(asset); } } if (!pipeline.postProcess) { return finalAssets; } invariant(pipeline.postProcess != null); let processedFinalAssets: Array<UncommittedAsset> = (await pipeline.postProcess(finalAssets)) ?? []; return processedFinalAssets; } async addDevDependency(opts: InternalDevDepOptions): Promise<void> { let {specifier, resolveFrom, range} = opts; let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; if (this.devDepRequests.has(key)) { return; } // Ensure that the package manager has an entry for this resolution. try { await this.options.packageManager.resolve( specifier, fromProjectPath(this.options.projectRoot, opts.resolveFrom), { range, }, ); } catch (err) { // ignore } let devDepRequest = await createDevDependency( opts, this.request.devDeps, this.options, ); this.devDepRequests.set(key, devDepRequest); } async runPipeline( pipeline: Pipeline, initialAsset: UncommittedAsset, ): Promise<Array<UncommittedAsset>> { if (pipeline.transformers.length === 0) { return [initialAsset]; } let initialType = initialAsset.value.type; let inputAssets = [initialAsset]; let resultingAssets = []; let finalAssets = []; for (let transformer of pipeline.transformers) { resultingAssets = []; for (let asset of inputAssets) { if ( asset.value.type !== initialType && (await this.loadNextPipeline({ filePath: initialAsset.value.filePath, isSource: asset.value.isSource, newType: asset.value.type, newPipeline: asset.value.pipeline, currentPipeline: pipeline, })) ) { finalAssets.push(asset); continue; } try { const measurement = tracer.createMeasurement( transformer.name, 'transform', fromProjectPathRelative(initialAsset.value.filePath), ); let transformerResults = await this.runTransformer( pipeline, asset, transformer.plugin, transformer.name, transformer.config, transformer.configKeyPath, this.parcelConfig, ); measurement && measurement.end(); for (let result of transformerResults) { if (result instanceof UncommittedAsset) { resultingAssets.push(result); continue; } resultingAssets.push( asset.createChildAsset( result, transformer.name, this.parcelConfig.filePath, transformer.configKeyPath, ), ); } } catch (e) { let diagnostic = errorToDiagnostic(e, { origin: transformer.name, filePath: fromProjectPath( this.options.projectRoot, asset.value.filePath, ), }); // If this request is a virtual asset that might not exist on the filesystem, // add the `code` property to each code frame in the diagnostics that match the // request's filepath. This can't be done by the transformer because it might not // have access to the original code (e.g. an inline script tag in HTML). if (this.request.code != null) { for (let d of diagnostic) { if (d.codeFrames) { for (let codeFrame of d.codeFrames) { if ( codeFrame.code == null && codeFrame.filePath === this.request.filePath ) { codeFrame.code = this.request.code; } } } } } throw new ThrowableDiagnostic({ diagnostic, }); } } inputAssets = resultingAssets; } // Make assets with ASTs generate unless they are CSS modules. This parallelizes generation // and distributes work more evenly across workers than if one worker needed to // generate all assets in a large bundle during packaging. await Promise.all( resultingAssets .filter( asset => asset.ast != null && !( this.options.mode === 'production' && asset.value.type === 'css' && asset.value.symbols ), ) .map(async asset => { if (asset.isASTDirty && asset.generate) { let output = await asset.generate(); asset.content = output.content; asset.mapBuffer = output.map?.toBuffer(); } asset.clearAST(); }), ); return finalAssets.concat(resultingAssets); } async loadPipeline( filePath: ProjectPath, isSource: boolean, pipeline: ?string, ): Promise<Pipeline> { let transformers = await this.parcelConfig.getTransformers( filePath, pipeline, this.request.isURL, ); for (let transformer of transformers) { let config = await this.loadTransformerConfig(transformer, isSource); if (config) { this.configs.set(transformer.name, config); } } return { id: transformers.map(t => t.name).join(':'), transformers: transformers.map(transformer => ({ name: transformer.name, resolveFrom: transformer.resolveFrom, config: this.configs.get(transformer.name)?.result, configKeyPath: transformer.keyPath, plugin: transformer.plugin, })), options: this.options, pluginOptions: this.pluginOptions, workerApi: this.workerApi, }; } async loadNextPipeline({ filePath, isSource, newType, newPipeline, currentPipeline, }: {| filePath: ProjectPath, isSource: boolean, newType: string, newPipeline: ?string, currentPipeline: Pipeline, |}): Promise<?Pipeline> { let filePathRelative = fromProjectPathRelative(filePath); let nextFilePath = toProjectPathUnsafe( filePathRelative.slice(0, -path.extname(filePathRelative).length) + '.' + newType, ); let nextPipeline = await this.loadPipeline( nextFilePath, isSource, newPipeline, ); if (nextPipeline.id === currentPipeline.id) { return null; } return nextPipeline; } async loadTransformerConfig( transformer: LoadedPlugin<Transformer<mixed>>, isSource: boolean, ): Promise<?Config> { let loadConfig = transformer.plugin.loadConfig; if (!loadConfig) { return; } let config = createConfig({ plugin: transformer.name, isSource, searchPath: this.request.filePath, env: this.request.env, }); await loadPluginConfig(transformer, config, this.options); for (let devDep of config.devDeps) { await this.addDevDependency(devDep); } return config; } async runTransformer( pipeline: Pipeline, asset: UncommittedAsset, transformer: Transformer<mixed>, transformerName: string, preloadedConfig: ?Config, configKeyPath?: string, parcelConfig: ParcelConfig, ): Promise<$ReadOnlyArray<TransformerResult | UncommittedAsset>> { if (asset.transformers.has(transformerName)) { return [asset]; } asset.transformers.add(transformerName); const logger = new PluginLogger({origin: transformerName}); const tracer = new PluginTracer({ origin: transformerName, category: 'transform', }); const resolve = async ( from: FilePath, to: string, options?: ResolveOptions, ): Promise<FilePath> => { let result = await this.resolverRunner.resolve( createDependency(this.options.projectRoot, { env: asset.value.env, specifier: to, specifierType: options?.specifierType || 'esm', packageConditions: options?.packageConditions, sourcePath: from, }), ); if (result.invalidateOnFileCreate) { this.invalidations.invalidateOnFileCreate.push( ...result.invalidateOnFileCreate.map(i => invalidateOnFileCreateToInternal(this.options.projectRoot, i), ), ); } if (result.invalidateOnFileChange) { for (let filePath of result.invalidateOnFileChange) { this.invalidations.invalidateOnFileChange.add( toProjectPath(this.options.projectRoot, filePath), ); } } if (result.diagnostics && result.diagnostics.length > 0) { throw new ThrowableDiagnostic({diagnostic: result.diagnostics}); } return fromProjectPath( this.options.projectRoot, nullthrows(result.assetGroup).filePath, ); }; // If an ast exists on the asset, but we cannot reuse it, // use the previous transform to generate code that we can re-parse. if ( asset.ast && asset.isASTDirty && (!transformer.canReuseAST || !transformer.canReuseAST({ ast: asset.ast, options: pipeline.pluginOptions, logger, tracer, })) && asset.generate ) { let output = await asset.generate(); asset.content = output.content; asset.mapBuffer = output.map?.toBuffer(); } // Load config for the transformer. let config = preloadedConfig; // Parse if there is no AST available from a previous transform. let parse = transformer.parse?.bind(transformer); if (!asset.ast && parse) { let ast = await parse({ asset: new Asset(asset), config, options: pipeline.pluginOptions, resolve, logger, tracer, }); if (ast) { asset.setAST(ast); asset.isASTDirty = false; } } // Transform. let transfomerResult: Array<TransformerResult | MutableAsset> = // $FlowFixMe the returned IMutableAsset really is a MutableAsset await transformer.transform({ asset: new MutableAsset(asset), config, options: pipeline.pluginOptions, resolve, logger, tracer, }); let results = await normalizeAssets(this.options, transfomerResult); // Create generate and postProcess function that can be called later asset.generate = (): Promise<GenerateOutput> => { let publicAsset = new Asset(asset); if (transformer.generate && asset.ast) { let generated = transformer.generate({ asset: publicAsset, ast: asset.ast, options: pipeline.pluginOptions, logger, tracer, }); asset.clearAST(); return Promise.resolve(generated); } throw new Error( 'Asset has an AST but no generate method is available on the transform', ); }; let postProcess = transformer.postProcess; if (postProcess) { pipeline.postProcess = async ( assets: Array<UncommittedAsset>, ): Promise<Array<UncommittedAsset> | null> => { let results = await postProcess.call(transformer, { assets: assets.map(asset => new MutableAsset(asset)), config, options: pipeline.pluginOptions, resolve, logger, tracer, }); return Promise.all( results.map(result => asset.createChildAsset( result, transformerName, parcelConfig.filePath, // configKeyPath, ), ), ); }; } return results; } } type Pipeline = {| id: string, transformers: Array<TransformerWithNameAndConfig>, options: ParcelOptions, pluginOptions: PluginOptions, workerApi: WorkerApi, postProcess?: PostProcessFunc, generate?: GenerateFunc, |}; type TransformerWithNameAndConfig = {| name: PackageName, plugin: Transformer<mixed>, config: ?Config, configKeyPath?: string, resolveFrom: ProjectPath, range?: ?SemverRange, |}; function normalizeAssets( options, results: Array<TransformerResult | MutableAsset>, ): Array<TransformerResult | UncommittedAsset> { return results.map(result => { if (result instanceof MutableAsset) { return mutableAssetToUncommittedAsset(result); } return result; }); }