UNPKG

@parcel/core

Version:
1,554 lines (1,449 loc) • 54.2 kB
// @flow strict-local import type {Diagnostic} from '@parcel/diagnostic'; import type {FileSystem} from '@parcel/fs'; import type { Async, Engines, FilePath, PackageJSON, PackageTargetDescriptor, TargetDescriptor, OutputFormat, } from '@parcel/types'; import type {StaticRunOpts, RunAPI} from '../RequestTracker'; import type {Entry, ParcelOptions, Target} from '../types'; import type {ConfigAndCachePath} from './ParcelConfigRequest'; import ThrowableDiagnostic, { convertSourceLocationToHighlight, generateJSONCodeHighlights, getJSONSourceLocation, encodeJSONKeyComponent, md, } from '@parcel/diagnostic'; import path from 'path'; import { loadConfig, resolveConfig, hashObject, validateSchema, } from '@parcel/utils'; import logger from '@parcel/logger'; import {createEnvironment} from '../Environment'; import createParcelConfigRequest, { getCachedParcelConfig, } from './ParcelConfigRequest'; // $FlowFixMe import browserslist from 'browserslist'; import {parse} from '@mischnic/json-sourcemap'; import invariant from 'assert'; import nullthrows from 'nullthrows'; import { COMMON_TARGET_DESCRIPTOR_SCHEMA, DESCRIPTOR_SCHEMA, PACKAGE_DESCRIPTOR_SCHEMA, ENGINES_SCHEMA, } from '../TargetDescriptor.schema'; import {optionsProxy, toInternalSourceLocation} from '../utils'; import {fromProjectPath, toProjectPath, joinProjectPath} from '../projectPath'; import {requestTypes} from '../RequestTracker'; import {BROWSER_ENVS} from '../public/Environment'; type RunOpts<TResult> = {| input: Entry, ...StaticRunOpts<TResult>, |}; const DEFAULT_DIST_DIRNAME = 'dist'; const JS_RE = /\.[mc]?js$/; const JS_EXTENSIONS = ['.js', '.mjs', '.cjs']; const COMMON_TARGETS = { main: { match: JS_RE, extensions: JS_EXTENSIONS, }, module: { // module field is always ESM. Don't allow .cjs extension here. match: /\.m?js$/, extensions: ['.js', '.mjs'], }, browser: { match: JS_RE, extensions: JS_EXTENSIONS, }, types: { match: /\.d\.ts$/, extensions: ['.d.ts'], }, }; const DEFAULT_ENGINES = { node: process.versions.node, browsers: [ 'last 1 Chrome version', 'last 1 Safari version', 'last 1 Firefox version', 'last 1 Edge version', ], }; export type TargetRequest = {| id: string, +type: typeof requestTypes.target_request, run: (RunOpts<TargetRequestResult>) => Async<TargetRequestResult>, input: Entry, |}; export type TargetRequestResult = Target[]; const type = 'target_request'; export default function createTargetRequest(input: Entry): TargetRequest { return { id: `${type}:${hashObject(input)}`, type: requestTypes.target_request, run, input, }; } export function skipTarget( targetName: string, exclusiveTarget?: FilePath, descriptorSource?: FilePath | Array<FilePath>, ): boolean { // We skip targets if they have a descriptor.source and don't match the current exclusiveTarget // They will be handled by a separate resolvePackageTargets call from their Entry point // but with exclusiveTarget set. return exclusiveTarget == null ? descriptorSource != null : targetName !== exclusiveTarget; } async function run({input, api, options}) { let targetResolver = new TargetResolver( api, optionsProxy(options, api.invalidateOnOptionChange), ); let targets: TargetRequestResult = await targetResolver.resolve( fromProjectPath(options.projectRoot, input.packagePath), input.target, ); assertTargetsAreNotEntries(targets, input, options); let configResult = nullthrows( await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()), ); let parcelConfig = getCachedParcelConfig(configResult, options); // Find named pipelines for each target. let pipelineNames = new Set(parcelConfig.getNamedPipelines()); for (let target of targets) { if (pipelineNames.has(target.name)) { target.pipeline = target.name; } } if (options.logLevel === 'verbose') { await debugResolvedTargets( input, targets, targetResolver.targetInfo, options, ); } return targets; } type TargetInfo = {| output: TargetKeyInfo, engines: TargetKeyInfo, context: TargetKeyInfo, includeNodeModules: TargetKeyInfo, outputFormat: TargetKeyInfo, isLibrary: TargetKeyInfo, shouldOptimize: TargetKeyInfo, shouldScopeHoist: TargetKeyInfo, |}; type TargetKeyInfo = | {| path: string, type?: 'key' | 'value', |} | {| inferred: string, type?: 'key' | 'value', message: string, |} | {| message: string, |}; export class TargetResolver { fs: FileSystem; api: RunAPI<Array<Target>>; options: ParcelOptions; targetInfo: Map<string, TargetInfo>; constructor(api: RunAPI<Array<Target>>, options: ParcelOptions) { this.api = api; this.fs = options.inputFS; this.options = options; this.targetInfo = new Map(); } async resolve( rootDir: FilePath, exclusiveTarget?: string, ): Promise<Array<Target>> { let optionTargets = this.options.targets; if (exclusiveTarget != null && optionTargets == null) { optionTargets = [exclusiveTarget]; } let packageTargets: Map<string, Target | null> = await this.resolvePackageTargets(rootDir, exclusiveTarget); let targets: Array<Target>; if (optionTargets) { if (Array.isArray(optionTargets)) { if (optionTargets.length === 0) { throw new ThrowableDiagnostic({ diagnostic: { message: `Targets option is an empty array`, origin: '@parcel/core', }, }); } // Only build the intersection of the exclusive target and option targets. if (exclusiveTarget != null) { optionTargets = optionTargets.filter( target => target === exclusiveTarget, ); } // If an array of strings is passed, it's a filter on the resolved package // targets. Load them, and find the matching targets. targets = optionTargets .map(target => { // null means skipped. if (!packageTargets.has(target)) { throw new ThrowableDiagnostic({ diagnostic: { message: md`Could not find target with name "${target}"`, origin: '@parcel/core', }, }); } return packageTargets.get(target); }) .filter(Boolean); } else { // Otherwise, it's an object map of target descriptors (similar to those // in package.json). Adapt them to native targets. targets = Object.entries(optionTargets) .map(([name, _descriptor]) => { let {distDir, ...descriptor} = parseDescriptor( name, _descriptor, null, JSON.stringify({targets: optionTargets}, null, '\t'), ); if (distDir == null) { let optionTargetsString = JSON.stringify( optionTargets, null, '\t', ); throw new ThrowableDiagnostic({ diagnostic: { message: md`Missing distDir for target "${name}"`, origin: '@parcel/core', codeFrames: [ { code: optionTargetsString, codeHighlights: generateJSONCodeHighlights( optionTargetsString || '', [ { key: `/${name}`, type: 'value', }, ], ), }, ], }, }); } let target: Target = { name, distDir: toProjectPath( this.options.projectRoot, path.resolve(this.fs.cwd(), distDir), ), publicUrl: descriptor.publicUrl ?? this.options.defaultTargetOptions.publicUrl, env: createEnvironment({ engines: descriptor.engines, context: descriptor.context, isLibrary: descriptor.isLibrary ?? this.options.defaultTargetOptions.isLibrary, includeNodeModules: descriptor.includeNodeModules, outputFormat: descriptor.outputFormat ?? this.options.defaultTargetOptions.outputFormat, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && descriptor.optimize !== false, shouldScopeHoist: this.options.defaultTargetOptions.shouldScopeHoist && descriptor.scopeHoist !== false, sourceMap: normalizeSourceMap( this.options, descriptor.sourceMap, ), }), }; if (descriptor.distEntry != null) { target.distEntry = descriptor.distEntry; } if (descriptor.source != null) { target.source = descriptor.source; } return target; }) .filter( target => !skipTarget(target.name, exclusiveTarget, target.source), ); } let serve = this.options.serveOptions; if ( serve && targets.length > 0 && targets.every(t => BROWSER_ENVS.has(t.env.context)) ) { // In serve mode, we only support a single browser target. If the user // provided more than one, or the matching target is not a browser, throw. if (targets.length > 1) { throw new ThrowableDiagnostic({ diagnostic: { message: `More than one target is not supported in serve mode`, origin: '@parcel/core', }, }); } targets[0].distDir = toProjectPath( this.options.projectRoot, serve.distDir, ); } } else { targets = Array.from(packageTargets.values()) .filter(Boolean) .filter(descriptor => { return ( descriptor && !skipTarget(descriptor.name, exclusiveTarget, descriptor.source) ); }); // Explicit targets were not provided. Either use a modern target for server // mode, or simply use the package.json targets. if ( this.options.serveOptions && targets.every(t => BROWSER_ENVS.has(t.env.context)) ) { // In serve mode, we only support a single browser target. Since the user // hasn't specified a target, use one targeting modern browsers for development let distDir = toProjectPath( this.options.projectRoot, this.options.serveOptions.distDir, ); let mainTarget = targets.length === 1 ? targets[0] : null; if (mainTarget?.env.isLibrary) { let loc = mainTarget.loc; throw new ThrowableDiagnostic({ diagnostic: { origin: '@parcel/core', message: md` Library targets are not supported in serve mode. `, codeFrames: loc ? [ { filePath: fromProjectPath( this.options.projectRoot, loc.filePath, ), codeHighlights: [ convertSourceLocationToHighlight( loc, 'Target declared here', ), ], }, ] : [], hints: [ `The "${mainTarget.name}" field is meant for libraries, not applications. Either remove the "${mainTarget.name}" field or choose a different target name.`, ], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } let context = mainTarget?.env.context ?? 'browser'; let engines = BROWSER_ENVS.has(context) ? {browsers: DEFAULT_ENGINES.browsers} : {node: DEFAULT_ENGINES.node}; targets = [ { name: 'default', distDir, publicUrl: this.options.defaultTargetOptions.publicUrl ?? '/', env: createEnvironment({ context, engines, includeNodeModules: mainTarget?.env.includeNodeModules, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize, outputFormat: mainTarget?.env.outputFormat ?? this.options.defaultTargetOptions.outputFormat, shouldScopeHoist: this.options.defaultTargetOptions.shouldScopeHoist, sourceMap: this.options.defaultTargetOptions.sourceMaps ? {} : undefined, }), }, ]; } } return targets; } async resolvePackageTargets( rootDir: FilePath, exclusiveTarget?: string, ): Promise<Map<string, Target | null>> { let rootFile = path.join(rootDir, 'index'); let conf = await loadConfig( this.fs, rootFile, ['package.json'], this.options.projectRoot, ); let rootFileProject = toProjectPath(this.options.projectRoot, rootFile); // Invalidate whenever a package.json file is added. this.api.invalidateOnFileCreate({ fileName: 'package.json', aboveFilePath: rootFileProject, }); let pkg; let pkgContents; let pkgFilePath: ?FilePath; let pkgDir: FilePath; let pkgMap; if (conf) { pkg = (conf.config: PackageJSON); let pkgFile = conf.files[0]; if (pkgFile == null) { throw new ThrowableDiagnostic({ diagnostic: { message: md`Expected package.json file in ${rootDir}`, origin: '@parcel/core', }, }); } let _pkgFilePath = (pkgFilePath = pkgFile.filePath); // For Flow pkgDir = path.dirname(_pkgFilePath); pkgContents = await this.fs.readFile(_pkgFilePath, 'utf8'); pkgMap = parse(pkgContents, undefined, {tabWidth: 1}); let pp = toProjectPath(this.options.projectRoot, _pkgFilePath); this.api.invalidateOnFileUpdate(pp); this.api.invalidateOnFileDelete(pp); } else { pkg = {}; pkgDir = this.fs.cwd(); } let pkgTargets = pkg.targets || {}; let pkgEngines: Engines = parseEngines( pkg.engines, pkgFilePath, pkgContents, '/engines', 'Invalid engines in package.json', ) || {}; let browsersLoc = {path: '/engines/browsers'}; let nodeLoc = {path: '/engines/node'}; if (pkgEngines.browsers == null) { let env = this.options.env.BROWSERSLIST_ENV ?? this.options.env.NODE_ENV ?? this.options.mode; if (pkg.browserslist != null) { let pkgBrowserslist = pkg.browserslist; let browserslist = typeof pkgBrowserslist === 'object' && !Array.isArray(pkgBrowserslist) ? pkgBrowserslist[env] : pkgBrowserslist; pkgEngines = { ...pkgEngines, browsers: browserslist, }; browsersLoc = {path: '/browserslist'}; } else { let browserslistConfig = await resolveConfig( this.fs, path.join(rootDir, 'index'), ['browserslist', '.browserslistrc'], this.options.projectRoot, ); this.api.invalidateOnFileCreate({ fileName: 'browserslist', aboveFilePath: rootFileProject, }); this.api.invalidateOnFileCreate({ fileName: '.browserslistrc', aboveFilePath: rootFileProject, }); if (browserslistConfig != null) { let contents = await this.fs.readFile(browserslistConfig, 'utf8'); let config = browserslist.parseConfig(contents); let browserslistBrowsers = config[env] || config.defaults; let pp = toProjectPath(this.options.projectRoot, browserslistConfig); if (browserslistBrowsers?.length > 0) { pkgEngines = { ...pkgEngines, browsers: browserslistBrowsers, }; browsersLoc = { message: `(defined in ${path.relative( process.cwd(), browserslistConfig, )})`, }; } // Invalidate whenever browserslist config file or relevant environment variables change this.api.invalidateOnFileUpdate(pp); this.api.invalidateOnFileDelete(pp); this.api.invalidateOnEnvChange('BROWSERSLIST_ENV'); this.api.invalidateOnEnvChange('NODE_ENV'); } } } let targets: Map<string, Target | null> = new Map(); let node = pkgEngines.node; let browsers = pkgEngines.browsers; let defaultEngines = this.options.defaultTargetOptions.engines; let context = browsers ?? node == null ? 'browser' : 'node'; if (context === 'browser' && pkgEngines.browsers == null) { pkgEngines = { ...pkgEngines, browsers: defaultEngines?.browsers ?? DEFAULT_ENGINES.browsers, }; browsersLoc = {message: '(default)'}; } else if (context === 'node' && pkgEngines.node == null) { pkgEngines = { ...pkgEngines, node: defaultEngines?.node ?? DEFAULT_ENGINES.node, }; nodeLoc = {message: '(default)'}; } // If there is a separate `browser` target, or an `engines.node` field but no browser targets, then // the `main` and `module` targets refer to node, otherwise browser. let mainContext = pkg.browser ?? pkgTargets.browser ?? (node != null && browsers == null) ? 'node' : 'browser'; let mainContextLoc: TargetKeyInfo = pkg.browser != null ? { inferred: '/browser', message: '(because a browser field also exists)', type: 'key', } : pkgTargets.browser ? { inferred: '/targets/browser', message: '(because a browser target also exists)', type: 'key', } : node != null && browsers == null ? nodeLoc.path ? { inferred: nodeLoc.path, message: '(because node engines were defined)', type: 'key', } : nodeLoc : {message: '(default)'}; let moduleContext = pkg.browser ?? pkgTargets.browser ? 'browser' : mainContext; let moduleContextLoc: TargetKeyInfo = pkg.browser != null ? { inferred: '/browser', message: '(because a browser field also exists)', type: 'key', } : pkgTargets.browser ? { inferred: '/targets/browser', message: '(becausea browser target also exists)', type: 'key', } : mainContextLoc; let getEnginesLoc = (targetName, descriptor): TargetKeyInfo => { let enginesLoc = `/targets/${targetName}/engines`; switch (context) { case 'browser': case 'web-worker': case 'service-worker': case 'worklet': { if (descriptor.engines) { return {path: enginesLoc + '/browsers'}; } else { return browsersLoc; } } case 'node': { if (descriptor.engines) { return {path: enginesLoc + '/node'}; } else { return nodeLoc; } } case 'electron-main': case 'electron-renderer': { if (descriptor.engines?.electron != null) { return {path: enginesLoc + '/electron'}; } else if (pkgEngines?.electron != null) { return {path: '/engines/electron'}; } } } return {message: '(default)'}; }; for (let targetName in COMMON_TARGETS) { let _targetDist; let pointer; if ( targetName === 'browser' && pkg[targetName] != null && typeof pkg[targetName] === 'object' && pkg.name ) { // The `browser` field can be a file path or an alias map. _targetDist = pkg[targetName][pkg.name]; pointer = `/${targetName}/${encodeJSONKeyComponent(pkg.name)}`; } else { _targetDist = pkg[targetName]; pointer = `/${targetName}`; } // For Flow let targetDist = _targetDist; if (typeof targetDist === 'string' || pkgTargets[targetName]) { let distDir; let distEntry; let loc; invariant(pkgMap != null); let _descriptor: mixed = pkgTargets[targetName] ?? {}; if (typeof targetDist === 'string') { distDir = toProjectPath( this.options.projectRoot, path.resolve(pkgDir, path.dirname(targetDist)), ); distEntry = path.basename(targetDist); loc = { filePath: nullthrows(pkgFilePath), ...getJSONSourceLocation(pkgMap.pointers[pointer], 'value'), }; } else { distDir = this.options.defaultTargetOptions.distDir ?? toProjectPath( this.options.projectRoot, path.join(pkgDir, DEFAULT_DIST_DIRNAME, targetName), ); } if (_descriptor == false) { continue; } let descriptor = parseCommonTargetDescriptor( targetName, _descriptor, pkgFilePath, pkgContents, ); if (skipTarget(targetName, exclusiveTarget, descriptor.source)) { targets.set(targetName, null); continue; } if ( distEntry != null && !COMMON_TARGETS[targetName].match.test(distEntry) ) { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); // $FlowFixMe let listFormat = new Intl.ListFormat('en-US', {type: 'disjunction'}); let extensions = listFormat.format( COMMON_TARGETS[targetName].extensions, ); let ext = path.extname(distEntry); throw new ThrowableDiagnostic({ diagnostic: { message: md`Unexpected output file type ${ext} in target "${targetName}"`, origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: pointer, type: 'value', message: `File extension must be ${extensions}`, }, ]), }, ], hints: [ `The "${targetName}" field is meant for libraries. If you meant to output a ${ext} file, either remove the "${targetName}" field or choose a different target name.`, ], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } if (descriptor.outputFormat === 'global') { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); throw new ThrowableDiagnostic({ diagnostic: { message: md`The "global" output format is not supported in the "${targetName}" target.`, origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: `/targets/${targetName}/outputFormat`, type: 'value', }, ]), }, ], hints: [ `The "${targetName}" field is meant for libraries. The outputFormat must be either "commonjs" or "esmodule". Either change or remove the declared outputFormat.`, ], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } let [inferredOutputFormat, inferredOutputFormatField] = this.inferOutputFormat( distEntry, descriptor, targetName, pkg, pkgFilePath, pkgContents, ); let outputFormat = descriptor.outputFormat ?? this.options.defaultTargetOptions.outputFormat ?? inferredOutputFormat ?? (targetName === 'module' ? 'esmodule' : 'commonjs'); let isModule = outputFormat === 'esmodule'; if ( targetName === 'main' && outputFormat === 'esmodule' && inferredOutputFormat !== 'esmodule' ) { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); throw new ThrowableDiagnostic({ diagnostic: { // prettier-ignore message: md`Output format "esmodule" cannot be used in the "main" target without a .mjs extension or "type": "module" field.`, origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: `/targets/${targetName}/outputFormat`, type: 'value', message: 'Declared output format defined here', }, { key: '/main', type: 'value', message: 'Inferred output format defined here', }, ]), }, ], hints: [ `Either change the output file extension to .mjs, add "type": "module" to package.json, or remove the declared outputFormat.`, ], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } if (descriptor.scopeHoist === false) { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); throw new ThrowableDiagnostic({ diagnostic: { message: 'Scope hoisting cannot be disabled for library targets.', origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: `/targets/${targetName}/scopeHoist`, type: 'value', }, ]), }, ], hints: [ `The "${targetName}" target is meant for libraries. Either remove the "scopeHoist" option, or use a different target name.`, ], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } let context = descriptor.context ?? (targetName === 'browser' ? 'browser' : isModule ? moduleContext : mainContext); let engines = descriptor.engines ?? pkgEngines; if (context === 'browser' && engines.browsers == null) { engines = { ...engines, browsers: defaultEngines?.browsers ?? DEFAULT_ENGINES.browsers, }; } else if (context === 'node' && engines.node == null) { engines = { ...engines, node: defaultEngines?.node ?? DEFAULT_ENGINES.node, }; } targets.set(targetName, { name: targetName, distDir, distEntry, publicUrl: descriptor.publicUrl ?? this.options.defaultTargetOptions.publicUrl, env: createEnvironment({ engines, context, includeNodeModules: descriptor.includeNodeModules ?? false, outputFormat, isLibrary: true, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && descriptor.optimize === true, shouldScopeHoist: true, sourceMap: normalizeSourceMap(this.options, descriptor.sourceMap), }), loc: toInternalSourceLocation(this.options.projectRoot, loc), }); this.targetInfo.set(targetName, { output: {path: pointer}, engines: getEnginesLoc(targetName, descriptor), context: descriptor.context ? {path: `/targets/${targetName}/context`} : targetName === 'browser' ? { message: '(inferred from target name)', inferred: pointer, type: 'key', } : isModule ? moduleContextLoc : mainContextLoc, includeNodeModules: descriptor.includeNodeModules ? {path: `/targets/${targetName}/includeNodeModules`, type: 'key'} : {message: '(default)'}, outputFormat: descriptor.outputFormat ? {path: `/targets/${targetName}/outputFormat`} : inferredOutputFormatField === '/type' ? { message: `(inferred from package.json#type)`, inferred: inferredOutputFormatField, } : inferredOutputFormatField != null ? { message: `(inferred from file extension)`, inferred: inferredOutputFormatField, } : {message: '(default)'}, isLibrary: {message: '(default)'}, shouldOptimize: descriptor.optimize ? {path: `/targets/${targetName}/optimize`} : {message: '(default)'}, shouldScopeHoist: {message: '(default)'}, }); } } let customTargets = (Object.keys(pkgTargets): Array<string>).filter( targetName => !COMMON_TARGETS[targetName], ); // Custom targets for (let targetName of customTargets) { let distPath: mixed = pkg[targetName]; let distDir; let distEntry; let loc; let pointer; if (distPath == null) { distDir = fromProjectPath( this.options.projectRoot, this.options.defaultTargetOptions.distDir, ) ?? path.join(pkgDir, DEFAULT_DIST_DIRNAME); if (customTargets.length >= 2) { distDir = path.join(distDir, targetName); } invariant(pkgMap != null); invariant(typeof pkgFilePath === 'string'); loc = { filePath: pkgFilePath, ...getJSONSourceLocation( pkgMap.pointers[`/targets/${targetName}`], 'key', ), }; } else { if (typeof distPath !== 'string') { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); throw new ThrowableDiagnostic({ diagnostic: { message: md`Invalid distPath for target "${targetName}"`, origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: `/${targetName}`, type: 'value', message: 'Expected type string', }, ]), }, ], }, }); } distDir = path.resolve(pkgDir, path.dirname(distPath)); distEntry = path.basename(distPath); invariant(typeof pkgFilePath === 'string'); invariant(pkgMap != null); loc = { filePath: pkgFilePath, ...getJSONSourceLocation(pkgMap.pointers[`/${targetName}`], 'value'), }; pointer = `/${targetName}`; } if (targetName in pkgTargets) { let descriptor = parsePackageDescriptor( targetName, pkgTargets[targetName], pkgFilePath, pkgContents, ); let pkgDir = path.dirname(nullthrows(pkgFilePath)); if (skipTarget(targetName, exclusiveTarget, descriptor.source)) { targets.set(targetName, null); continue; } let [inferredOutputFormat, inferredOutputFormatField] = this.inferOutputFormat( distEntry, descriptor, targetName, pkg, pkgFilePath, pkgContents, ); if (descriptor.scopeHoist === false && descriptor.isLibrary) { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); throw new ThrowableDiagnostic({ diagnostic: { message: 'Scope hoisting cannot be disabled for library targets.', origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: `/targets/${targetName}/scopeHoist`, type: 'value', }, { key: `/targets/${targetName}/isLibrary`, type: 'value', }, ]), }, ], hints: [`Either remove the "scopeHoist" or "isLibrary" option.`], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } let isLibrary = descriptor.isLibrary ?? this.options.defaultTargetOptions.isLibrary ?? false; let shouldScopeHoist = isLibrary ? true : this.options.defaultTargetOptions.shouldScopeHoist; let engines = descriptor.engines ?? pkgEngines; if (descriptor.context === 'browser' && engines.browsers == null) { engines = { ...engines, browsers: defaultEngines?.browsers ?? DEFAULT_ENGINES.browsers, }; } else if (descriptor.context === 'node' && engines.node == null) { engines = { ...engines, node: defaultEngines?.node ?? DEFAULT_ENGINES.node, }; } targets.set(targetName, { name: targetName, distDir: toProjectPath( this.options.projectRoot, descriptor.distDir != null ? path.resolve(pkgDir, descriptor.distDir) : distDir, ), distEntry, publicUrl: descriptor.publicUrl ?? this.options.defaultTargetOptions.publicUrl, env: createEnvironment({ engines, context: descriptor.context, includeNodeModules: descriptor.includeNodeModules, outputFormat: descriptor.outputFormat ?? this.options.defaultTargetOptions.outputFormat ?? inferredOutputFormat ?? undefined, isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize && // Libraries are not optimized by default, users must explicitly configure this. (isLibrary ? descriptor.optimize === true : descriptor.optimize !== false), shouldScopeHoist: shouldScopeHoist && descriptor.scopeHoist !== false, sourceMap: normalizeSourceMap(this.options, descriptor.sourceMap), }), loc: toInternalSourceLocation(this.options.projectRoot, loc), }); this.targetInfo.set(targetName, { output: pointer != null ? {path: pointer} : {message: '(default)'}, engines: getEnginesLoc(targetName, descriptor), context: descriptor.context ? {path: `/targets/${targetName}/context`} : {message: '(default)'}, includeNodeModules: descriptor.includeNodeModules ? {path: `/targets/${targetName}/includeNodeModules`, type: 'key'} : {message: '(default)'}, outputFormat: descriptor.outputFormat ? {path: `/targets/${targetName}/outputFormat`} : inferredOutputFormatField === '/type' ? { message: `(inferred from package.json#type)`, inferred: inferredOutputFormatField, } : inferredOutputFormatField != null ? { message: `(inferred from file extension)`, inferred: inferredOutputFormatField, } : {message: '(default)'}, isLibrary: descriptor.isLibrary != null ? {path: `/targets/${targetName}/isLibrary`} : {message: '(default)'}, shouldOptimize: descriptor.optimize != null ? {path: `/targets/${targetName}/optimize`} : {message: '(default)'}, shouldScopeHoist: descriptor.scopeHoist != null ? {path: `/targets/${targetName}/scopeHoist`} : {message: '(default)'}, }); } } // If no explicit targets were defined, add a default. if (targets.size === 0) { targets.set('default', { name: 'default', distDir: this.options.defaultTargetOptions.distDir ?? toProjectPath( this.options.projectRoot, path.join(pkgDir, DEFAULT_DIST_DIRNAME), ), publicUrl: this.options.defaultTargetOptions.publicUrl, env: createEnvironment({ engines: pkgEngines, context, outputFormat: this.options.defaultTargetOptions.outputFormat, isLibrary: this.options.defaultTargetOptions.isLibrary, shouldOptimize: this.options.defaultTargetOptions.shouldOptimize, shouldScopeHoist: this.options.defaultTargetOptions.shouldScopeHoist ?? (this.options.mode === 'production' && !this.options.defaultTargetOptions.isLibrary), sourceMap: this.options.defaultTargetOptions.sourceMaps ? {} : undefined, }), }); } assertNoDuplicateTargets(this.options, targets, pkgFilePath, pkgContents); return targets; } inferOutputFormat( distEntry: ?FilePath, descriptor: PackageTargetDescriptor, targetName: string, pkg: PackageJSON, pkgFilePath: ?FilePath, pkgContents: ?string, ): [?OutputFormat, ?string] { // Infer the outputFormat based on package.json properties. // If the extension is .mjs it's always a module. // If the extension is .cjs, it's always commonjs. // If the "type" field is set to "module" and the extension is .js, it's a module. let ext = distEntry != null ? path.extname(distEntry) : null; let inferredOutputFormat, inferredOutputFormatField; switch (ext) { case '.mjs': inferredOutputFormat = 'esmodule'; inferredOutputFormatField = `/${targetName}`; break; case '.cjs': inferredOutputFormat = 'commonjs'; inferredOutputFormatField = `/${targetName}`; break; case '.js': if (pkg.type === 'module') { inferredOutputFormat = 'esmodule'; inferredOutputFormatField = '/type'; } break; } if ( descriptor.outputFormat && inferredOutputFormat && descriptor.outputFormat !== inferredOutputFormat ) { let contents: string = typeof pkgContents === 'string' ? pkgContents : // $FlowFixMe JSON.stringify(pkgContents, null, '\t'); let expectedExtensions; switch (descriptor.outputFormat) { case 'esmodule': expectedExtensions = ['.mjs', '.js']; break; case 'commonjs': expectedExtensions = ['.cjs', '.js']; break; case 'global': expectedExtensions = ['.js']; break; } // $FlowFixMe let listFormat = new Intl.ListFormat('en-US', {type: 'disjunction'}); throw new ThrowableDiagnostic({ diagnostic: { message: md`Declared output format "${descriptor.outputFormat}" does not match expected output format "${inferredOutputFormat}".`, origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath ?? undefined, code: contents, codeHighlights: generateJSONCodeHighlights(contents, [ { key: `/targets/${targetName}/outputFormat`, type: 'value', message: 'Declared output format defined here', }, { key: nullthrows(inferredOutputFormatField), type: 'value', message: 'Inferred output format defined here', }, ]), }, ], hints: [ inferredOutputFormatField === '/type' ? 'Either remove the target\'s declared "outputFormat" or remove the "type" field.' : `Either remove the target's declared "outputFormat" or change the extension to ${listFormat.format( expectedExtensions, )}.`, ], documentationURL: 'https://parceljs.org/features/targets/#library-targets', }, }); } return [inferredOutputFormat, inferredOutputFormatField]; } } function parseEngines( engines: mixed, pkgPath: ?FilePath, pkgContents: ?string, prependKey: string, message: string, ): Engines | typeof undefined { if (engines === undefined) { return engines; } else { validateSchema.diagnostic( ENGINES_SCHEMA, {data: engines, source: pkgContents, filePath: pkgPath, prependKey}, '@parcel/core', message, ); // $FlowFixMe we just verified this return engines; } } function parseDescriptor( targetName: string, descriptor: mixed, pkgPath: ?FilePath, pkgContents: ?string, ): TargetDescriptor { validateSchema.diagnostic( DESCRIPTOR_SCHEMA, { data: descriptor, source: pkgContents, filePath: pkgPath, prependKey: `/targets/${targetName}`, }, '@parcel/core', `Invalid target descriptor for target "${targetName}"`, ); // $FlowFixMe we just verified this return descriptor; } function parsePackageDescriptor( targetName: string, descriptor: mixed, pkgPath: ?FilePath, pkgContents: ?string, ): PackageTargetDescriptor { validateSchema.diagnostic( PACKAGE_DESCRIPTOR_SCHEMA, { data: descriptor, source: pkgContents, filePath: pkgPath, prependKey: `/targets/${targetName}`, }, '@parcel/core', `Invalid target descriptor for target "${targetName}"`, ); // $FlowFixMe we just verified this return descriptor; } function parseCommonTargetDescriptor( targetName: string, descriptor: mixed, pkgPath: ?FilePath, pkgContents: ?string, ): PackageTargetDescriptor { validateSchema.diagnostic( COMMON_TARGET_DESCRIPTOR_SCHEMA, { data: descriptor, source: pkgContents, filePath: pkgPath, prependKey: `/targets/${targetName}`, }, '@parcel/core', `Invalid target descriptor for target "${targetName}"`, ); // $FlowFixMe we just verified this return descriptor; } function assertNoDuplicateTargets(options, targets, pkgFilePath, pkgContents) { // Detect duplicate targets by destination path and provide a nice error. // Without this, an assertion is thrown much later after naming the bundles and finding duplicates. let targetsByPath: Map<string, Array<string>> = new Map(); for (let target of targets.values()) { if (!target) { continue; } let {distEntry} = target; if (distEntry != null) { let distPath = path.join( fromProjectPath(options.projectRoot, target.distDir), distEntry, ); if (!targetsByPath.has(distPath)) { targetsByPath.set(distPath, []); } targetsByPath.get(distPath)?.push(target.name); } } let diagnostics: Array<Diagnostic> = []; for (let [targetPath, targetNames] of targetsByPath) { if (targetNames.length > 1 && pkgContents != null && pkgFilePath != null) { diagnostics.push({ message: md`Multiple targets have the same destination path "${path.relative( path.dirname(pkgFilePath), targetPath, )}"`, origin: '@parcel/core', codeFrames: [ { language: 'json', filePath: pkgFilePath || undefined, code: pkgContents, codeHighlights: generateJSONCodeHighlights( pkgContents, targetNames.map(t => ({ key: `/${t}`, type: 'value', })), ), }, ], }); } } if (diagnostics.length > 0) { // Only add hints to the last diagnostic so it isn't duplicated on each one diagnostics[diagnostics.length - 1].hints = [ 'Try removing the duplicate targets, or changing the destination paths.', ]; throw new ThrowableDiagnostic({ diagnostic: diagnostics, }); } } function normalizeSourceMap(options: ParcelOptions, sourceMap) { if (options.defaultTargetOptions.sourceMaps) { if (typeof sourceMap === 'boolean') { return sourceMap ? {} : undefined; } else { return sourceMap ?? {}; } } else { return undefined; } } function assertTargetsAreNotEntries( targets: Array<Target>, input: Entry, options: ParcelOptions, ) { for (const target of targets) { if ( target.distEntry != null && joinProjectPath(target.distDir, target.distEntry) === input.filePath ) { let loc = target.loc; let relativeEntry = path.relative( process.cwd(), fromProjectPath(options.projectRoot, input.filePath), ); let codeFrames = []; if (loc) { codeFrames.push({ filePath: fromProjectPath(options.projectRoot, loc.filePath), codeHighlights: [ convertSourceLocationToHighlight(loc, 'Target defined here'), ], }); let inputLoc = input.loc; if (inputLoc) { let highlight = convertSourceLocationToHighlight( inputLoc, 'Entry defined here', ); if (inputLoc.filePath === loc.filePath) { codeFrames[0].codeHighlights.push(highlight); } else { codeFrames.push({ filePath: fromProjectPath(options.projectRoot, inputLoc.filePath), codeHighlights: [highlight], }); } } } throw new ThrowableDiagnostic({ diagnostic: { origin: '@parcel/core', message: `Target "${target.name}" is configured to overwrite entry "${relativeEntry}".`, codeFrames, hints: [ (COMMON_TARGETS[target.name] ? `The "${target.name}" field is an _output_ file path so that your build can be consumed by other tools. ` : '') + `Change the "${target.name}" field to point to an output file rather than your source code.`, ], documentationURL: 'https://parceljs.org/features/targets/', }, }); } } } async function debugResolvedTargets(input, targets, targetInfo, options) { for (let target of targets) { let info = targetInfo.get(target.name); let loc = target.loc; if (!loc || !info) { continue; } let output = fromProjectPath(options.projectRoot, target.distDir); if (target.distEntry != null) { output = path.join(output, target.distEntry); } // Resolve relevant engines for context. let engines; switch (target.