UNPKG

@parcel/core

Version:
711 lines (648 loc) • 18.2 kB
// @flow strict-local import type { Async, FilePath, PackageName, RawParcelConfig, ResolvedParcelConfigFile, } from '@parcel/types'; import type {FileSystem} from '@parcel/fs'; import type {StaticRunOpts} from '../RequestTracker'; import type { ExtendableParcelConfigPipeline, PureParcelConfigPipeline, ParcelOptions, ProcessedParcelConfig, } from '../types'; import { isDirectoryInside, hashObject, resolveConfig, validateSchema, findAlternativeNodeModules, findAlternativeFiles, } from '@parcel/utils'; import ThrowableDiagnostic, { generateJSONCodeHighlights, escapeMarkdown, md, errorToDiagnostic, } from '@parcel/diagnostic'; import {parse} from 'json5'; import path from 'path'; import invariant from 'assert'; import ParcelConfigSchema from '../ParcelConfig.schema'; import {optionsProxy} from '../utils'; import ParcelConfig from '../ParcelConfig'; import {createBuildCache} from '../buildCache'; import {toProjectPath} from '../projectPath'; import {requestTypes} from '../RequestTracker'; type ConfigMap<K, V> = {[K]: V, ...}; export type ConfigAndCachePath = {| config: ProcessedParcelConfig, cachePath: string, |}; type RunOpts<TResult> = {| input: null, ...StaticRunOpts<TResult>, |}; export type ParcelConfigRequest = {| id: string, type: typeof requestTypes.parcel_config_request, input: null, run: (RunOpts<ParcelConfigRequestResult>) => Async<ParcelConfigRequestResult>, |}; export type ParcelConfigRequestResult = ConfigAndCachePath; type ParcelConfigChain = {| config: ProcessedParcelConfig, extendedFiles: Array<FilePath>, |}; const type = 'parcel_config_request'; export default function createParcelConfigRequest(): ParcelConfigRequest { return { id: type, type: requestTypes[type], async run({api, options}) { let { config, extendedFiles, usedDefault, }: {| ...ParcelConfigChain, usedDefault: boolean, |} = await loadParcelConfig( optionsProxy(options, api.invalidateOnOptionChange), ); api.invalidateOnFileUpdate(config.filePath); api.invalidateOnFileDelete(config.filePath); for (let filePath of extendedFiles) { let pp = toProjectPath(options.projectRoot, filePath); api.invalidateOnFileUpdate(pp); api.invalidateOnFileDelete(pp); } if (usedDefault) { let resolveFrom = getResolveFrom(options.inputFS, options.projectRoot); api.invalidateOnFileCreate({ fileName: '.parcelrc', aboveFilePath: toProjectPath(options.projectRoot, resolveFrom), }); } let cachePath = hashObject(config); await options.cache.set(cachePath, config); let result = {config, cachePath}; // TODO: don't store config twice (once in the graph and once in a separate cache entry) api.storeResult(result); return result; }, input: null, }; } const parcelConfigCache = createBuildCache(); export function getCachedParcelConfig( result: ConfigAndCachePath, options: ParcelOptions, ): ParcelConfig { let {config: processedConfig, cachePath} = result; let config = parcelConfigCache.get(cachePath); if (config) { return config; } config = new ParcelConfig(processedConfig, options); parcelConfigCache.set(cachePath, config); return config; } export async function loadParcelConfig( options: ParcelOptions, ): Promise<{|...ParcelConfigChain, usedDefault: boolean|}> { let parcelConfig = await resolveParcelConfig(options); if (!parcelConfig) { throw new Error('Could not find a .parcelrc'); } return parcelConfig; } export async function resolveParcelConfig( options: ParcelOptions, ): Promise<?{|...ParcelConfigChain, usedDefault: boolean|}> { let resolveFrom = getResolveFrom(options.inputFS, options.projectRoot); let configPath = options.config != null ? (await options.packageManager.resolve(options.config, resolveFrom)) .resolved : await resolveConfig( options.inputFS, resolveFrom, ['.parcelrc'], options.projectRoot, ); let usedDefault = false; if (configPath == null && options.defaultConfig != null) { usedDefault = true; configPath = ( await options.packageManager.resolve(options.defaultConfig, resolveFrom) ).resolved; } if (configPath == null) { return null; } let contents; try { contents = await options.inputFS.readFile(configPath, 'utf8'); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: { message: md`Could not find parcel config at ${path.relative( options.projectRoot, configPath, )}`, origin: '@parcel/core', }, }); } let {config, extendedFiles}: ParcelConfigChain = await parseAndProcessConfig( configPath, contents, options, ); if (options.additionalReporters.length > 0) { config.reporters = [ ...options.additionalReporters.map(({packageName, resolveFrom}) => ({ packageName, resolveFrom, })), ...(config.reporters ?? []), ]; } return {config, extendedFiles, usedDefault}; } export function create( config: ResolvedParcelConfigFile, options: ParcelOptions, ): Promise<ParcelConfigChain> { return processConfigChain(config, config.filePath, options); } // eslint-disable-next-line require-await export async function parseAndProcessConfig( configPath: FilePath, contents: string, options: ParcelOptions, ): Promise<ParcelConfigChain> { let config: RawParcelConfig; try { config = parse(contents); } catch (e) { let pos = { line: e.lineNumber, column: e.columnNumber, }; throw new ThrowableDiagnostic({ diagnostic: { message: `Failed to parse .parcelrc`, origin: '@parcel/core', codeFrames: [ { filePath: configPath, language: 'json5', code: contents, codeHighlights: [ { start: pos, end: pos, message: escapeMarkdown(e.message), }, ], }, ], }, }); } return processConfigChain(config, configPath, options); } function processPipeline( options: ParcelOptions, pipeline: ?Array<PackageName>, keyPath: string, filePath: FilePath, ) { if (pipeline) { return pipeline.map((pkg, i) => { // $FlowFixMe if (pkg === '...') return pkg; return { packageName: pkg, resolveFrom: toProjectPath(options.projectRoot, filePath), keyPath: `${keyPath}/${i}`, }; }); } } const RESERVED_PIPELINES = new Set([ 'node:', 'npm:', 'http:', 'https:', 'data:', 'tel:', 'mailto:', ]); async function processMap( // $FlowFixMe map: ?ConfigMap<any, any>, keyPath: string, filePath: FilePath, options: ParcelOptions, // $FlowFixMe ): Promise<ConfigMap<any, any> | typeof undefined> { if (!map) return undefined; // $FlowFixMe let res: ConfigMap<any, any> = {}; for (let k in map) { let i = k.indexOf(':'); if (i > 0 && RESERVED_PIPELINES.has(k.slice(0, i + 1))) { let code = await options.inputFS.readFile(filePath, 'utf8'); throw new ThrowableDiagnostic({ diagnostic: { message: `Named pipeline '${k.slice(0, i + 1)}' is reserved.`, origin: '@parcel/core', codeFrames: [ { filePath: filePath, language: 'json5', code, codeHighlights: generateJSONCodeHighlights(code, [ { key: `${keyPath}/${k}`, type: 'key', }, ]), }, ], documentationURL: 'https://parceljs.org/features/dependency-resolution/#url-schemes', }, }); } if (typeof map[k] === 'string') { res[k] = { packageName: map[k], resolveFrom: toProjectPath(options.projectRoot, filePath), keyPath: `${keyPath}/${k}`, }; } else { res[k] = processPipeline(options, map[k], `${keyPath}/${k}`, filePath); } } return res; } export async function processConfig( configFile: ResolvedParcelConfigFile, options: ParcelOptions, ): Promise<ProcessedParcelConfig> { return { filePath: toProjectPath(options.projectRoot, configFile.filePath), ...(configFile.resolveFrom != null ? { resolveFrom: toProjectPath( options.projectRoot, configFile.resolveFrom, ), } : { /*::...null*/ }), resolvers: processPipeline( options, configFile.resolvers, '/resolvers', configFile.filePath, ), transformers: await processMap( configFile.transformers, '/transformers', configFile.filePath, options, ), bundler: configFile.bundler != null ? { packageName: configFile.bundler, resolveFrom: toProjectPath( options.projectRoot, configFile.filePath, ), keyPath: '/bundler', } : undefined, namers: processPipeline( options, configFile.namers, '/namers', configFile.filePath, ), runtimes: processPipeline( options, configFile.runtimes, '/runtimes', configFile.filePath, ), packagers: await processMap( configFile.packagers, '/packagers', configFile.filePath, options, ), optimizers: await processMap( configFile.optimizers, '/optimizers', configFile.filePath, options, ), compressors: await processMap( configFile.compressors, '/compressors', configFile.filePath, options, ), reporters: processPipeline( options, configFile.reporters, '/reporters', configFile.filePath, ), validators: await processMap( configFile.validators, '/validators', configFile.filePath, options, ), }; } export async function processConfigChain( configFile: RawParcelConfig | ResolvedParcelConfigFile, filePath: FilePath, options: ParcelOptions, ): Promise<ParcelConfigChain> { // Validate config... let relativePath = path.relative(options.inputFS.cwd(), filePath); validateConfigFile(configFile, relativePath); // Process config... let config: ProcessedParcelConfig = await processConfig( { filePath, ...configFile, }, options, ); let extendedFiles: Array<FilePath> = []; if (configFile.extends != null) { let exts = Array.isArray(configFile.extends) ? configFile.extends : [configFile.extends]; let errors = []; if (exts.length !== 0) { let extStartConfig; let i = 0; for (let ext of exts) { try { let key = Array.isArray(configFile.extends) ? `/extends/${i}` : '/extends'; let resolved = await resolveExtends(ext, filePath, key, options); extendedFiles.push(resolved); let {extendedFiles: moreExtendedFiles, config: nextConfig} = await processExtendedConfig(filePath, key, ext, resolved, options); extendedFiles = extendedFiles.concat(moreExtendedFiles); extStartConfig = extStartConfig ? mergeConfigs(extStartConfig, nextConfig) : nextConfig; } catch (err) { errors.push(err); } i++; } // Merge with the inline config last if (extStartConfig) { config = mergeConfigs(extStartConfig, config); } } if (errors.length > 0) { throw new ThrowableDiagnostic({ diagnostic: errors.flatMap(e => e.diagnostics ?? errorToDiagnostic(e)), }); } } return {config, extendedFiles}; } export async function resolveExtends( ext: string, configPath: FilePath, extendsKey: string, options: ParcelOptions, ): Promise<FilePath> { if (ext.startsWith('.')) { return path.resolve(path.dirname(configPath), ext); } else { try { let {resolved} = await options.packageManager.resolve(ext, configPath); return options.inputFS.realpath(resolved); } catch (err) { let parentContents = await options.inputFS.readFile(configPath, 'utf8'); let alternatives = await findAlternativeNodeModules( options.inputFS, ext, path.dirname(configPath), ); throw new ThrowableDiagnostic({ diagnostic: { message: `Cannot find extended parcel config`, origin: '@parcel/core', codeFrames: [ { filePath: configPath, language: 'json5', code: parentContents, codeHighlights: generateJSONCodeHighlights(parentContents, [ { key: extendsKey, type: 'value', message: md`Cannot find module "${ext}"${ alternatives[0] ? `, did you mean "${alternatives[0]}"?` : '' }`, }, ]), }, ], }, }); } } } async function processExtendedConfig( configPath: FilePath, extendsKey: string, extendsSpecifier: string, resolvedExtendedConfigPath: FilePath, options: ParcelOptions, ): Promise<ParcelConfigChain> { let contents; try { contents = await options.inputFS.readFile( resolvedExtendedConfigPath, 'utf8', ); } catch (e) { let parentContents = await options.inputFS.readFile(configPath, 'utf8'); let alternatives = await findAlternativeFiles( options.inputFS, extendsSpecifier, path.dirname(resolvedExtendedConfigPath), options.projectRoot, ); throw new ThrowableDiagnostic({ diagnostic: { message: 'Cannot find extended parcel config', origin: '@parcel/core', codeFrames: [ { filePath: configPath, language: 'json5', code: parentContents, codeHighlights: generateJSONCodeHighlights(parentContents, [ { key: extendsKey, type: 'value', message: md`"${extendsSpecifier}" does not exist${ alternatives[0] ? `, did you mean "${alternatives[0]}"?` : '' }`, }, ]), }, ], }, }); } return parseAndProcessConfig(resolvedExtendedConfigPath, contents, options); } export function validateConfigFile( config: RawParcelConfig | ResolvedParcelConfigFile, relativePath: FilePath, ) { try { validateNotEmpty(config, relativePath); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: { message: e.message, origin: '@parcel/core', }, }); } validateSchema.diagnostic( ParcelConfigSchema, {data: config, filePath: relativePath}, '@parcel/core', 'Invalid Parcel Config', ); } export function validateNotEmpty( config: RawParcelConfig | ResolvedParcelConfigFile, relativePath: FilePath, ) { invariant.notDeepStrictEqual(config, {}, `${relativePath} can't be empty`); } export function mergeConfigs( base: ProcessedParcelConfig, ext: ProcessedParcelConfig, ): ProcessedParcelConfig { return { filePath: ext.filePath, resolvers: assertPurePipeline( mergePipelines(base.resolvers, ext.resolvers), ), transformers: mergeMaps( base.transformers, ext.transformers, mergePipelines, ), validators: mergeMaps(base.validators, ext.validators, mergePipelines), bundler: ext.bundler || base.bundler, namers: assertPurePipeline(mergePipelines(base.namers, ext.namers)), runtimes: assertPurePipeline(mergePipelines(base.runtimes, ext.runtimes)), packagers: mergeMaps(base.packagers, ext.packagers), optimizers: mergeMaps(base.optimizers, ext.optimizers, mergePipelines), compressors: mergeMaps(base.compressors, ext.compressors, mergePipelines), reporters: assertPurePipeline( mergePipelines(base.reporters, ext.reporters), ), }; } export function getResolveFrom( fs: FileSystem, projectRoot: FilePath, ): FilePath { let cwd = fs.cwd(); let dir = isDirectoryInside(cwd, projectRoot) ? cwd : projectRoot; return path.join(dir, 'index'); } function assertPurePipeline( pipeline: ExtendableParcelConfigPipeline, ): PureParcelConfigPipeline { return pipeline.map(s => { invariant(typeof s !== 'string'); return s; }); } export function mergePipelines( base: ?ExtendableParcelConfigPipeline, ext: ?ExtendableParcelConfigPipeline, ): ExtendableParcelConfigPipeline { if (ext == null) { return base ?? []; } if (ext.filter(v => v === '...').length > 1) { throw new Error( 'Only one spread element can be included in a config pipeline', ); } // Merge the base pipeline if a rest element is defined let spreadIndex = ext.indexOf('...'); if (spreadIndex >= 0) { return [ ...ext.slice(0, spreadIndex), ...(base ?? []), ...ext.slice(spreadIndex + 1), ]; } else { return ext; } } export function mergeMaps<K: string, V>( base: ?ConfigMap<K, V>, ext: ?ConfigMap<K, V>, merger?: (a: V, b: V) => V, ): ConfigMap<K, V> { if (!ext || Object.keys(ext).length === 0) { return base || {}; } if (!base) { return ext; } let res: ConfigMap<K, V> = {}; // Add the extension options first so they have higher precedence in the output glob map for (let k in ext) { //$FlowFixMe Flow doesn't correctly infer the type. See https://github.com/facebook/flow/issues/1736. let key: K = (k: any); res[key] = merger && base[key] != null ? merger(base[key], ext[key]) : ext[key]; } // Add base options that aren't defined in the extension for (let k in base) { // $FlowFixMe let key: K = (k: any); if (res[key] == null) { res[key] = base[key]; } } return res; }