Files
vscode/build/gulpfile.extensions.ts
T
roblourensandCopilot 573eff8eb4 [Perf] build: reduce local validation costs (#339968)
* build: reduce local validation costs (Written by Copilot)

Add client-only incremental builds, transpile extension JavaScript with correct module and output contracts, and reuse matching Electron runtimes for test startup. Update validation guidance and cover cold/warm state transitions and runtime loading.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: validate cold fast builds in CI (Written by Copilot)

Temporarily exercise cold build-fast and shipping extension activation on Windows and Linux before the existing Electron unit suites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: seed cold CI activation profile (Written by Copilot)

Use the existing scenario harness profile migration setting so built-in Copilot participates in exact shipping-extension activation validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: remove temporary CI validation (Written by Copilot)

Windows x64 and Linux x64 both passed cold default build-fast and exact shipping-extension activation, language-service, and Markdown preview checks in Code OSS run 37404189501. Restore normal CI wiring and retain the validation evidence in the PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build: repair caches and transpile modules (Written by Copilot)

Treat installed Electron runtime filesystem failures as cache misses while preserving repository configuration errors. Route explicit TypeScript module suffixes through extension transpilation and exclude sources and declarations from asset copying. Exercise the actual gulp task on an isolated fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-06 11:11:35 -07:00

342 lines
13 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Increase max listeners for event emitters
import { EventEmitter } from 'events';
EventEmitter.defaultMaxListeners = 100;
import es from 'event-stream';
import fancyLog from 'fancy-log';
import * as fs from 'fs';
import glob from 'glob';
import { gulp, filter, plumber, sourcemaps } from './lib/gulp/facade.ts';
import * as path from 'path';
import * as nodeUtil from 'util';
import * as ext from './lib/extensions.ts';
import { getVersion } from './lib/getVersion.ts';
import { createReporter } from './lib/reporter.ts';
import * as task from './lib/gulp/task.ts';
import * as tsb from './lib/tsb/index.ts';
import { createTsgoStream, spawnTsgo } from './lib/tsgo.ts';
import { getCompilerOptionsFromTsConfig } from './lib/tsconfigUtils.ts';
import * as util from './lib/util.ts';
import watcher from './lib/watch/index.ts';
const root = path.dirname(import.meta.dirname);
const commit = getVersion(root);
// Tracks active extension compilations to emit aggregate
// "Starting compilation" / "Finished compilation" messages
// that the problem matcher in tasks.json relies on.
let activeExtensionCompilations = 0;
function onExtensionCompilationStart(): void {
if (activeExtensionCompilations === 0) {
fancyLog('Starting compilation');
}
activeExtensionCompilations++;
}
function onExtensionCompilationEnd(): void {
activeExtensionCompilations--;
if (activeExtensionCompilations === 0) {
fancyLog('Finished compilation');
}
}
// To save 250ms for each gulp startup, we are caching the result here
// const compilations = glob.sync('**/tsconfig.json', {
// cwd: extensionsPath,
// ignore: ['**/out/**', '**/node_modules/**']
// });
const compilations = [
'extensions/configuration-editing/tsconfig.json',
'extensions/css-language-features/client/tsconfig.json',
'extensions/css-language-features/server/tsconfig.json',
'extensions/debug-auto-launch/tsconfig.json',
'extensions/debug-server-ready/tsconfig.json',
'extensions/emmet/tsconfig.json',
'extensions/extension-editing/tsconfig.json',
'extensions/git/tsconfig.json',
'extensions/git-base/tsconfig.json',
'extensions/github/tsconfig.json',
'extensions/github-authentication/tsconfig.json',
'extensions/grunt/tsconfig.json',
'extensions/gulp/tsconfig.json',
'extensions/html-language-features/client/tsconfig.json',
'extensions/html-language-features/server/tsconfig.json',
'extensions/ipynb/tsconfig.json',
'extensions/jake/tsconfig.json',
'extensions/json-language-features/client/tsconfig.json',
'extensions/json-language-features/server/tsconfig.json',
'extensions/markdown-language-features/tsconfig.json',
'extensions/markdown-math/tsconfig.json',
'extensions/media-preview/tsconfig.json',
'extensions/merge-conflict/tsconfig.json',
'extensions/mermaid-markdown-features/tsconfig.json',
'extensions/terminal-suggest/tsconfig.json',
'extensions/microsoft-authentication/tsconfig.json',
'extensions/notebook-renderers/tsconfig.json',
'extensions/npm/tsconfig.json',
'extensions/php-language-features/tsconfig.json',
'extensions/references-view/tsconfig.json',
'extensions/search-result/tsconfig.json',
'extensions/simple-browser/tsconfig.json',
'extensions/tunnel-forwarding/tsconfig.json',
'extensions/typescript-language-features/web/tsconfig.json',
'extensions/typescript-language-features/tsconfig.json',
'extensions/vscode-api-tests/tsconfig.json',
'extensions/vscode-colorize-tests/tsconfig.json',
'extensions/vscode-colorize-perf-tests/tsconfig.json',
'extensions/vscode-test-resolver/tsconfig.json',
'.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json',
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
'.vscode/extensions/vscode-extras/tsconfig.json',
'.vscode/extensions/vscode-pr-pinger/tsconfig.json',
];
const getBaseUrl = (out: string) => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`;
function rewriteTsgoSourceMappingUrlsIfNeeded(build: boolean, out: string, baseUrl: string): Promise<void> {
if (!build) {
return Promise.resolve();
}
return util.streamToPromise(
gulp.src(path.join(out, '**', '*.js'), { base: out })
.pipe(util.rewriteSourceMappingURL(baseUrl))
.pipe(gulp.dest(out))
);
}
export function createExtensionTasks(tsconfigFile: string, repoRoot = root) {
const absolutePath = path.join(repoRoot, tsconfigFile);
const relativeDirname = path.dirname(tsconfigFile.replace(/^(.*\/)?extensions\//i, ''));
const overrideOptions: { sourceMap?: boolean; inlineSources?: boolean; base?: string } = {};
overrideOptions.sourceMap = true;
const name = relativeDirname.replace(/\//g, '-');
const srcRoot = path.dirname(tsconfigFile);
const srcBase = path.join(repoRoot, srcRoot, 'src');
const src = path.join(srcBase, '**');
const srcOpts = { cwd: repoRoot, base: srcBase, dot: true };
const out = getCompilerOptionsFromTsConfig(absolutePath).outDir ?? path.join(repoRoot, srcRoot, 'out');
const baseUrl = getBaseUrl(path.relative(repoRoot, out));
function createPipeline(build: boolean, emitError?: boolean, transpileOnly?: boolean) {
const reporter = createReporter('extensions');
overrideOptions.inlineSources = Boolean(build);
overrideOptions.base = path.dirname(absolutePath);
const compilation = tsb.create(absolutePath, overrideOptions, { verbose: false, transpileOnly, transpileWithEsbuild: true }, err => reporter(err.toString()));
const pipeline = function () {
const input = es.through();
const tsFilter = filter(['**/*.{ts,mts,cts}', '!**/lib/lib*.d.{ts,mts,cts}', '!**/node_modules/**'], { restore: true, dot: true });
const output = input
.pipe(plumber({
errorHandler: function (err) {
if (err && !err.__reporter__) {
reporter(err);
}
}
}))
.pipe(tsFilter)
.pipe(util.loadSourcemaps())
.pipe(compilation())
.pipe(build ? util.stripSourceMappingURL() : es.through())
.pipe(sourcemaps.write('.', {
sourceMappingURL: !build ? undefined : f => `${baseUrl}/${f.relative}.map`,
addComment: !!build,
includeContent: !!build,
// note: trailing slash is important, else the source URLs in V8's file coverage are incorrect
sourceRoot: '../src/',
}))
.pipe(tsFilter.restore)
.pipe(reporter.end(!!emitError));
return es.duplex(input, output);
};
// add src-stream for project files
pipeline.tsProjectSrc = () => {
return compilation.src(srcOpts);
};
return pipeline;
}
const tsBuildInfoFile = path.join(path.dirname(absolutePath), path.basename(absolutePath, '.json') + '.tsbuildinfo');
const cleanTask = task.define(`clean-extension-${name}`, async () => {
await util.rimraf(out)();
fs.rmSync(tsBuildInfoFile, { force: true });
});
const transpileTask = task.define(`transpile-extension:${name}`, task.series(cleanTask, () => {
const pipeline = createPipeline(false, true, true);
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.{ts,mts,cts}'], { dot: true }));
const input = es.merge(nonts, pipeline.tsProjectSrc());
return input
.pipe(pipeline())
.pipe(gulp.dest(out));
}));
const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, async () => {
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.{ts,mts,cts}'], { dot: true }));
const copyNonTs = util.streamToPromise(nonts.pipe(gulp.dest(out)));
const tsgo = spawnTsgo(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl));
await Promise.all([copyNonTs, tsgo]);
}));
const watchTask = task.define(`watch-extension:${name}`, task.series(cleanTask, () => {
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.{ts,mts,cts}'], { dot: true }));
const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } });
const watchNonTs = watchInput.pipe(filter(['**', '!**/*.{ts,mts,cts}'], { dot: true })).pipe(gulp.dest(out));
const tsgoStream = watchInput.pipe(util.debounce(() => {
onExtensionCompilationStart();
const stream = createTsgoStream(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl));
// Wrap in a result stream that always emits 'end' (even on
// error) so the debounce resets to idle and can process future
// file changes. Errors from tsgo (e.g. type errors causing a
// non-zero exit code) are already reported by spawnTsgo's
// runReporter, so swallowing the stream error is safe.
const result = es.through();
stream.on('end', () => {
onExtensionCompilationEnd();
result.emit('end');
});
stream.on('error', () => {
onExtensionCompilationEnd();
result.emit('end');
});
return result;
}, 200));
const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream);
return watchStream;
}));
// Tasks
task.task(transpileTask);
task.task(compileTask);
task.task(watchTask);
return { transpileTask, compileTask, watchTask };
}
const tasks = compilations.map(tsconfigFile => createExtensionTasks(tsconfigFile));
const transpileExtensionsTask = task.define('transpile-extensions', task.parallel(...tasks.map(t => t.transpileTask)));
task.task(transpileExtensionsTask);
export const compileExtensionsTask = task.define('compile-extensions', task.parallel(...tasks.map(t => t.compileTask)));
task.task(compileExtensionsTask);
export const watchExtensionsTask = task.define('watch-extensions', task.parallel(...tasks.map(t => t.watchTask)));
task.task(watchExtensionsTask);
//#region Extension media
export const compileExtensionMediaTask = task.define('compile-extension-media', () => ext.buildExtensionMedia(false));
task.task(compileExtensionMediaTask);
export const watchExtensionMedia = task.define('watch-extension-media', () => ext.buildExtensionMedia(true));
task.task(watchExtensionMedia);
export const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => ext.buildExtensionMedia(false, '.build/extensions'));
task.task(compileExtensionMediaBuildTask);
//#endregion
//#region Azure Pipelines
/**
* Cleans the build directory for extensions
*/
export const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions'));
/**
* brings in the marketplace extensions for the build
*/
const bundleMarketplaceExtensionsBuildTask = task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build')));
/**
* Compiles the non-native extensions for the build
* @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that.
*/
export const compileNonNativeExtensionsBuildTask = task.define('compile-non-native-extensions-build', task.series(
bundleMarketplaceExtensionsBuildTask,
task.define('bundle-non-native-extensions-build', () => ext.packageNonNativeLocalExtensionsStream(false).pipe(gulp.dest('.build')))
));
task.task(compileNonNativeExtensionsBuildTask);
/**
* Compiles the native extensions for the build
* @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that.
*/
export const compileNativeExtensionsBuildTask = task.define('compile-native-extensions-build', () => ext.packageNativeLocalExtensionsStream(false).pipe(gulp.dest('.build')));
task.task(compileNativeExtensionsBuildTask);
/**
* Compiles the built-in copilot extension for the build.
* Used by non-CI local builds where copilot is not downloaded as a VSIX.
*/
export const compileCopilotExtensionBuildTask = task.define('compile-copilot-extension-build', () => ext.packageCopilotExtensionStream().pipe(gulp.dest('.build')));
task.task(compileCopilotExtensionBuildTask);
/**
* Compiles the extensions for the build.
* This is essentially a helper task that combines {@link cleanExtensionsBuildTask}, {@link compileNonNativeExtensionsBuildTask} and {@link compileNativeExtensionsBuildTask}
*/
export const compileAllExtensionsBuildTask = task.define('compile-extensions-build', task.series(
cleanExtensionsBuildTask,
bundleMarketplaceExtensionsBuildTask,
task.define('bundle-extensions-build', () => ext.packageAllLocalExtensionsStream(false).pipe(gulp.dest('.build'))),
));
task.task(compileAllExtensionsBuildTask);
//#endregion
export const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false));
task.task(compileWebExtensionsTask);
export const watchWebExtensionsTask = task.define('watch-web', () => buildWebExtensions(true));
task.task(watchWebExtensionsTask);
async function buildWebExtensions(isWatch: boolean): Promise<void> {
const extensionsPath = path.join(root, 'extensions');
// Find all esbuild.browser.mts files
const esbuildConfigLocations = await nodeUtil.promisify(glob)(
path.join(extensionsPath, '**', 'esbuild.browser.mts'),
{ ignore: ['**/node_modules'] }
);
const promises: Promise<unknown>[] = [];
// Esbuild for extensions
if (esbuildConfigLocations.length > 0) {
promises.push(
ext.esbuildExtensions('packaging web extension (esbuild)', isWatch, esbuildConfigLocations.map(script => ({ script }))),
// Also run type check on extensions
...esbuildConfigLocations.flatMap(script => {
const roots = ext.getBuildRootsForExtension(path.dirname(script));
return roots.map(root => ext.typeCheckExtension(root, true));
})
);
}
await Promise.all(promises);
}