UNPKG

@parcel/core

Version:
455 lines (406 loc) • 13.3 kB
// @flow strict-local import type {Diagnostic} from '@parcel/diagnostic'; import type { Async, FileCreateInvalidation, FilePath, Resolver, } from '@parcel/types'; import type {StaticRunOpts} from '../RequestTracker'; import type { AssetGroup, Config, Dependency, DevDepRequest, DevDepRequestRef, ParcelOptions, } from '../types'; import type {ConfigAndCachePath} from './ParcelConfigRequest'; import ThrowableDiagnostic, { convertSourceLocationToHighlight, errorToDiagnostic, md, } from '@parcel/diagnostic'; import {PluginLogger} from '@parcel/logger'; import nullthrows from 'nullthrows'; import path from 'path'; import {normalizePath} from '@parcel/utils'; import {report} from '../ReporterRunner'; import {getPublicDependency} from '../public/Dependency'; import PluginOptions from '../public/PluginOptions'; import ParcelConfig from '../ParcelConfig'; import createParcelConfigRequest, { getCachedParcelConfig, } from './ParcelConfigRequest'; import {invalidateOnFileCreateToInternal} from '../utils'; import { fromProjectPath, fromProjectPathRelative, toProjectPath, toProjectPathUnsafe, } from '../projectPath'; import {Priority} from '../types'; import {createBuildCache} from '../buildCache'; import type {LoadedPlugin} from '../ParcelConfig'; import {createConfig} from '../InternalConfig'; import {loadPluginConfig, runConfigRequest} from './ConfigRequest'; import { createDevDependency, getDevDepRequests, invalidateDevDeps, runDevDepRequest, } from './DevDepRequest'; import {tracer, PluginTracer} from '@parcel/profiler'; import {requestTypes} from '../RequestTracker'; export type PathRequest = {| id: string, +type: typeof requestTypes.path_request, run: (RunOpts<PathRequestResult>) => Async<PathRequestResult>, input: PathRequestInput, |}; export type PathRequestResult = null | void | AssetGroup; export type PathRequestInput = {| dependency: Dependency, name: string, |}; type RunOpts<TResult> = {| input: PathRequestInput, ...StaticRunOpts<TResult>, |}; const PIPELINE_REGEX = /^([a-z0-9-]+?):(.*)$/i; export default function createPathRequest( input: PathRequestInput, ): PathRequest { return { id: input.dependency.id + ':' + input.name, type: requestTypes.path_request, run, input, }; } async function run({input, api, options}): Promise<PathRequestResult> { let configResult = nullthrows( await api.runRequest<null, ConfigAndCachePath>(createParcelConfigRequest()), ); let config = getCachedParcelConfig(configResult, options); let {devDeps, invalidDevDeps} = await getDevDepRequests(api); invalidateDevDeps(invalidDevDeps, options, config); let resolverRunner = new ResolverRunner({ options, config, previousDevDeps: devDeps, }); let result: ResolverResult = await resolverRunner.resolve(input.dependency); if (result.invalidateOnEnvChange) { for (let env of result.invalidateOnEnvChange) { api.invalidateOnEnvChange(env); } } if (result.invalidateOnFileCreate) { for (let file of result.invalidateOnFileCreate) { api.invalidateOnFileCreate( invalidateOnFileCreateToInternal(options.projectRoot, file), ); } } if (result.invalidateOnFileChange) { for (let filePath of result.invalidateOnFileChange) { let pp = toProjectPath(options.projectRoot, filePath); api.invalidateOnFileUpdate(pp); api.invalidateOnFileDelete(pp); } } for (let config of resolverRunner.configs.values()) { await runConfigRequest(api, config); } for (let devDepRequest of resolverRunner.devDepRequests.values()) { await runDevDepRequest(api, devDepRequest); } if (result.assetGroup) { api.invalidateOnFileDelete(result.assetGroup.filePath); return result.assetGroup; } if (result.diagnostics && result.diagnostics.length > 0) { let err = new ThrowableDiagnostic({diagnostic: result.diagnostics}); // $FlowFixMe[prop-missing] err.code = 'MODULE_NOT_FOUND'; throw err; } } type ResolverRunnerOpts = {| config: ParcelConfig, options: ParcelOptions, previousDevDeps: Map<string, string>, |}; type ResolverResult = {| assetGroup: ?AssetGroup, invalidateOnFileCreate?: Array<FileCreateInvalidation>, invalidateOnFileChange?: Array<FilePath>, invalidateOnEnvChange?: Array<string>, diagnostics?: Array<Diagnostic>, |}; const configCache = createBuildCache(); export class ResolverRunner { config: ParcelConfig; options: ParcelOptions; pluginOptions: PluginOptions; previousDevDeps: Map<string, string>; devDepRequests: Map<string, DevDepRequest | DevDepRequestRef>; configs: Map<string, Config>; constructor({config, options, previousDevDeps}: ResolverRunnerOpts) { this.config = config; this.options = options; this.pluginOptions = new PluginOptions(this.options); this.previousDevDeps = previousDevDeps; this.devDepRequests = new Map(); this.configs = new Map(); } async getDiagnostic( dependency: Dependency, message: string, ): Async<Diagnostic> { let diagnostic: Diagnostic = { message, origin: '@parcel/core', }; if (dependency.loc && dependency.sourcePath != null) { let filePath = fromProjectPath( this.options.projectRoot, dependency.sourcePath, ); diagnostic.codeFrames = [ { filePath, code: await this.options.inputFS .readFile(filePath, 'utf8') .catch(() => ''), codeHighlights: dependency.loc ? [convertSourceLocationToHighlight(dependency.loc)] : [], }, ]; } return diagnostic; } async loadConfigs( resolvers: Array<LoadedPlugin<Resolver<mixed>>>, ): Promise<void> { for (let plugin of resolvers) { // Only load config for a plugin once per build. let config = configCache.get(plugin.name); if (!config && plugin.plugin.loadConfig != null) { config = createConfig({ plugin: plugin.name, searchPath: toProjectPathUnsafe('index'), }); await loadPluginConfig(plugin, config, this.options); configCache.set(plugin.name, config); this.configs.set(plugin.name, config); } if (config) { for (let devDep of config.devDeps) { let devDepRequest = await createDevDependency( devDep, this.previousDevDeps, this.options, ); this.runDevDepRequest(devDepRequest); } this.configs.set(plugin.name, config); } } } runDevDepRequest(devDepRequest: DevDepRequest | DevDepRequestRef) { let {specifier, resolveFrom} = devDepRequest; let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; this.devDepRequests.set(key, devDepRequest); } async resolve(dependency: Dependency): Promise<ResolverResult> { let dep = getPublicDependency(dependency, this.options); report({ type: 'buildProgress', phase: 'resolving', dependency: dep, }); let resolvers = await this.config.getResolvers(); await this.loadConfigs(resolvers); let pipeline; let specifier; let validPipelines = new Set(this.config.getNamedPipelines()); let match = dependency.specifier.match(PIPELINE_REGEX); if ( match && // Don't consider absolute paths. Absolute paths are only supported for entries, // and include e.g. `C:\` on Windows, conflicting with pipelines. !path.isAbsolute(dependency.specifier) ) { [, pipeline, specifier] = match; if (!validPipelines.has(pipeline)) { // This may be a url protocol or scheme rather than a pipeline, such as // `url('http://example.com/foo.png')`. Pass it to resolvers to handle. specifier = dependency.specifier; pipeline = null; } } else { specifier = dependency.specifier; } // Entrypoints, convert ProjectPath in module specifier to absolute path if (dep.resolveFrom == null) { specifier = path.join(this.options.projectRoot, specifier); } let diagnostics: Array<Diagnostic> = []; let invalidateOnFileCreate = []; let invalidateOnFileChange = []; let invalidateOnEnvChange = []; for (let resolver of resolvers) { let measurement; try { measurement = tracer.createMeasurement( resolver.name, 'resolve', specifier, ); let result = await resolver.plugin.resolve({ specifier, pipeline, dependency: dep, options: this.pluginOptions, logger: new PluginLogger({origin: resolver.name}), tracer: new PluginTracer({ origin: resolver.name, category: 'resolver', }), config: this.configs.get(resolver.name)?.result, }); measurement && measurement.end(); if (result) { if (result.meta) { dependency.resolverMeta = result.meta; dependency.meta = { ...dependency.meta, ...result.meta, }; } if (result.priority != null) { dependency.priority = dependency.resolverPriority = Priority[result.priority]; } if (result.invalidateOnEnvChange) { invalidateOnEnvChange.push(...result.invalidateOnEnvChange); } if (result.invalidateOnFileCreate) { invalidateOnFileCreate.push(...result.invalidateOnFileCreate); } if (result.invalidateOnFileChange) { invalidateOnFileChange.push(...result.invalidateOnFileChange); } if (result.isExcluded) { return { assetGroup: null, invalidateOnFileCreate, invalidateOnFileChange, invalidateOnEnvChange, }; } if (result.filePath != null) { let resultFilePath = result.filePath; if (!path.isAbsolute(resultFilePath)) { throw new Error( md`Resolvers must return an absolute path, ${resolver.name} returned: ${resultFilePath}`, ); } return { assetGroup: { canDefer: result.canDefer, filePath: toProjectPath( this.options.projectRoot, resultFilePath, ), query: result.query?.toString(), sideEffects: result.sideEffects, code: result.code, env: dependency.env, pipeline: result.pipeline === undefined ? pipeline ?? dependency.pipeline : result.pipeline, isURL: dep.specifierType === 'url', }, invalidateOnFileCreate, invalidateOnFileChange, invalidateOnEnvChange, }; } if ( result.diagnostics != null && !( Array.isArray(result.diagnostics) && result.diagnostics.length === 0 ) ) { let errorDiagnostic = errorToDiagnostic( new ThrowableDiagnostic({diagnostic: result.diagnostics}), { origin: resolver.name, filePath: specifier, }, ); diagnostics.push(...errorDiagnostic); } } } catch (e) { // Add error to error map, we'll append these to the standard error if we can't resolve the asset let errorDiagnostic = errorToDiagnostic(e, { origin: resolver.name, filePath: specifier, }); if (Array.isArray(errorDiagnostic)) { diagnostics.push(...errorDiagnostic); } else { diagnostics.push(errorDiagnostic); } break; } finally { measurement && measurement.end(); // Add dev dependency for the resolver. This must be done AFTER running it due to // the potential for lazy require() that aren't executed until the request runs. let devDepRequest = await createDevDependency( { specifier: resolver.name, resolveFrom: resolver.resolveFrom, }, this.previousDevDeps, this.options, ); this.runDevDepRequest(devDepRequest); } } if (dep.isOptional) { return { assetGroup: null, invalidateOnFileCreate, invalidateOnFileChange, invalidateOnEnvChange, }; } let resolveFrom = dependency.resolveFrom ?? dependency.sourcePath; let dir = resolveFrom != null ? normalizePath(fromProjectPathRelative(resolveFrom)) : ''; let diagnostic = await this.getDiagnostic( dependency, md`Failed to resolve '${dependency.specifier}' ${ dir ? `from '${dir}'` : '' }`, ); diagnostics.unshift(diagnostic); return { assetGroup: null, invalidateOnFileCreate, invalidateOnFileChange, invalidateOnEnvChange, diagnostics, }; } }