import type { DependencySpecifier, SemverRange, Invalidations, } from '@atlaspack/types'; import type {AtlaspackConfig} from '../AtlaspackConfig'; import type { DevDepRequest, AtlaspackOptions, InternalDevDepOptions, DevDepRequestRef, } from '../types'; import type {RequestResult, RunAPI} from '../RequestTracker'; import type {ProjectPath} from '../projectPath'; import {createBuildCache} from '@atlaspack/build-cache'; import nullthrows from 'nullthrows'; import {getInvalidationHash} from '../assetUtils'; import {invalidateOnFileCreateToInternal} from '../utils'; import { fromProjectPath, fromProjectPathRelative, toProjectPath, } from '../projectPath'; import {requestTypes} from '../RequestTracker'; // 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: WeakMap = new WeakMap(); export async function createDevDependency( opts: InternalDevDepOptions, requestDevDeps: Map, options: AtlaspackOptions, ): Promise { 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 { type: 'ref', specifier, resolveFrom, hash, }; } let resolveFromAbsolute = fromProjectPath(options.projectRoot, resolveFrom); // Ensure that the package manager has an entry for this resolution. try { await options.packageManager.resolve(specifier, resolveFromAbsolute); } catch (err: any) { // ignore } 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; invalidDevDeps: Array; }; export async function getDevDepRequests( api: RunAPI, ): Promise { async function getPreviousDevDepRequests() { const allDevDepRequests = await Promise.all( api .getSubRequests() .filter((req) => req.requestType === requestTypes.dev_dep_request) .map( async ( req, ): Promise<[string, DevDepRequestResult | null | undefined]> => [ req.id, await api.getRequestResult(req.id), ], ), ); const nonNullDevDepRequests: Array> = []; for (const [id, result] of allDevDepRequests) { if (result != null) { nonNullDevDepRequests.push([id, result]); } } // @ts-expect-error TS2769 return new Map(nonNullDevDepRequests); } const previousDevDepRequests = await getPreviousDevDepRequests(); return { devDeps: new Map( [...previousDevDepRequests.entries()] // @ts-expect-error TS2769 .filter(([id]: [any]) => api.canSkipSubrequest(id)) .map(([, req]: [any, any]) => [ `${req.specifier}:${fromProjectPathRelative(req.resolveFrom)}`, req.hash, ]), ), invalidDevDeps: await Promise.all( [...previousDevDepRequests.entries()] // @ts-expect-error TS2769 .filter(([id]: [any]) => !api.canSkipSubrequest(id)) .flatMap(([, req]: [any, any]) => { return [ { specifier: req.specifier, resolveFrom: req.resolveFrom, }, // @ts-expect-error TS7006 ...(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, options: AtlaspackOptions, config: AtlaspackConfig, ) { 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); } } } export type DevDepRequestResult = { specifier: DependencySpecifier; resolveFrom: ProjectPath; hash: string; additionalInvalidations: | undefined | Array<{ range?: SemverRange | null | undefined; resolveFrom: ProjectPath; specifier: DependencySpecifier; }>; }; const devDepRequests: Map = createBuildCache(); export function resolveDevDepRequestRef( devDepRequestRef: DevDepRequest | DevDepRequestRef, ): DevDepRequest { const devDepRequest = // @ts-expect-error TS2339 devDepRequestRef.type === 'ref' ? devDepRequests.get(devDepRequestRef.hash) : devDepRequestRef; if (devDepRequest == null) { throw new Error( `Worker send back a reference to a missing dev dep request. This might happen due to internal in-memory build caches not being cleared between builds or due a race condition. ${ process.env.NODE_ENV === 'test' ? `If this is a unit test, call atlaspack.clearBuildCaches() between tests` : '' } This is a bug in Atlaspack.`, ); } // @ts-expect-error TS2339 if (devDepRequestRef.type !== 'ref') { // @ts-expect-error TS2345 devDepRequests.set(devDepRequest.hash, devDepRequest); } // @ts-expect-error TS2322 return devDepRequest; } export async function runDevDepRequest( api: RunAPI, devDepRequestRef: DevDepRequest | DevDepRequestRef, ) { await api.runRequest({ id: 'dev_dep_request:' + devDepRequestRef.specifier + ':' + devDepRequestRef.hash, type: requestTypes.dev_dep_request, // @ts-expect-error TS2322 run: ({api}) => { const devDepRequest = resolveDevDepRequestRef(devDepRequestRef); for (let filePath of nullthrows( devDepRequest.invalidateOnFileChange, 'DevDepRequest missing invalidateOnFileChange', )) { api.invalidateOnFileUpdate(filePath); api.invalidateOnFileDelete(filePath); } for (let invalidation of nullthrows( devDepRequest.invalidateOnFileCreate, 'DevDepRequest missing 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, ): Array { 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 {type: 'ref', specifier, resolveFrom, hash}; } else { pluginCache.set(specifier, hash); return devDepRequest; } }); }