@parcel/core
Version:
572 lines (489 loc) • 16.3 kB
JavaScript
// @flow strict-local
import type {
Asset,
AsyncSubscription,
BuildEvent,
BuildSuccessEvent,
InitialParcelOptions,
PackagedBundle as IPackagedBundle,
ParcelTransformOptions,
ParcelResolveOptions,
ParcelResolveResult,
} from '@parcel/types';
import path from 'path';
import type {ParcelOptions} from './types';
// eslint-disable-next-line no-unused-vars
import type {FarmOptions, SharedReference} from '@parcel/workers';
import type {Diagnostic} from '@parcel/diagnostic';
import invariant from 'assert';
import ThrowableDiagnostic, {anyToDiagnostic} from '@parcel/diagnostic';
import {assetFromValue} from './public/Asset';
import {PackagedBundle} from './public/Bundle';
import BundleGraph from './public/BundleGraph';
import WorkerFarm from '@parcel/workers';
import nullthrows from 'nullthrows';
import {BuildAbortError} from './utils';
import {loadParcelConfig} from './requests/ParcelConfigRequest';
import ReporterRunner from './ReporterRunner';
import dumpGraphToGraphViz from './dumpGraphToGraphViz';
import resolveOptions from './resolveOptions';
import {ValueEmitter} from '@parcel/events';
import {registerCoreWithSerializer} from './registerCoreWithSerializer';
import {PromiseQueue} from '@parcel/utils';
import ParcelConfig from './ParcelConfig';
import logger from '@parcel/logger';
import RequestTracker, {
getWatcherOptions,
requestGraphEdgeTypes,
} from './RequestTracker';
import createValidationRequest from './requests/ValidationRequest';
import createParcelBuildRequest from './requests/ParcelBuildRequest';
import createAssetRequest from './requests/AssetRequest';
import createPathRequest from './requests/PathRequest';
import {createEnvironment} from './Environment';
import {createDependency} from './Dependency';
import {Disposable} from '@parcel/events';
import {init as initSourcemaps} from '@parcel/source-map';
import {init as initRust} from '@parcel/rust';
import {
fromProjectPath,
toProjectPath,
fromProjectPathRelative,
} from './projectPath';
import {tracer} from '@parcel/profiler';
import {setFeatureFlags} from '@parcel/feature-flags';
registerCoreWithSerializer();
export const INTERNAL_TRANSFORM: symbol = Symbol('internal_transform');
export const INTERNAL_RESOLVE: symbol = Symbol('internal_resolve');
export default class Parcel {
#requestTracker /*: RequestTracker*/;
#config /*: ParcelConfig*/;
#farm /*: WorkerFarm*/;
#initialized /*: boolean*/ = false;
#disposable /*: Disposable */;
#initialOptions /*: InitialParcelOptions */;
#reporterRunner /*: ReporterRunner*/;
#resolvedOptions /*: ?ParcelOptions*/ = null;
#optionsRef /*: SharedReference */;
#watchAbortController /*: AbortController*/;
#watchQueue /*: PromiseQueue<?BuildEvent>*/ = new PromiseQueue<?BuildEvent>({
maxConcurrent: 1,
});
#watchEvents /*: ValueEmitter<
| {|
+error: Error,
+buildEvent?: void,
|}
| {|
+buildEvent: BuildEvent,
+error?: void,
|},
> */;
#watcherSubscription /*: ?AsyncSubscription*/;
#watcherCount /*: number*/ = 0;
#requestedAssetIds /*: Set<string>*/ = new Set();
isProfiling /*: boolean */;
constructor(options: InitialParcelOptions) {
this.#initialOptions = options;
}
async _init(): Promise<void> {
if (this.#initialized) {
return;
}
await initSourcemaps;
await initRust?.();
let resolvedOptions: ParcelOptions = await resolveOptions(
this.#initialOptions,
);
this.#resolvedOptions = resolvedOptions;
let {config} = await loadParcelConfig(resolvedOptions);
this.#config = new ParcelConfig(config, resolvedOptions);
setFeatureFlags(resolvedOptions.featureFlags);
if (this.#initialOptions.workerFarm) {
if (this.#initialOptions.workerFarm.ending) {
throw new Error('Supplied WorkerFarm is ending');
}
this.#farm = this.#initialOptions.workerFarm;
} else {
this.#farm = createWorkerFarm({
shouldPatchConsole: resolvedOptions.shouldPatchConsole,
shouldTrace: resolvedOptions.shouldTrace,
});
}
await resolvedOptions.cache.ensure();
let {dispose: disposeOptions, ref: optionsRef} =
await this.#farm.createSharedReference(resolvedOptions, false);
this.#optionsRef = optionsRef;
this.#disposable = new Disposable();
if (this.#initialOptions.workerFarm) {
// If we don't own the farm, dispose of only these references when
// Parcel ends.
this.#disposable.add(disposeOptions);
} else {
// Otherwise, when shutting down, end the entire farm we created.
this.#disposable.add(() => this.#farm.end());
}
this.#watchEvents = new ValueEmitter();
this.#disposable.add(() => this.#watchEvents.dispose());
this.#reporterRunner = new ReporterRunner({
options: resolvedOptions,
reporters: await this.#config.getReporters(),
workerFarm: this.#farm,
});
this.#disposable.add(this.#reporterRunner);
logger.verbose({
origin: '@parcel/core',
message: 'Intializing request tracker...',
});
this.#requestTracker = await RequestTracker.init({
farm: this.#farm,
options: resolvedOptions,
});
this.#initialized = true;
}
async run(): Promise<BuildSuccessEvent> {
let startTime = Date.now();
if (!this.#initialized) {
await this._init();
}
let result = await this._build({startTime});
await this._end();
if (result.type === 'buildFailure') {
throw new BuildError(result.diagnostics);
}
return result;
}
async _end(): Promise<void> {
this.#initialized = false;
await this.#requestTracker.writeToCache();
await this.#disposable.dispose();
}
async _startNextBuild(): Promise<?BuildEvent> {
this.#watchAbortController = new AbortController();
await this.#farm.callAllWorkers('clearConfigCache', []);
try {
let buildEvent = await this._build({
signal: this.#watchAbortController.signal,
});
this.#watchEvents.emit({
buildEvent,
});
return buildEvent;
} catch (err) {
// Ignore BuildAbortErrors and only emit critical errors.
if (!(err instanceof BuildAbortError)) {
throw err;
}
}
}
async watch(
cb?: (err: ?Error, buildEvent?: BuildEvent) => mixed,
): Promise<AsyncSubscription> {
if (!this.#initialized) {
await this._init();
}
let watchEventsDisposable;
if (cb) {
watchEventsDisposable = this.#watchEvents.addListener(
({error, buildEvent}) => cb(error, buildEvent),
);
}
if (this.#watcherCount === 0) {
this.#watcherSubscription = await this._getWatcherSubscription();
await this.#reporterRunner.report({type: 'watchStart'});
// Kick off a first build, but don't await its results. Its results will
// be provided to the callback.
this.#watchQueue.add(() => this._startNextBuild());
this.#watchQueue.run();
}
this.#watcherCount++;
let unsubscribePromise;
const unsubscribe = async () => {
if (watchEventsDisposable) {
watchEventsDisposable.dispose();
}
this.#watcherCount--;
if (this.#watcherCount === 0) {
await nullthrows(this.#watcherSubscription).unsubscribe();
this.#watcherSubscription = null;
await this.#reporterRunner.report({type: 'watchEnd'});
this.#watchAbortController.abort();
await this.#watchQueue.run();
await this._end();
}
};
return {
unsubscribe() {
if (unsubscribePromise == null) {
unsubscribePromise = unsubscribe();
}
return unsubscribePromise;
},
};
}
async _build({
signal,
startTime = Date.now(),
}: {|
signal?: AbortSignal,
startTime?: number,
|} = {
/*::...null*/
}): Promise<BuildEvent> {
this.#requestTracker.setSignal(signal);
let options = nullthrows(this.#resolvedOptions);
try {
if (options.shouldProfile) {
await this.startProfiling();
}
if (options.shouldTrace) {
tracer.enable();
}
await this.#reporterRunner.report({
type: 'buildStart',
});
this.#requestTracker.graph.invalidateOnBuildNodes();
let request = createParcelBuildRequest({
optionsRef: this.#optionsRef,
requestedAssetIds: this.#requestedAssetIds,
signal,
});
let {bundleGraph, bundleInfo, changedAssets, assetRequests} =
await this.#requestTracker.runRequest(request, {force: true});
this.#requestedAssetIds.clear();
await dumpGraphToGraphViz(
// $FlowFixMe
this.#requestTracker.graph,
'RequestGraph',
requestGraphEdgeTypes,
);
let event = {
type: 'buildSuccess',
changedAssets: new Map(
Array.from(changedAssets).map(([id, asset]) => [
id,
assetFromValue(asset, options),
]),
),
bundleGraph: new BundleGraph<IPackagedBundle>(
bundleGraph,
(bundle, bundleGraph, options) =>
PackagedBundle.getWithInfo(
bundle,
bundleGraph,
options,
bundleInfo.get(bundle.id),
),
options,
),
buildTime: Date.now() - startTime,
requestBundle: async bundle => {
let bundleNode = bundleGraph._graph.getNodeByContentKey(bundle.id);
invariant(bundleNode?.type === 'bundle', 'Bundle does not exist');
if (!bundleNode.value.isPlaceholder) {
// Nothing to do.
return {
type: 'buildSuccess',
changedAssets: new Map(),
bundleGraph: event.bundleGraph,
buildTime: 0,
requestBundle: event.requestBundle,
unstable_requestStats: {},
};
}
for (let assetId of bundleNode.value.entryAssetIds) {
this.#requestedAssetIds.add(assetId);
}
if (this.#watchQueue.getNumWaiting() === 0) {
if (this.#watchAbortController) {
this.#watchAbortController.abort();
}
this.#watchQueue.add(() => this._startNextBuild());
}
let results = await this.#watchQueue.run();
let result = results.filter(Boolean).pop();
if (result.type === 'buildFailure') {
throw new BuildError(result.diagnostics);
}
return result;
},
unstable_requestStats: this.#requestTracker.flushStats(),
};
await this.#reporterRunner.report(event);
await this.#requestTracker.runRequest(
createValidationRequest({optionsRef: this.#optionsRef, assetRequests}),
{force: assetRequests.length > 0},
);
if (this.#reporterRunner.errors.length) {
throw this.#reporterRunner.errors;
}
return event;
} catch (e) {
if (e instanceof BuildAbortError) {
throw e;
}
let diagnostic = anyToDiagnostic(e);
let event = {
type: 'buildFailure',
diagnostics: Array.isArray(diagnostic) ? diagnostic : [diagnostic],
unstable_requestStats: this.#requestTracker.flushStats(),
};
await this.#reporterRunner.report(event);
return event;
} finally {
if (this.isProfiling) {
await this.stopProfiling();
}
await this.#farm.callAllWorkers('clearConfigCache', []);
}
}
async _getWatcherSubscription(): Promise<AsyncSubscription> {
invariant(this.#watcherSubscription == null);
let resolvedOptions = nullthrows(this.#resolvedOptions);
let opts = getWatcherOptions(resolvedOptions);
let sub = await resolvedOptions.inputFS.watch(
resolvedOptions.watchDir,
async (err, events) => {
if (err) {
logger.verbose({
message: `File watch event error occured`,
meta: {err},
});
this.#watchEvents.emit({error: err});
return;
}
logger.verbose({
message: `File watch event emitted with ${events.length} events. Sample event: [${events[0]?.type}] ${events[0]?.path}`,
});
let isInvalid = await this.#requestTracker.respondToFSEvents(
events,
Number.POSITIVE_INFINITY,
);
if (isInvalid && this.#watchQueue.getNumWaiting() === 0) {
if (this.#watchAbortController) {
this.#watchAbortController.abort();
}
this.#watchQueue.add(() => this._startNextBuild());
this.#watchQueue.run();
}
},
opts,
);
return {unsubscribe: () => sub.unsubscribe()};
}
// This is mainly for integration tests and it not public api!
_getResolvedParcelOptions(): ParcelOptions {
return nullthrows(
this.#resolvedOptions,
'Resolved options is null, please let parcel initialize before accessing this.',
);
}
async startProfiling(): Promise<void> {
if (this.isProfiling) {
throw new Error('Parcel is already profiling');
}
logger.info({origin: '@parcel/core', message: 'Starting profiling...'});
this.isProfiling = true;
await this.#farm.startProfile();
}
stopProfiling(): Promise<void> {
if (!this.isProfiling) {
throw new Error('Parcel is not profiling');
}
logger.info({origin: '@parcel/core', message: 'Stopping profiling...'});
this.isProfiling = false;
return this.#farm.endProfile();
}
takeHeapSnapshot(): Promise<void> {
logger.info({origin: '@parcel/core', message: 'Taking heap snapshot...'});
return this.#farm.takeHeapSnapshot();
}
async unstable_transform(
options: ParcelTransformOptions,
): Promise<Array<Asset>> {
if (!this.#initialized) {
await this._init();
}
let projectRoot = nullthrows(this.#resolvedOptions).projectRoot;
let request = createAssetRequest({
...options,
filePath: toProjectPath(projectRoot, options.filePath),
optionsRef: this.#optionsRef,
env: createEnvironment({
...options.env,
loc:
options.env?.loc != null
? {
...options.env.loc,
filePath: toProjectPath(projectRoot, options.env.loc.filePath),
}
: undefined,
}),
});
let res = await this.#requestTracker.runRequest(request, {
force: true,
});
return res.map(asset =>
assetFromValue(asset, nullthrows(this.#resolvedOptions)),
);
}
async unstable_resolve(
request: ParcelResolveOptions,
): Promise<?ParcelResolveResult> {
if (!this.#initialized) {
await this._init();
}
let projectRoot = nullthrows(this.#resolvedOptions).projectRoot;
if (request.resolveFrom == null && path.isAbsolute(request.specifier)) {
request.specifier = fromProjectPathRelative(
toProjectPath(projectRoot, request.specifier),
);
}
let dependency = createDependency(projectRoot, {
...request,
env: createEnvironment({
...request.env,
loc:
request.env?.loc != null
? {
...request.env.loc,
filePath: toProjectPath(projectRoot, request.env.loc.filePath),
}
: undefined,
}),
});
let req = createPathRequest({
dependency,
name: request.specifier,
});
let res = await this.#requestTracker.runRequest(req, {
force: true,
});
if (!res) {
return null;
}
return {
filePath: fromProjectPath(projectRoot, res.filePath),
code: res.code,
query: res.query,
sideEffects: res.sideEffects,
};
}
}
export class BuildError extends ThrowableDiagnostic {
constructor(diagnostic: Array<Diagnostic> | Diagnostic) {
super({diagnostic});
this.name = 'BuildError';
}
}
export function createWorkerFarm(
options: $Shape<FarmOptions> = {},
): WorkerFarm {
return new WorkerFarm({
...options,
// $FlowFixMe
workerPath: process.browser
? '@parcel/core/src/worker.js'
: require.resolve('./worker'),
});
}