UNPKG

@parcel/core

Version:
233 lines (208 loc) • 7 kB
// @flow import type {DependencySpecifier, SemverRange} from '@parcel/types'; import type ParcelConfig from '../ParcelConfig'; import type { DevDepRequest, ParcelOptions, InternalDevDepOptions, } from '../types'; import type {RunAPI} from '../RequestTracker'; import type {ProjectPath} from '../projectPath'; import nullthrows from 'nullthrows'; import {getInvalidationHash} from '../assetUtils'; import {createBuildCache} from '../buildCache'; import {invalidateOnFileCreateToInternal} from '../utils'; import { fromProjectPath, fromProjectPathRelative, toProjectPath, } from '../projectPath'; // A cache of dev dep requests keyed by invalidations. // If the package manager returns the same invalidation object, then // we can reuse the dev dep request rather than recomputing the project // paths and hashes. const devDepRequestCache = new WeakMap(); export async function createDevDependency( opts: InternalDevDepOptions, requestDevDeps: Map<string, string>, options: ParcelOptions, ): Promise<DevDepRequest> { let {specifier, resolveFrom, additionalInvalidations} = opts; let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; // If the request sent us a hash, we know the dev dep and all of its dependencies didn't change. // Reuse the same hash in the response. No need to send back invalidations as the request won't // be re-run anyway. let hash = requestDevDeps.get(key); if (hash != null) { return { specifier, resolveFrom, hash, }; } let resolveFromAbsolute = fromProjectPath(options.projectRoot, resolveFrom); // Ensure that the package manager has an entry for this resolution. await options.packageManager.resolve(specifier, resolveFromAbsolute); let invalidations = options.packageManager.getInvalidations( specifier, resolveFromAbsolute, ); let cached = devDepRequestCache.get(invalidations); if (cached != null) { return cached; } let invalidateOnFileChangeProject = [ ...invalidations.invalidateOnFileChange, ].map(f => toProjectPath(options.projectRoot, f)); // It is possible for a transformer to have multiple different hashes due to // different dependencies (e.g. conditional requires) so we must always // recompute the hash and compare rather than only sending a transformer // dev dependency once. hash = await getInvalidationHash( invalidateOnFileChangeProject.map(f => ({ type: 'file', filePath: f, })), options, ); let devDepRequest: DevDepRequest = { specifier, resolveFrom, hash, invalidateOnFileCreate: invalidations.invalidateOnFileCreate.map(i => invalidateOnFileCreateToInternal(options.projectRoot, i), ), invalidateOnFileChange: new Set(invalidateOnFileChangeProject), invalidateOnStartup: invalidations.invalidateOnStartup, additionalInvalidations, }; devDepRequestCache.set(invalidations, devDepRequest); return devDepRequest; } export type DevDepSpecifier = {| specifier: DependencySpecifier, resolveFrom: ProjectPath, |}; type DevDepRequests = {| devDeps: Map<string, string>, invalidDevDeps: Array<DevDepSpecifier>, |}; export async function getDevDepRequests<TResult>( api: RunAPI<TResult>, ): Promise<DevDepRequests> { let previousDevDepRequests = new Map( await Promise.all( api .getSubRequests() .filter(req => req.type === 'dev_dep_request') .map(async req => [ req.id, nullthrows(await api.getRequestResult<DevDepRequest>(req.id)), ]), ), ); return { devDeps: new Map( [...previousDevDepRequests.entries()] .filter(([id]) => api.canSkipSubrequest(id)) .map(([, req]) => [ `${req.specifier}:${fromProjectPathRelative(req.resolveFrom)}`, req.hash, ]), ), invalidDevDeps: await Promise.all( [...previousDevDepRequests.entries()] .filter(([id]) => !api.canSkipSubrequest(id)) .flatMap(([, req]) => { return [ { specifier: req.specifier, resolveFrom: req.resolveFrom, }, ...(req.additionalInvalidations ?? []).map(i => ({ specifier: i.specifier, resolveFrom: i.resolveFrom, })), ]; }), ), }; } // Tracks dev deps that have been invalidated during this build // so we don't invalidate the require cache more than once. const invalidatedDevDeps = createBuildCache(); export function invalidateDevDeps( invalidDevDeps: Array<DevDepSpecifier>, options: ParcelOptions, config: ParcelConfig, ) { for (let {specifier, resolveFrom} of invalidDevDeps) { let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`; if (!invalidatedDevDeps.has(key)) { config.invalidatePlugin(specifier); options.packageManager.invalidate( specifier, fromProjectPath(options.projectRoot, resolveFrom), ); invalidatedDevDeps.set(key, true); } } } type DevDepRequestResult = {| specifier: DependencySpecifier, resolveFrom: ProjectPath, hash: string, additionalInvalidations: void | Array<{| range?: ?SemverRange, resolveFrom: ProjectPath, specifier: DependencySpecifier, |}>, |}; export async function runDevDepRequest<TResult>( api: RunAPI<TResult>, devDepRequest: DevDepRequest, ) { await api.runRequest<null, DevDepRequestResult | void>({ id: 'dev_dep_request:' + devDepRequest.specifier + ':' + devDepRequest.hash, type: 'dev_dep_request', run: ({api}) => { for (let filePath of nullthrows(devDepRequest.invalidateOnFileChange)) { api.invalidateOnFileUpdate(filePath); api.invalidateOnFileDelete(filePath); } for (let invalidation of nullthrows( devDepRequest.invalidateOnFileCreate, )) { api.invalidateOnFileCreate(invalidation); } if (devDepRequest.invalidateOnStartup) { api.invalidateOnStartup(); } api.storeResult({ specifier: devDepRequest.specifier, resolveFrom: devDepRequest.resolveFrom, hash: devDepRequest.hash, additionalInvalidations: devDepRequest.additionalInvalidations, }); }, input: null, }); } // A cache of plugin dependency hashes that we've already sent to the main thread. // Automatically cleared before each build. const pluginCache = createBuildCache(); export function getWorkerDevDepRequests( devDepRequests: Array<DevDepRequest>, ): Array<DevDepRequest> { return devDepRequests.map(devDepRequest => { // If we've already sent a matching transformer + hash to the main thread during this build, // there's no need to repeat ourselves. let {specifier, resolveFrom, hash} = devDepRequest; if (hash === pluginCache.get(specifier)) { return {specifier, resolveFrom, hash}; } else { pluginCache.set(specifier, hash); return devDepRequest; } }); }