UNPKG

@parcel/core

Version:
510 lines (451 loc) • 13.6 kB
// @flow import type { Glob, Transformer, Resolver, Bundler, Namer, Runtime, PackageName, Optimizer, Compressor, Packager, Reporter, Semver, SemverRange, Validator, FilePath, } from '@parcel/types'; import type { ProcessedParcelConfig, ParcelPluginNode, PureParcelConfigPipeline, ExtendableParcelConfigPipeline, ParcelOptions, } from './types'; import ThrowableDiagnostic, { md, generateJSONCodeHighlights, } from '@parcel/diagnostic'; import json5 from 'json5'; import nullthrows from 'nullthrows'; import {globToRegex} from '@parcel/utils'; import {basename} from 'path'; import loadPlugin from './loadParcelPlugin'; import { type ProjectPath, fromProjectPath, fromProjectPathRelative, toProjectPathUnsafe, } from './projectPath'; type GlobMap<T> = {[Glob]: T, ...}; type SerializedParcelConfig = {| $$raw: boolean, config: ProcessedParcelConfig, options: ParcelOptions, |}; export type LoadedPlugin<T> = {| name: string, version: Semver, plugin: T, resolveFrom: ProjectPath, keyPath?: string, range?: ?SemverRange, |}; export default class ParcelConfig { options: ParcelOptions; filePath: ProjectPath; resolvers: PureParcelConfigPipeline; transformers: GlobMap<ExtendableParcelConfigPipeline>; bundler: ?ParcelPluginNode; namers: PureParcelConfigPipeline; runtimes: PureParcelConfigPipeline; packagers: GlobMap<ParcelPluginNode>; validators: GlobMap<ExtendableParcelConfigPipeline>; optimizers: GlobMap<ExtendableParcelConfigPipeline>; compressors: GlobMap<ExtendableParcelConfigPipeline>; reporters: PureParcelConfigPipeline; pluginCache: Map<PackageName, any>; regexCache: Map<string, RegExp>; constructor(config: ProcessedParcelConfig, options: ParcelOptions) { this.options = options; this.filePath = config.filePath; this.resolvers = config.resolvers || []; this.transformers = config.transformers || {}; this.runtimes = config.runtimes || []; this.bundler = config.bundler; this.namers = config.namers || []; this.packagers = config.packagers || {}; this.optimizers = config.optimizers || {}; this.compressors = config.compressors || {}; this.reporters = config.reporters || []; this.validators = config.validators || {}; this.pluginCache = new Map(); this.regexCache = new Map(); } static deserialize(serialized: SerializedParcelConfig): ParcelConfig { return new ParcelConfig(serialized.config, serialized.options); } getConfig(): ProcessedParcelConfig { return { filePath: this.filePath, resolvers: this.resolvers, transformers: this.transformers, validators: this.validators, runtimes: this.runtimes, bundler: this.bundler, namers: this.namers, packagers: this.packagers, optimizers: this.optimizers, compressors: this.compressors, reporters: this.reporters, }; } serialize(): SerializedParcelConfig { return { $$raw: false, config: this.getConfig(), options: this.options, }; } _loadPlugin<T>(node: ParcelPluginNode): Promise<{| plugin: T, version: Semver, resolveFrom: ProjectPath, range: ?SemverRange, |} | null> { let plugin = this.pluginCache.get(node.packageName); if (plugin) { return plugin; } plugin = loadPlugin<T>( node.packageName, fromProjectPath(this.options.projectRoot, node.resolveFrom), node.keyPath, this.options, ); this.pluginCache.set(node.packageName, plugin); return plugin; } async loadPlugin<T>(node: ParcelPluginNode): Promise<LoadedPlugin<T> | null> { let plugin = await this._loadPlugin(node); if (!plugin) { return null; } return { ...plugin, name: node.packageName, keyPath: node.keyPath, }; } invalidatePlugin(packageName: PackageName) { this.pluginCache.delete(packageName); } async loadPlugins<T>( plugins: PureParcelConfigPipeline, ): Promise<Array<LoadedPlugin<T>>> { return (await Promise.all(plugins.map(p => this.loadPlugin<T>(p)))).filter( Boolean, ); } async getResolvers(): Promise<Array<LoadedPlugin<Resolver<mixed>>>> { if (this.resolvers.length === 0) { throw await this.missingPluginError( this.resolvers, 'No resolver plugins specified in .parcelrc config', '/resolvers', ); } return this.loadPlugins<Resolver<mixed>>(this.resolvers); } _getValidatorNodes(filePath: ProjectPath): $ReadOnlyArray<ParcelPluginNode> { let validators: PureParcelConfigPipeline = this.matchGlobMapPipelines(filePath, this.validators) || []; return validators; } getValidatorNames(filePath: ProjectPath): Array<string> { let validators: PureParcelConfigPipeline = this._getValidatorNodes(filePath); return validators.map(v => v.packageName); } getValidators( filePath: ProjectPath, ): Promise<Array<LoadedPlugin<Validator>>> { let validators = this._getValidatorNodes(filePath); return this.loadPlugins<Validator>(validators); } getNamedPipelines(): $ReadOnlyArray<string> { return Object.keys(this.transformers) .filter(glob => glob.includes(':')) .map(glob => glob.split(':')[0]); } async getTransformers( filePath: ProjectPath, pipeline?: ?string, allowEmpty?: boolean, ): Promise<Array<LoadedPlugin<Transformer<mixed>>>> { let transformers: PureParcelConfigPipeline | null = this.matchGlobMapPipelines(filePath, this.transformers, pipeline); if (!transformers || transformers.length === 0) { if (allowEmpty) { return []; } throw await this.missingPluginError( this.transformers, md`No transformers found for __${fromProjectPathRelative(filePath)}__` + (pipeline != null ? ` with pipeline: '${pipeline}'` : '') + '.', '/transformers', ); } return this.loadPlugins<Transformer<mixed>>(transformers); } async getBundler(): Promise<LoadedPlugin<Bundler<mixed>>> { if (!this.bundler) { throw await this.missingPluginError( [], 'No bundler specified in .parcelrc config', '/bundler', ); } return nullthrows(await this.loadPlugin<Bundler<mixed>>(this.bundler)); } async getNamers(): Promise<Array<LoadedPlugin<Namer<mixed>>>> { if (this.namers.length === 0) { throw await this.missingPluginError( this.namers, 'No namer plugins specified in .parcelrc config', '/namers', ); } return this.loadPlugins<Namer<mixed>>(this.namers); } getRuntimes(): Promise<Array<LoadedPlugin<Runtime<mixed>>>> { if (!this.runtimes) { return Promise.resolve([]); } return this.loadPlugins<Runtime<mixed>>(this.runtimes); } async getPackager( filePath: FilePath, pipeline: ?string, ): Promise<LoadedPlugin<Packager<mixed, mixed>>> { // If a pipeline is specified, but it doesn't exist in the optimizers config, ignore it. // Pipelines for bundles come from their entry assets, so the pipeline likely exists in transformers. if (pipeline) { let prefix = pipeline + ':'; if (!Object.keys(this.packagers).some(glob => glob.startsWith(prefix))) { pipeline = null; } } let packager = this.matchGlobMap( toProjectPathUnsafe(filePath), this.packagers, pipeline, ); if (!packager) { throw await this.missingPluginError( this.packagers, md`No packager found for __${filePath}__.`, '/packagers', ); } return nullthrows(await this.loadPlugin<Packager<mixed, mixed>>(packager)); } _getOptimizerNodes( filePath: FilePath, pipeline: ?string, ): PureParcelConfigPipeline { // If a pipeline is specified, but it doesn't exist in the optimizers config, ignore it. // Pipelines for bundles come from their entry assets, so the pipeline likely exists in transformers. if (pipeline) { let prefix = pipeline + ':'; if (!Object.keys(this.optimizers).some(glob => glob.startsWith(prefix))) { pipeline = null; } } return ( this.matchGlobMapPipelines( toProjectPathUnsafe(filePath), this.optimizers, pipeline, ) ?? [] ); } getOptimizerNames(filePath: FilePath, pipeline: ?string): Array<string> { let optimizers = this._getOptimizerNodes(filePath, pipeline); return optimizers.map(o => o.packageName); } getOptimizers( filePath: FilePath, pipeline: ?string, ): Promise<Array<LoadedPlugin<Optimizer<mixed, mixed>>>> { let optimizers = this._getOptimizerNodes(filePath, pipeline); if (optimizers.length === 0) { return Promise.resolve([]); } return this.loadPlugins<Optimizer<mixed, mixed>>(optimizers); } async getCompressors( filePath: FilePath, ): Promise<Array<LoadedPlugin<Compressor>>> { let compressors = this.matchGlobMapPipelines( toProjectPathUnsafe(filePath), this.compressors, ) ?? []; if (compressors.length === 0) { throw await this.missingPluginError( this.compressors, md`No compressors found for __${filePath}__.`, '/compressors', ); } return this.loadPlugins<Compressor>(compressors); } getReporters(): Promise<Array<LoadedPlugin<Reporter>>> { return this.loadPlugins<Reporter>(this.reporters); } isGlobMatch( projectPath: ProjectPath, pattern: Glob, pipeline?: ?string, ): boolean { // glob's shouldn't be dependant on absolute paths anyway let filePath = fromProjectPathRelative(projectPath); let [patternPipeline, patternGlob] = pattern.split(':'); if (!patternGlob) { patternGlob = patternPipeline; patternPipeline = null; } let re = this.regexCache.get(patternGlob); if (!re) { re = globToRegex(patternGlob, {dot: true, nocase: true}); this.regexCache.set(patternGlob, re); } return ( (pipeline === patternPipeline || (!pipeline && !patternPipeline)) && (re.test(filePath) || re.test(basename(filePath))) ); } matchGlobMap<T>( filePath: ProjectPath, globMap: {|[Glob]: T|}, pipeline?: ?string, ): ?T { for (let pattern in globMap) { if (this.isGlobMatch(filePath, pattern, pipeline)) { return globMap[pattern]; } } return null; } matchGlobMapPipelines( filePath: ProjectPath, globMap: {|[Glob]: ExtendableParcelConfigPipeline|}, pipeline?: ?string, ): PureParcelConfigPipeline { let matches = []; if (pipeline) { // If a pipeline is requested, a the glob needs to match exactly let exactMatch; for (let pattern in globMap) { if (this.isGlobMatch(filePath, pattern, pipeline)) { exactMatch = globMap[pattern]; break; } } if (!exactMatch) { return []; } else { matches.push(exactMatch); } } for (let pattern in globMap) { if (this.isGlobMatch(filePath, pattern)) { matches.push(globMap[pattern]); } } let flatten = () => { let pipeline = matches.shift() || []; let spreadIndex = pipeline.indexOf('...'); if (spreadIndex >= 0) { pipeline = [ ...pipeline.slice(0, spreadIndex), ...flatten(), ...pipeline.slice(spreadIndex + 1), ]; } if (pipeline.includes('...')) { throw new Error( 'Only one spread parameter can be included in a config pipeline', ); } return pipeline; }; let res = flatten(); // $FlowFixMe afaik this should work return res; } async missingPluginError( plugins: | GlobMap<ExtendableParcelConfigPipeline> | GlobMap<ParcelPluginNode> | PureParcelConfigPipeline, message: string, key: string, ): Promise<ThrowableDiagnostic> { let configsWithPlugin; if (Array.isArray(plugins)) { configsWithPlugin = new Set(getConfigPaths(this.options, plugins)); } else { configsWithPlugin = new Set( Object.keys(plugins).flatMap(k => Array.isArray(plugins[k]) ? getConfigPaths(this.options, plugins[k]) : [getConfigPath(this.options, plugins[k])], ), ); } if (configsWithPlugin.size === 0) { configsWithPlugin.add( fromProjectPath(this.options.projectRoot, this.filePath), ); } let seenKey = false; let codeFrames = await Promise.all( [...configsWithPlugin].map(async filePath => { let configContents = await this.options.inputFS.readFile( filePath, 'utf8', ); if (!json5.parse(configContents)[key.slice(1)]) { key = ''; } else { seenKey = true; } return { filePath, code: configContents, codeHighlights: generateJSONCodeHighlights(configContents, [{key}]), }; }), ); return new ThrowableDiagnostic({ diagnostic: { message, origin: '@parcel/core', codeFrames, hints: !seenKey ? ['Try extending __@parcel/config-default__'] : [], }, }); } } function getConfigPaths(options, nodes) { return nodes .map(node => (node !== '...' ? getConfigPath(options, node) : null)) .filter(Boolean); } function getConfigPath(options, node) { return fromProjectPath(options.projectRoot, node.resolveFrom); }