@parcel/core
Version:
545 lines (487 loc) • 16.6 kB
JavaScript
// @flow strict-local
import type {Async, Bundle as IBundle, Namer} from '@parcel/types';
import type {SharedReference} from '@parcel/workers';
import type ParcelConfig, {LoadedPlugin} from '../ParcelConfig';
import type {StaticRunOpts, RunAPI} from '../RequestTracker';
import type {
Asset,
AssetGroup,
Bundle as InternalBundle,
Config,
DevDepRequest,
DevDepRequestRef,
ParcelOptions,
} from '../types';
import type {ConfigAndCachePath} from './ParcelConfigRequest';
import invariant from 'assert';
import assert from 'assert';
import nullthrows from 'nullthrows';
import {PluginLogger} from '@parcel/logger';
import ThrowableDiagnostic, {errorToDiagnostic} from '@parcel/diagnostic';
import AssetGraph from '../AssetGraph';
import BundleGraph from '../public/BundleGraph';
import InternalBundleGraph, {bundleGraphEdgeTypes} from '../BundleGraph';
import MutableBundleGraph from '../public/MutableBundleGraph';
import {Bundle, NamedBundle} from '../public/Bundle';
import {report} from '../ReporterRunner';
import dumpGraphToGraphViz from '../dumpGraphToGraphViz';
import {hashString} from '@parcel/rust';
import PluginOptions from '../public/PluginOptions';
import applyRuntimes from '../applyRuntimes';
import {PARCEL_VERSION, OPTION_CHANGE} from '../constants';
import {assertSignalNotAborted, optionsProxy} from '../utils';
import createParcelConfigRequest, {
getCachedParcelConfig,
} from './ParcelConfigRequest';
import {
createDevDependency,
getDevDepRequests,
invalidateDevDeps,
runDevDepRequest,
} from './DevDepRequest';
import {createConfig} from '../InternalConfig';
import {
loadPluginConfig,
runConfigRequest,
type PluginWithLoadConfig,
} from './ConfigRequest';
import {
joinProjectPath,
fromProjectPathRelative,
toProjectPathUnsafe,
} from '../projectPath';
import createAssetGraphRequest from './AssetGraphRequest';
import {tracer, PluginTracer} from '@parcel/profiler';
import {requestTypes} from '../RequestTracker';
import type {ProjectPath} from '../projectPath';
type BundleGraphRequestInput = {|
requestedAssetIds: Set<string>,
signal?: AbortSignal,
optionsRef: SharedReference,
|};
type BundleGraphRequestResult = {|
bundleGraph: InternalBundleGraph,
|};
type RunInput = {|
input: BundleGraphRequestInput,
...StaticRunOpts<BundleGraphResult>,
|};
// TODO: Rename to BundleGraphRequestResult
export type BundleGraphResult = {|
bundleGraph: InternalBundleGraph,
changedAssets: Map<string, Asset>,
assetRequests: Array<AssetGroup>,
|};
type BundleGraphRequest = {|
id: string,
+type: typeof requestTypes.bundle_graph_request,
run: RunInput => Async<BundleGraphResult>,
input: BundleGraphRequestInput,
|};
export default function createBundleGraphRequest(
input: BundleGraphRequestInput,
): BundleGraphRequest {
return {
type: requestTypes.bundle_graph_request,
id: 'BundleGraph',
run: async input => {
let {options, api, invalidateReason} = input;
let {optionsRef, requestedAssetIds, signal} = input.input;
let measurement = tracer.createMeasurement('building');
let request = createAssetGraphRequest({
name: 'Main',
entries: options.entries,
optionsRef,
shouldBuildLazily: options.shouldBuildLazily,
lazyIncludes: options.lazyIncludes,
lazyExcludes: options.lazyExcludes,
requestedAssetIds,
});
let {assetGraph, changedAssets, assetRequests} = await api.runRequest(
request,
{
force: options.shouldBuildLazily && requestedAssetIds.size > 0,
},
);
measurement && measurement.end();
assertSignalNotAborted(signal);
// If any subrequests are invalid (e.g. dev dep requests or config requests),
// bail on incremental bundling. We also need to invalidate for option changes,
// which are hoisted to direct invalidations on the bundle graph request.
let subRequestsInvalid =
Boolean(invalidateReason & OPTION_CHANGE) ||
input.api
.getSubRequests()
.some(req => !input.api.canSkipSubrequest(req.id));
if (subRequestsInvalid) {
assetGraph.safeToIncrementallyBundle = false;
}
let configResult = nullthrows(
await input.api.runRequest<null, ConfigAndCachePath>(
createParcelConfigRequest(),
),
);
assertSignalNotAborted(signal);
let parcelConfig = getCachedParcelConfig(configResult, input.options);
let {devDeps, invalidDevDeps} = await getDevDepRequests(input.api);
invalidateDevDeps(invalidDevDeps, input.options, parcelConfig);
let bundlingMeasurement = tracer.createMeasurement('bundling');
let builder = new BundlerRunner(input, parcelConfig, devDeps);
let res: BundleGraphResult = await builder.bundle({
graph: assetGraph,
changedAssets: changedAssets,
assetRequests,
});
bundlingMeasurement && bundlingMeasurement.end();
for (let [id, asset] of changedAssets) {
res.changedAssets.set(id, asset);
}
await dumpGraphToGraphViz(
// $FlowFixMe Added in Flow 0.121.0 upgrade in #4381 (Windows only)
res.bundleGraph._graph,
'BundleGraph',
bundleGraphEdgeTypes,
);
return res;
},
input,
};
}
class BundlerRunner {
options: ParcelOptions;
optionsRef: SharedReference;
config: ParcelConfig;
pluginOptions: PluginOptions;
api: RunAPI<BundleGraphResult>;
previousDevDeps: Map<string, string>;
devDepRequests: Map<string, DevDepRequest | DevDepRequestRef>;
configs: Map<string, Config>;
cacheKey: string;
constructor(
{input, api, options}: RunInput,
config: ParcelConfig,
previousDevDeps: Map<string, string>,
) {
this.options = options;
this.api = api;
this.optionsRef = input.optionsRef;
this.config = config;
this.previousDevDeps = previousDevDeps;
this.devDepRequests = new Map();
this.configs = new Map();
this.pluginOptions = new PluginOptions(
optionsProxy(this.options, api.invalidateOnOptionChange),
);
this.cacheKey =
hashString(
`${PARCEL_VERSION}:BundleGraph:${
JSON.stringify(options.entries) ?? ''
}${options.mode}${options.shouldBuildLazily ? 'lazy' : 'eager'}`,
) + '-BundleGraph';
}
async loadConfigs() {
// Load all configs up front so we can use them in the cache key
let bundler = await this.config.getBundler();
await this.loadConfig(bundler);
let namers = await this.config.getNamers();
for (let namer of namers) {
await this.loadConfig(namer);
}
let runtimes = await this.config.getRuntimes();
for (let runtime of runtimes) {
await this.loadConfig(runtime);
}
}
async loadConfig<T: PluginWithLoadConfig>(plugin: LoadedPlugin<T>) {
let config = createConfig({
plugin: plugin.name,
searchPath: toProjectPathUnsafe('index'),
});
await loadPluginConfig(plugin, config, this.options);
await runConfigRequest(this.api, config);
for (let devDep of config.devDeps) {
let devDepRequest = await createDevDependency(
devDep,
this.previousDevDeps,
this.options,
);
await this.runDevDepRequest(devDepRequest);
}
this.configs.set(plugin.name, config);
}
async runDevDepRequest(devDepRequest: DevDepRequest | DevDepRequestRef) {
let {specifier, resolveFrom} = devDepRequest;
let key = `${specifier}:${fromProjectPathRelative(resolveFrom)}`;
this.devDepRequests.set(key, devDepRequest);
await runDevDepRequest(this.api, devDepRequest);
}
async bundle({
graph,
changedAssets,
assetRequests,
}: {|
graph: AssetGraph,
changedAssets: Map<string, Asset>,
assetRequests: Array<AssetGroup>,
|}): Promise<BundleGraphResult> {
report({
type: 'buildProgress',
phase: 'bundling',
});
await this.loadConfigs();
let plugin = await this.config.getBundler();
let {plugin: bundler, name, resolveFrom} = plugin;
// if a previous asset graph hash is passed in, check if the bundle graph is also available
let previousBundleGraphResult: ?BundleGraphRequestResult;
if (graph.safeToIncrementallyBundle) {
try {
previousBundleGraphResult = await this.api.getPreviousResult();
} catch {
// if the bundle graph had an error or was removed, don't fail the build
}
}
if (previousBundleGraphResult == null) {
graph.safeToIncrementallyBundle = false;
}
let internalBundleGraph;
let logger = new PluginLogger({origin: name});
let tracer = new PluginTracer({
origin: name,
category: 'bundle',
});
try {
if (previousBundleGraphResult) {
internalBundleGraph = previousBundleGraphResult.bundleGraph;
for (let changedAssetId of changedAssets.keys()) {
// Copy over the whole node to also have correct symbol data
let changedAssetNode = nullthrows(
graph.getNodeByContentKey(changedAssetId),
);
invariant(changedAssetNode.type === 'asset');
internalBundleGraph.updateAsset(changedAssetNode);
}
} else {
internalBundleGraph = InternalBundleGraph.fromAssetGraph(
graph,
this.options.mode === 'production',
);
invariant(internalBundleGraph != null); // ensures the graph was created
await dumpGraphToGraphViz(
// $FlowFixMe
internalBundleGraph._graph,
'before_bundle',
bundleGraphEdgeTypes,
);
let mutableBundleGraph = new MutableBundleGraph(
internalBundleGraph,
this.options,
);
let measurement;
let measurementFilename;
if (tracer.enabled) {
measurementFilename = graph
.getEntryAssets()
.map(asset => fromProjectPathRelative(asset.filePath))
.join(', ');
measurement = tracer.createMeasurement(
plugin.name,
'bundling:bundle',
measurementFilename,
);
}
// this the normal bundle workflow (bundle, optimizing, run-times, naming)
await bundler.bundle({
bundleGraph: mutableBundleGraph,
config: this.configs.get(plugin.name)?.result,
options: this.pluginOptions,
logger,
tracer,
});
measurement && measurement.end();
if (this.pluginOptions.mode === 'production') {
let optimizeMeasurement;
try {
if (tracer.enabled) {
optimizeMeasurement = tracer.createMeasurement(
plugin.name,
'bundling:optimize',
nullthrows(measurementFilename),
);
}
await bundler.optimize({
bundleGraph: mutableBundleGraph,
config: this.configs.get(plugin.name)?.result,
options: this.pluginOptions,
logger,
});
} catch (e) {
throw new ThrowableDiagnostic({
diagnostic: errorToDiagnostic(e, {
origin: plugin.name,
}),
});
} finally {
optimizeMeasurement && optimizeMeasurement.end();
await dumpGraphToGraphViz(
// $FlowFixMe[incompatible-call]
internalBundleGraph._graph,
'after_optimize',
);
}
}
// Add dev dependency for the bundler. 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: name,
resolveFrom,
},
this.previousDevDeps,
this.options,
);
await this.runDevDepRequest(devDepRequest);
}
} catch (e) {
if (internalBundleGraph != null) {
this.api.storeResult(
{
bundleGraph: internalBundleGraph,
changedAssets: new Map(),
assetRequests: [],
},
this.cacheKey,
);
}
throw new ThrowableDiagnostic({
diagnostic: errorToDiagnostic(e, {
origin: name,
}),
});
} finally {
invariant(internalBundleGraph != null); // ensures the graph was created
await dumpGraphToGraphViz(
// $FlowFixMe[incompatible-call]
internalBundleGraph._graph,
'after_bundle',
bundleGraphEdgeTypes,
);
}
let changedRuntimes = new Map();
if (!previousBundleGraphResult) {
let namers = await this.config.getNamers();
// inline bundles must still be named so the PackagerRunner
// can match them to the correct packager/optimizer plugins.
let bundles = internalBundleGraph.getBundles({includeInline: true});
await Promise.all(
bundles.map(bundle =>
this.nameBundle(namers, bundle, internalBundleGraph),
),
);
changedRuntimes = await applyRuntimes({
bundleGraph: internalBundleGraph,
api: this.api,
config: this.config,
options: this.options,
optionsRef: this.optionsRef,
pluginOptions: this.pluginOptions,
previousDevDeps: this.previousDevDeps,
devDepRequests: this.devDepRequests,
configs: this.configs,
});
// Add dev deps for namers, AFTER running them to account for lazy require().
for (let namer of namers) {
let devDepRequest = await createDevDependency(
{
specifier: namer.name,
resolveFrom: namer.resolveFrom,
},
this.previousDevDeps,
this.options,
);
await this.runDevDepRequest(devDepRequest);
}
this.validateBundles(internalBundleGraph);
// Pre-compute the hashes for each bundle so they are only computed once and shared between workers.
internalBundleGraph.getBundleGraphHash();
}
await dumpGraphToGraphViz(
// $FlowFixMe
internalBundleGraph._graph,
'after_runtimes',
bundleGraphEdgeTypes,
);
this.api.storeResult(
{
bundleGraph: internalBundleGraph,
changedAssets: new Map(),
assetRequests: [],
},
this.cacheKey,
);
return {
bundleGraph: internalBundleGraph,
changedAssets: changedRuntimes,
assetRequests,
};
}
validateBundles(bundleGraph: InternalBundleGraph): void {
let bundles = bundleGraph.getBundles();
let bundleNames = bundles.map(b =>
joinProjectPath(b.target.distDir, nullthrows(b.name)),
);
let seenNames = new Set<ProjectPath>();
let duplicateNames = bundleNames.filter(name => {
let isDuplicate = seenNames.has(name);
seenNames.add(name);
return isDuplicate;
});
assert(
duplicateNames.length === 0,
'Bundles must have unique names. Conflicting names: ' +
duplicateNames.join(', '),
);
}
async nameBundle(
namers: Array<LoadedPlugin<Namer<mixed>>>,
internalBundle: InternalBundle,
internalBundleGraph: InternalBundleGraph,
): Promise<void> {
let bundle = Bundle.get(internalBundle, internalBundleGraph, this.options);
let bundleGraph = new BundleGraph<IBundle>(
internalBundleGraph,
NamedBundle.get.bind(NamedBundle),
this.options,
);
for (let namer of namers) {
let measurement;
try {
measurement = tracer.createMeasurement(namer.name, 'namer', bundle.id);
let name = await namer.plugin.name({
bundle,
bundleGraph,
config: this.configs.get(namer.name)?.result,
options: this.pluginOptions,
logger: new PluginLogger({origin: namer.name}),
tracer: new PluginTracer({origin: namer.name, category: 'namer'}),
});
if (name != null) {
internalBundle.name = name;
let {hashReference} = internalBundle;
internalBundle.displayName = name.includes(hashReference)
? name.replace(hashReference, '[hash]')
: name;
return;
}
} catch (e) {
throw new ThrowableDiagnostic({
diagnostic: errorToDiagnostic(e, {
origin: namer.name,
}),
});
} finally {
measurement && measurement.end();
}
}
throw new Error('Unable to name bundle');
}
}