UNPKG

@parcel/core

Version:
288 lines (257 loc) • 7.99 kB
// @flow import type { Async, Config as IConfig, PluginOptions as IPluginOptions, PluginLogger as IPluginLogger, PluginTracer as IPluginTracer, NamedBundle as INamedBundle, BundleGraph as IBundleGraph, } from '@parcel/types'; import {readConfig, hashObject} from '@parcel/utils'; import type { Config, ParcelOptions, InternalFileCreateInvalidation, } from '../types'; import type {LoadedPlugin} from '../ParcelConfig'; import type {RequestResult, RunAPI} from '../RequestTracker'; import type {ProjectPath} from '../projectPath'; import {serializeRaw} from '../serializer.js'; import {PluginLogger} from '@parcel/logger'; import PluginOptions from '../public/PluginOptions'; import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic'; import PublicConfig from '../public/Config'; import {optionsProxy} from '../utils'; import {getInvalidationHash} from '../assetUtils'; import {hashString, Hash} from '@parcel/rust'; import {PluginTracer} from '@parcel/profiler'; import {requestTypes} from '../RequestTracker'; import {fromProjectPath, fromProjectPathRelative} from '../projectPath'; import {createBuildCache} from '../buildCache'; export type PluginWithLoadConfig = { loadConfig?: ({| config: IConfig, options: IPluginOptions, logger: IPluginLogger, tracer: IPluginTracer, |}) => Async<mixed>, ... }; export type PluginWithBundleConfig = { loadConfig?: ({| config: IConfig, options: IPluginOptions, logger: IPluginLogger, tracer: IPluginTracer, |}) => Async<mixed>, loadBundleConfig?: ({| bundle: INamedBundle, bundleGraph: IBundleGraph<INamedBundle>, config: IConfig, options: IPluginOptions, logger: IPluginLogger, tracer: IPluginTracer, |}) => Async<mixed>, ... }; export type ConfigRequest = { id: string, invalidateOnFileChange: Set<ProjectPath>, invalidateOnConfigKeyChange: Array<{| filePath: ProjectPath, configKey: string, |}>, invalidateOnFileCreate: Array<InternalFileCreateInvalidation>, invalidateOnEnvChange: Set<string>, invalidateOnOptionChange: Set<string>, invalidateOnStartup: boolean, invalidateOnBuild: boolean, ... }; export type ConfigRequestResult = void; export async function loadPluginConfig<T: PluginWithLoadConfig>( loadedPlugin: LoadedPlugin<T>, config: Config, options: ParcelOptions, ): Promise<void> { let loadConfig = loadedPlugin.plugin.loadConfig; if (!loadConfig) { return; } try { config.result = await loadConfig({ config: new PublicConfig(config, options), options: new PluginOptions( optionsProxy(options, option => { config.invalidateOnOptionChange.add(option); }), ), logger: new PluginLogger({origin: loadedPlugin.name}), tracer: new PluginTracer({ origin: loadedPlugin.name, category: 'loadConfig', }), }); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { origin: loadedPlugin.name, }), }); } } const configKeyCache = createBuildCache(); export async function getConfigKeyContentHash( filePath: ProjectPath, configKey: string, options: ParcelOptions, ): Async<string> { let cacheKey = `${fromProjectPathRelative(filePath)}:${configKey}`; let cachedValue = configKeyCache.get(cacheKey); if (cachedValue) { return cachedValue; } let conf = await readConfig( options.inputFS, fromProjectPath(options.projectRoot, filePath), ); if (conf == null || conf.config[configKey] == null) { // This can occur when a config key has been removed entirely during `respondToFSEvents` return ''; } let contentHash = typeof conf.config[configKey] === 'object' ? hashObject(conf.config[configKey]) : hashString(JSON.stringify(conf.config[configKey])); configKeyCache.set(cacheKey, contentHash); return contentHash; } export async function runConfigRequest<TResult: RequestResult>( api: RunAPI<TResult>, configRequest: ConfigRequest, ) { let { invalidateOnFileChange, invalidateOnConfigKeyChange, invalidateOnFileCreate, invalidateOnEnvChange, invalidateOnOptionChange, invalidateOnStartup, invalidateOnBuild, } = configRequest; // If there are no invalidations, then no need to create a node. if ( invalidateOnFileChange.size === 0 && invalidateOnConfigKeyChange.length === 0 && invalidateOnFileCreate.length === 0 && invalidateOnOptionChange.size === 0 && invalidateOnEnvChange.size === 0 && !invalidateOnStartup && !invalidateOnBuild ) { return; } await api.runRequest<null, void>({ id: 'config_request:' + configRequest.id, type: requestTypes.config_request, run: async ({api, options}) => { for (let filePath of invalidateOnFileChange) { api.invalidateOnFileUpdate(filePath); api.invalidateOnFileDelete(filePath); } for (let {filePath, configKey} of invalidateOnConfigKeyChange) { let contentHash = await getConfigKeyContentHash( filePath, configKey, options, ); api.invalidateOnConfigKeyChange(filePath, configKey, contentHash); } for (let invalidation of invalidateOnFileCreate) { api.invalidateOnFileCreate(invalidation); } for (let env of invalidateOnEnvChange) { api.invalidateOnEnvChange(env); } for (let option of invalidateOnOptionChange) { api.invalidateOnOptionChange(option); } if (invalidateOnStartup) { api.invalidateOnStartup(); } if (invalidateOnBuild) { api.invalidateOnBuild(); } }, input: null, }); } export async function getConfigHash( config: Config, pluginName: string, options: ParcelOptions, ): Promise<string> { if (config.result == null) { return ''; } let hash = new Hash(); hash.writeString(config.id); // If there is no result hash set by the transformer, default to hashing the included // files if any, otherwise try to hash the config result itself. if (config.cacheKey == null) { if (config.invalidateOnFileChange.size > 0) { hash.writeString( await getInvalidationHash( [...config.invalidateOnFileChange].map(filePath => ({ type: 'file', filePath, })), options, ), ); } else if (config.result != null) { try { hash.writeBuffer(serializeRaw(config.result)); } catch (err) { throw new ThrowableDiagnostic({ diagnostic: { message: 'Config result is not hashable because it contains non-serializable objects. Please use config.setCacheKey to set the hash manually.', origin: pluginName, }, }); } } } else { hash.writeString(config.cacheKey ?? ''); } return hash.finish(); } export function getConfigRequests( configs: Array<Config>, ): Array<ConfigRequest> { return configs .filter(config => { // No need to send to the graph if there are no invalidations. return ( config.invalidateOnFileChange.size > 0 || config.invalidateOnConfigKeyChange.length > 0 || config.invalidateOnFileCreate.length > 0 || config.invalidateOnEnvChange.size > 0 || config.invalidateOnOptionChange.size > 0 || config.invalidateOnStartup || config.invalidateOnBuild ); }) .map(config => ({ id: config.id, invalidateOnFileChange: config.invalidateOnFileChange, invalidateOnConfigKeyChange: config.invalidateOnConfigKeyChange, invalidateOnFileCreate: config.invalidateOnFileCreate, invalidateOnEnvChange: config.invalidateOnEnvChange, invalidateOnOptionChange: config.invalidateOnOptionChange, invalidateOnStartup: config.invalidateOnStartup, invalidateOnBuild: config.invalidateOnBuild, })); }