import type { Glob, Transformer, Resolver, Bundler, Namer, Runtime, PackageName, Optimizer, Compressor, Packager, Reporter, Semver, Validator, FilePath, } from '@atlaspack/types'; import type { ProcessedAtlaspackConfig, AtlaspackPluginNode, PureAtlaspackConfigPipeline, ExtendableAtlaspackConfigPipeline, AtlaspackOptions, } from './types'; import ThrowableDiagnostic, { md, generateJSONCodeHighlights, } from '@atlaspack/diagnostic'; import json5 from 'json5'; import {globToRegex} from '@atlaspack/utils'; import {basename} from 'path'; import loadPlugin from './loadAtlaspackPlugin'; import { ProjectPath, fromProjectPath, fromProjectPathRelative, toProjectPathUnsafe, } from './projectPath'; type GlobMap = Partial>; type SerializedAtlaspackConfig = { $$raw: boolean; config: ProcessedAtlaspackConfig; options: AtlaspackOptions; }; export type LoadedPlugin = { name: string; version: Semver; plugin: T; resolveFrom: ProjectPath; keyPath?: string; }; export class AtlaspackConfig { options: AtlaspackOptions; filePath: ProjectPath; resolvers: PureAtlaspackConfigPipeline; transformers: GlobMap; bundler: AtlaspackPluginNode | null | undefined; namers: PureAtlaspackConfigPipeline; runtimes: PureAtlaspackConfigPipeline; packagers: GlobMap; validators: GlobMap; optimizers: GlobMap; compressors: GlobMap; reporters: PureAtlaspackConfigPipeline; pluginCache: Map; regexCache: Map; constructor(config: ProcessedAtlaspackConfig, options: AtlaspackOptions) { 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: SerializedAtlaspackConfig): AtlaspackConfig { return new AtlaspackConfig(serialized.config, serialized.options); } getConfig(): ProcessedAtlaspackConfig { 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(): SerializedAtlaspackConfig { return { $$raw: false, config: this.getConfig(), options: this.options, }; } _loadPlugin(node: AtlaspackPluginNode): Promise<{ plugin: T; version: Semver; resolveFrom: ProjectPath; }> { let plugin = this.pluginCache.get(node.packageName); if (plugin) { return plugin; } plugin = loadPlugin( node.packageName, fromProjectPath(this.options.projectRoot, node.resolveFrom), node.keyPath, this.options, ); this.pluginCache.set(node.packageName, plugin); return plugin; } async loadPlugin(node: AtlaspackPluginNode): Promise> { let plugin = await this._loadPlugin(node); // @ts-expect-error TS2322 return { ...plugin, name: node.packageName, keyPath: node.keyPath, }; } invalidatePlugin(packageName: PackageName) { this.pluginCache.delete(packageName); } loadPlugins( plugins: PureAtlaspackConfigPipeline, ): Promise>> { return Promise.all(plugins.map((p) => this.loadPlugin(p))); } async getResolvers(): Promise>>> { if (this.resolvers.length === 0) { throw await this.missingPluginError( this.resolvers, 'No resolver plugins specified in .parcelrc config', '/resolvers', ); } return this.loadPlugins>(this.resolvers); } _getValidatorNodes( filePath: ProjectPath, ): ReadonlyArray { let validators: PureAtlaspackConfigPipeline = this.matchGlobMapPipelines(filePath, this.validators) || []; return validators; } getValidatorNames(filePath: ProjectPath): Array { let validators: PureAtlaspackConfigPipeline = this._getValidatorNodes(filePath); return validators.map((v) => v.packageName); } getValidators( filePath: ProjectPath, ): Promise>> { let validators = this._getValidatorNodes(filePath); return this.loadPlugins(validators); } getNamedPipelines(): ReadonlyArray { return Object.keys(this.transformers) .filter((glob) => glob.includes(':')) .map((glob) => glob.split(':')[0]); } async getTransformers( filePath: ProjectPath, pipeline?: string | null, allowEmpty?: boolean, ): Promise>>> { let transformers: PureAtlaspackConfigPipeline | 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>(transformers); } async getBundler(): Promise>> { if (!this.bundler) { throw await this.missingPluginError( [], 'No bundler specified in .parcelrc config', '/bundler', ); } return this.loadPlugin>(this.bundler); } async getNamers(): Promise>>> { if (this.namers.length === 0) { throw await this.missingPluginError( this.namers, 'No namer plugins specified in .parcelrc config', '/namers', ); } return this.loadPlugins>(this.namers); } getRuntimes(): Promise>>> { if (!this.runtimes) { return Promise.resolve([]); } return this.loadPlugins>(this.runtimes); } async getPackager( filePath: FilePath, ): Promise>> { let packager = this.matchGlobMap( toProjectPathUnsafe(filePath), this.packagers, ); if (!packager) { throw await this.missingPluginError( this.packagers, md`No packager found for __${filePath}__.`, '/packagers', ); } return this.loadPlugin>(packager); } _getOptimizerNodes( filePath: FilePath, pipeline?: string | null, ): PureAtlaspackConfigPipeline { // 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 | null, ): Array { let optimizers = this._getOptimizerNodes(filePath, pipeline); return optimizers.map((o) => o.packageName); } getOptimizers( filePath: FilePath, pipeline?: string | null, ): Promise>>> { let optimizers = this._getOptimizerNodes(filePath, pipeline); if (optimizers.length === 0) { return Promise.resolve([]); } return this.loadPlugins>(optimizers); } async getCompressors( filePath: FilePath, ): Promise>> { 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(compressors); } getReporters(): Promise>> { return this.loadPlugins(this.reporters); } isGlobMatch( projectPath: ProjectPath, pattern: Glob, pipeline?: string | null, ): boolean { // glob's shouldn't be dependant on absolute paths anyway let filePath = fromProjectPathRelative(projectPath); let [patternPipeline, patternGlob] = pattern.split(':'); if (!patternGlob) { patternGlob = patternPipeline; // @ts-expect-error TS2322 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( filePath: ProjectPath, globMap: Partial>, ): T | null | undefined { for (let pattern in globMap) { if (this.isGlobMatch(filePath, pattern)) { return globMap[pattern]; } } return null; } matchGlobMapPipelines( filePath: ProjectPath, globMap: Partial>, pipeline?: string | null, ): PureAtlaspackConfigPipeline { let matches: Array = []; 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)) { // @ts-expect-error TS2345 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(); // @ts-expect-error TS2322 return res; } async missingPluginError( plugins: | GlobMap | GlobMap | PureAtlaspackConfigPipeline, message: string, key: string, ): Promise { let configsWithPlugin; if (Array.isArray(plugins)) { configsWithPlugin = new Set(getConfigPaths(this.options, plugins)); } else { configsWithPlugin = new Set( Object.keys(plugins).flatMap((k) => // @ts-expect-error TS7053 Array.isArray(plugins[k]) ? // @ts-expect-error TS7053 getConfigPaths(this.options, plugins[k]) : // @ts-expect-error TS7053 [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: '@atlaspack/core', codeFrames, hints: !seenKey ? ['Try extending __@atlaspack/config-default__'] : [], }, }); } } function getConfigPaths( options: AtlaspackOptions, nodes: | AtlaspackPluginNode | PureAtlaspackConfigPipeline | ExtendableAtlaspackConfigPipeline, ) { return ( nodes // @ts-expect-error TS2339 .map((node) => (node !== '...' ? getConfigPath(options, node) : null)) .filter(Boolean) ); } function getConfigPath( options: AtlaspackOptions, node: AtlaspackPluginNode | ExtendableAtlaspackConfigPipeline, ) { // @ts-expect-error TS2339 return fromProjectPath(options.projectRoot, node.resolveFrom); }