mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-04 01:06:22 +01:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 999f638b-185c-4f4c-8d5c-1e21c9fe71e5
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
import { stat } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import esbuild from 'esbuild';
|
|
|
|
export interface RunConfig {
|
|
readonly srcDir: string;
|
|
readonly outdir: string;
|
|
readonly entryPoints: esbuild.BuildOptions['entryPoints'];
|
|
readonly additionalOptions?: Partial<esbuild.BuildOptions>;
|
|
readonly additionalWatchPaths?: readonly string[];
|
|
readonly beforeBuild?: () => Promise<unknown> | unknown;
|
|
}
|
|
|
|
// `esbuild.stop()` shuts down the single esbuild service shared by all concurrent builds, so we
|
|
// must only call it once no builds are in flight. Otherwise a finishing build would tear down the
|
|
// service while a sibling build (e.g. running in the same `Promise.all`) is still using it.
|
|
let pendingBuilds = 0;
|
|
|
|
async function buildOnce(
|
|
options: esbuild.BuildOptions,
|
|
beforeBuild?: () => Promise<unknown> | unknown,
|
|
): Promise<esbuild.BuildResult> {
|
|
pendingBuilds++;
|
|
try {
|
|
await beforeBuild?.();
|
|
return await esbuild.build(options);
|
|
} finally {
|
|
if (--pendingBuilds === 0) {
|
|
esbuild.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shared build/watch runner for extension esbuild scripts.
|
|
*/
|
|
export async function runBuild(
|
|
config: RunConfig,
|
|
baseOptions: esbuild.BuildOptions,
|
|
args: string[],
|
|
didBuild?: (outDir: string) => unknown,
|
|
): Promise<void> {
|
|
let outdir = config.outdir;
|
|
const outputRootIndex = args.indexOf('--outputRoot');
|
|
if (outputRootIndex >= 0) {
|
|
const outputRoot = args[outputRootIndex + 1];
|
|
const outputDirName = path.basename(outdir);
|
|
outdir = path.join(outputRoot, outputDirName);
|
|
}
|
|
|
|
const resolvedOptions: esbuild.BuildOptions = {
|
|
...baseOptions,
|
|
entryPoints: config.entryPoints,
|
|
outdir,
|
|
...(config.additionalOptions || {}),
|
|
};
|
|
|
|
const isWatch = args.indexOf('--watch') >= 0;
|
|
if (isWatch) {
|
|
await watchWithParcel(
|
|
resolvedOptions,
|
|
config.srcDir,
|
|
config.additionalWatchPaths ?? [],
|
|
config.beforeBuild,
|
|
() => didBuild?.(outdir),
|
|
);
|
|
} else {
|
|
try {
|
|
await buildOnce(resolvedOptions, config.beforeBuild);
|
|
await didBuild?.(outdir);
|
|
} catch {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We use @parcel/watcher as it has much lower cpu usage when idle compared to esbuild's watch mode
|
|
async function watchWithParcel(
|
|
options: esbuild.BuildOptions,
|
|
srcDir: string,
|
|
additionalWatchPaths: readonly string[],
|
|
beforeBuild?: () => Promise<unknown> | unknown,
|
|
didBuild?: () => Promise<unknown> | unknown,
|
|
): Promise<void> {
|
|
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
const rebuild = () => {
|
|
if (debounce) {
|
|
clearTimeout(debounce);
|
|
}
|
|
debounce = setTimeout(async () => {
|
|
try {
|
|
// Also instead of retaining the esbuild context, we are re-running the entire build on each change.
|
|
// This reduces memory usage since most projects don't need to be re-built often.
|
|
const result = await buildOnce(options, beforeBuild);
|
|
if (result.errors.length === 0) {
|
|
await didBuild?.();
|
|
}
|
|
} catch (error) {
|
|
console.error('[watch] build error:', error);
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
const watcher = await import('@parcel/watcher');
|
|
const ignoredOutputPaths: string[] = [];
|
|
if (options.outdir) {
|
|
const outdirGlob = options.outdir.replace(/\\/g, '/').replace(/\/$/, '');
|
|
ignoredOutputPaths.push(outdirGlob, `${outdirGlob}/**`);
|
|
}
|
|
|
|
const subscribe = async (watchPath: string, ignore: readonly string[]) => {
|
|
const watchPathStat = await stat(watchPath);
|
|
const watchedFile = watchPathStat.isDirectory() ? undefined : path.resolve(watchPath);
|
|
const watchRoot = watchedFile ? path.dirname(watchedFile) : watchPath;
|
|
return watcher.subscribe(watchRoot, (error, events) => {
|
|
if (error) {
|
|
console.error('[watch] watcher error:', error);
|
|
return;
|
|
}
|
|
if (!watchedFile || events.some(event => path.resolve(event.path) === watchedFile)) {
|
|
rebuild();
|
|
}
|
|
}, {
|
|
ignore: [...ignore],
|
|
});
|
|
};
|
|
await Promise.all([
|
|
subscribe(srcDir, ['**/node_modules/**', '**/dist/**', '**/out/**', ...ignoredOutputPaths]),
|
|
...additionalWatchPaths.map(watchPath => subscribe(watchPath, ignoredOutputPaths)),
|
|
]);
|
|
rebuild();
|
|
}
|