@parcel/core
Version:
229 lines (206 loc) • 7.1 kB
JavaScript
// @flow strict-local
import type {ContentKey} from '@parcel/graph';
import type {Async} from '@parcel/types';
import type {SharedReference} from '@parcel/workers';
import type {StaticRunOpts} from '../RequestTracker';
import {requestTypes} from '../RequestTracker';
import type {PackagedBundleInfo} from '../types';
import type BundleGraph from '../BundleGraph';
import type {BundleInfo} from '../PackagerRunner';
import {HASH_REF_PREFIX} from '../constants';
import {joinProjectPath} from '../projectPath';
import nullthrows from 'nullthrows';
import {hashString} from '@parcel/rust';
import {
canSkipPackageRequest,
createPackageRequest,
getPackageRequestId,
} from './PackageRequest';
import createWriteBundleRequest from './WriteBundleRequest';
type WriteBundlesRequestInput = {|
bundleGraph: BundleGraph,
optionsRef: SharedReference,
|};
export type WriteBundlesRequestResult = Map<string, PackagedBundleInfo[]>;
type RunInput<TResult> = {|
input: WriteBundlesRequestInput,
...StaticRunOpts<TResult>,
|};
export type WriteBundlesRequest = {|
id: ContentKey,
+type: typeof requestTypes.write_bundles_request,
run: (
RunInput<WriteBundlesRequestResult>,
) => Async<WriteBundlesRequestResult>,
input: WriteBundlesRequestInput,
|};
/**
* Packages, optimizes, and writes all bundles to the dist directory.
*/
export default function createWriteBundlesRequest(
input: WriteBundlesRequestInput,
): WriteBundlesRequest {
return {
type: requestTypes.write_bundles_request,
id: 'write_bundles:' + input.bundleGraph.getBundleGraphHash(),
run,
input,
};
}
async function run({input, api, farm, options}) {
let {bundleGraph, optionsRef} = input;
let {ref, dispose} = await farm.createSharedReference(bundleGraph);
api.invalidateOnOptionChange('shouldContentHash');
let res = new Map();
let bundleInfoMap: {|
[string]: BundleInfo[],
|} = {};
let writeEarlyPromises: {[string]: Promise<PackagedBundleInfo[]>} = {};
let hashRefToNameHash = new Map();
let bundles = bundleGraph.getBundles().filter(bundle => {
// Do not package and write placeholder bundles to disk. We just
// need to update the name so other bundles can reference it.
if (bundle.isPlaceholder) {
let hash = bundle.id.slice(-8);
hashRefToNameHash.set(bundle.hashReference, hash);
let name = nullthrows(
bundle.name,
`Expected ${bundle.type} bundle to have a name`,
).replace(bundle.hashReference, hash);
res.set(bundle.id, [
{
filePath: joinProjectPath(bundle.target.distDir, name),
type: bundle.type, // FIXME: this is wrong if the packager changes the type...
stats: {
time: 0,
size: 0,
},
},
]);
return false;
}
return true;
});
// Package on the main thread if there is only one bundle to package.
// This avoids the cost of serializing the bundle graph for single file change builds.
let useMainThread =
bundles.length === 1 ||
bundles.filter(b => !api.canSkipSubrequest(getPackageRequestId(b)))
.length === 1;
try {
await Promise.all(
bundles.map(async bundle => {
let request = createPackageRequest({
bundle,
bundleGraph,
bundleGraphReference: ref,
optionsRef,
useMainThread,
});
let {info: infos} = await api.runRequest(request, {
force: !(await canSkipPackageRequest(api, request)),
});
if (!useMainThread) {
// Force a refresh of the cache to avoid a race condition
// between threaded reads and writes that can result in an LMDB cache miss:
// 1. The main thread has read some value from cache, necessitating a read transaction.
// 2. Concurrently, Thread A finishes a packaging request.
// 3. Subsequently, the main thread is tasked with this request, but fails because the read transaction is stale.
// This only occurs if the reading thread has a transaction that was created before the writing thread committed,
// and the transaction is still live when the reading thread attempts to get the written value.
// See https://github.com/parcel-bundler/parcel/issues/9121
options.cache.refresh();
}
bundleInfoMap[bundle.id] = infos;
if (infos.every(info => info.hashReferences.length === 0)) {
hashRefToNameHash.set(
bundle.hashReference,
options.shouldContentHash
? infos.length === 1
? infos[0].hash.slice(-8)
: hashString(infos.map(i => i.hash).join(':')).slice(-8)
: bundle.id.slice(-8),
);
for (let info of infos) {
let writeBundleRequest = createWriteBundleRequest({
bundle,
info,
hashRefToNameHash,
bundleGraph,
});
let promise = api.runRequest(writeBundleRequest);
// If the promise rejects before we await it (below), we don't want to crash the build.
promise.catch(() => {});
writeEarlyPromises[info.cacheKeys.content] = promise;
}
}
}),
);
assignComplexNameHashes(hashRefToNameHash, bundles, bundleInfoMap, options);
await Promise.all(
bundles.map(bundle => {
let promise = Promise.all(
bundleInfoMap[bundle.id].map(info => {
return (
writeEarlyPromises[info.cacheKeys.content] ??
api.runRequest(
createWriteBundleRequest({
bundle,
info,
hashRefToNameHash,
bundleGraph,
}),
)
);
}),
);
return promise.then(r => res.set(bundle.id, r.flat()));
}),
);
api.storeResult(res);
return res;
} finally {
await dispose();
}
}
function assignComplexNameHashes(
hashRefToNameHash,
bundles,
bundleInfoMap,
options,
) {
for (let bundle of bundles) {
if (hashRefToNameHash.get(bundle.hashReference) != null) {
continue;
}
hashRefToNameHash.set(
bundle.hashReference,
options.shouldContentHash
? hashString(
[...getBundlesIncludedInHash(bundle.id, bundleInfoMap)]
.flatMap(bundleId => bundleInfoMap[bundleId].map(i => i.hash))
.join(':'),
).slice(-8)
: bundle.id.slice(-8),
);
}
}
function getBundlesIncludedInHash(
bundleId,
bundleInfoMap,
included = new Set(),
) {
included.add(bundleId);
for (let info of bundleInfoMap[bundleId]) {
for (let hashRef of info.hashReferences) {
let referencedId = getIdFromHashRef(hashRef);
if (!included.has(referencedId)) {
getBundlesIncludedInHash(referencedId, bundleInfoMap, included);
}
}
}
return included;
}
function getIdFromHashRef(hashRef: string) {
return hashRef.slice(HASH_REF_PREFIX.length);
}