From cca17c1b7fcad5d82ae2d97aed64ff6704ba06ca Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:02:55 -0800 Subject: [PATCH 1/3] Use ts-go for building our extensions Also reverts to use experimental decorators due to stage 3 decorators not being supported yet https://github.com/microsoft/typescript-go/issues/2354 The skipLib check is needed to work around some jsdom types issues --- build/gulpfile.extensions.ts | 44 ++++---- build/lib/tsgo.ts | 115 ++++++++++++++++++++ extensions/git-base/src/decorators.ts | 8 +- extensions/git/src/api/extension.ts | 10 +- extensions/git/src/commands.ts | 9 +- extensions/git/src/decorators.ts | 13 ++- extensions/github/src/util.ts | 11 +- extensions/notebook-renderers/tsconfig.json | 3 +- extensions/tsconfig.base.json | 3 +- 9 files changed, 168 insertions(+), 48 deletions(-) create mode 100644 build/lib/tsgo.ts diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index 6f5cf0d25d8..fba395e7bbc 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -7,21 +7,22 @@ import { EventEmitter } from 'events'; EventEmitter.defaultMaxListeners = 100; +import es from 'event-stream'; +import glob from 'glob'; import gulp from 'gulp'; +import filter from 'gulp-filter'; +import plumber from 'gulp-plumber'; +import sourcemaps from 'gulp-sourcemaps'; import * as path from 'path'; import * as nodeUtil from 'util'; -import es from 'event-stream'; -import filter from 'gulp-filter'; -import * as util from './lib/util.ts'; -import { getVersion } from './lib/getVersion.ts'; -import * as task from './lib/task.ts'; -import watcher from './lib/watch/index.ts'; -import { createReporter } from './lib/reporter.ts'; -import glob from 'glob'; -import plumber from 'gulp-plumber'; import * as ext from './lib/extensions.ts'; +import { getVersion } from './lib/getVersion.ts'; +import { createReporter } from './lib/reporter.ts'; +import * as task from './lib/task.ts'; import * as tsb from './lib/tsb/index.ts'; -import sourcemaps from 'gulp-sourcemaps'; +import { createTsgoStream, spawnTsgo } from './lib/tsgo.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); @@ -150,25 +151,22 @@ const tasks = compilations.map(function (tsconfigFile) { .pipe(gulp.dest(out)); })); - const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, () => { - const pipeline = createPipeline(false, true); - const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); - const input = es.merge(nonts, pipeline.tsProjectSrc()); + const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, async () => { + const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true })); + const copyNonTs = util.streamToPromise(nonts.pipe(gulp.dest(out))); + const tsgo = spawnTsgo(absolutePath); - return input - .pipe(pipeline()) - .pipe(gulp.dest(out)); + await Promise.all([copyNonTs, tsgo]); })); const watchTask = task.define(`watch-extension:${name}`, task.series(cleanTask, () => { - const pipeline = createPipeline(false); - const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); - const input = es.merge(nonts, pipeline.tsProjectSrc()); + const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true })); const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } }); + const watchNonTs = watchInput.pipe(filter(['**', '!**/*.ts'], { dot: true })).pipe(gulp.dest(out)); + const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath), 200)); + const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream); - return watchInput - .pipe(util.incremental(pipeline, input)) - .pipe(gulp.dest(out)); + return watchStream; })); // Tasks diff --git a/build/lib/tsgo.ts b/build/lib/tsgo.ts new file mode 100644 index 00000000000..f11b823a268 --- /dev/null +++ b/build/lib/tsgo.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import es from 'event-stream'; +import * as path from 'path'; +import { createReporter } from './reporter.ts'; + +const root = path.dirname(path.dirname(import.meta.dirname)); +const tsgoPath = path.join(root, 'node_modules', '.bin', 'tsgo'); +const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; + +export function spawnTsgo(projectPath: string): Promise { + const reporter = createReporter('extensions'); + let report: NodeJS.ReadWriteStream | undefined; + + const beginReport = (emitError: boolean) => { + if (report) { + report.end(); + } + report = reporter.end(emitError); + }; + + const endReport = () => { + if (!report) { + return; + } + report.end(); + report = undefined; + }; + + const args = [tsgoPath, '--project', projectPath, '--pretty', 'false']; + + beginReport(false); + + const child = cp.spawn(process.execPath, args, { + cwd: root, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let buffer = ''; + const handleLine = (line: string) => { + const trimmed = line.replace(ansiRegex, '').trim(); + if (!trimmed) { + return; + } + if (/Starting compilation|File change detected/i.test(trimmed)) { + beginReport(false); + return; + } + if (/Compilation complete/i.test(trimmed)) { + endReport(); + return; + } + + const match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(trimmed); + + if (match) { + const fullpath = path.isAbsolute(match[1]) ? match[1] : path.join(root, match[1]); + const message = match[3]; + reporter(fullpath + message); + } else { + reporter(trimmed); + } + }; + + const handleData = (data: Buffer) => { + buffer += data.toString('utf8'); + const lines = buffer.split(/\r?\n/); + buffer = lines.pop() ?? ''; + for (const line of lines) { + handleLine(line); + } + }; + + child.stdout?.on('data', handleData); + child.stderr?.on('data', handleData); + + const done = new Promise((resolve, reject) => { + child.on('exit', code => { + if (buffer.trim()) { + handleLine(buffer); + buffer = ''; + } + endReport(); + if (code === 0) { + resolve(); + return; + } + reject(new Error(`tsgo exited with code ${code ?? 'unknown'}`)); + }); + child.on('error', err => { + endReport(); + reject(err); + }); + }); + + return done; +} + +export function createTsgoStream(projectPath: string): NodeJS.ReadWriteStream { + const stream = es.through(); + + spawnTsgo(projectPath).then(() => { + stream.emit('end'); + }).catch(() => { + // Errors are already reported by spawnTsgo via the reporter. + // Don't emit 'error' on the stream as that would exit the watch process. + stream.emit('end'); + }); + + return stream; +} diff --git a/extensions/git-base/src/decorators.ts b/extensions/git-base/src/decorators.ts index 067d32cdb5f..23f8dfd357f 100644 --- a/extensions/git-base/src/decorators.ts +++ b/extensions/git-base/src/decorators.ts @@ -47,11 +47,11 @@ function _throttle(fn: Function, key: string): Function { return trigger; } -function decorate(decorator: (fn: Function, key: string) => Function): Function { - return function (original: any, context: ClassMethodDecoratorContext) { - if (context.kind !== 'method') { +function decorate(decorator: (fn: Function, key: string) => Function): MethodDecorator { + return (_target: any, key: string | symbol, descriptor: PropertyDescriptor): void => { + if (typeof descriptor.value !== 'function') { throw new Error('not supported'); } - return decorator(original, context.name.toString()); + descriptor.value = decorator(descriptor.value, String(key)); }; } diff --git a/extensions/git/src/api/extension.ts b/extensions/git/src/api/extension.ts index a716fa00dae..7b0313b6c26 100644 --- a/extensions/git/src/api/extension.ts +++ b/extensions/git/src/api/extension.ts @@ -9,14 +9,14 @@ import { ApiRepository, ApiImpl } from './api1'; import { Event, EventEmitter } from 'vscode'; import { CloneManager } from '../cloneManager'; -function deprecated(original: unknown, context: ClassMemberDecoratorContext) { - if (typeof original !== 'function' || context.kind !== 'method') { +function deprecated(_target: unknown, key: string | symbol, descriptor: PropertyDescriptor): void { + if (typeof descriptor.value !== 'function') { throw new Error('not supported'); } - const key = context.name.toString(); - return function (this: unknown, ...args: unknown[]) { - console.warn(`Git extension API method '${key}' is deprecated.`); + const original = descriptor.value; + descriptor.value = function (this: unknown, ...args: unknown[]) { + console.warn(`Git extension API method '${String(key)}' is deprecated.`); return original.apply(this, args); }; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b3e05e0016b..4451a5e4620 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -372,13 +372,12 @@ interface ScmCommand { const Commands: ScmCommand[] = []; -function command(commandId: string, options: ScmCommandOptions = {}): Function { - return (value: unknown, context: ClassMethodDecoratorContext) => { - if (typeof value !== 'function' || context.kind !== 'method') { +function command(commandId: string, options: ScmCommandOptions = {}): MethodDecorator { + return (_target: any, key: string | symbol, descriptor: PropertyDescriptor): void => { + if (typeof descriptor.value !== 'function') { throw new Error('not supported'); } - const key = context.name.toString(); - Commands.push({ commandId, key, method: value, options }); + Commands.push({ commandId, key: String(key), method: descriptor.value, options }); }; } diff --git a/extensions/git/src/decorators.ts b/extensions/git/src/decorators.ts index 0e59a849ed2..3aa7d5dc557 100644 --- a/extensions/git/src/decorators.ts +++ b/extensions/git/src/decorators.ts @@ -6,11 +6,14 @@ import { done } from './util'; function decorate(decorator: (fn: Function, key: string) => Function): Function { - return function (original: unknown, context: ClassMethodDecoratorContext) { - if (typeof original === 'function' && (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter')) { - return decorator(original, context.name.toString()); + return (_target: any, key: string, descriptor: PropertyDescriptor): void => { + if (typeof descriptor.value === 'function') { + descriptor.value = decorator(descriptor.value, key); + } else if (typeof descriptor.get === 'function') { + descriptor.get = decorator(descriptor.get, key) as () => any; + } else { + throw new Error('not supported'); } - throw new Error('not supported'); }; } @@ -85,5 +88,5 @@ export function debounce(delay: number): Function { clearTimeout(this[timerKey]); this[timerKey] = setTimeout(() => fn.apply(this, args), delay); }; - }); + }) as MethodDecorator; } diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index f7f54ec5f3f..2247292dd93 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -24,11 +24,14 @@ export class DisposableStore { } function decorate(decorator: (fn: Function, key: string) => Function): Function { - return function (original: any, context: ClassMethodDecoratorContext) { - if (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter') { - return decorator(original, context.name.toString()); + return (_target: any, key: string, descriptor: PropertyDescriptor): void => { + if (typeof descriptor.value === 'function') { + descriptor.value = decorator(descriptor.value, key); + } else if (typeof descriptor.get === 'function') { + descriptor.get = decorator(descriptor.get, key) as () => any; + } else { + throw new Error('not supported'); } - throw new Error('not supported'); }; } diff --git a/extensions/notebook-renderers/tsconfig.json b/extensions/notebook-renderers/tsconfig.json index 0bc7baa21be..0c1d35312f7 100644 --- a/extensions/notebook-renderers/tsconfig.json +++ b/extensions/notebook-renderers/tsconfig.json @@ -12,7 +12,8 @@ ], "typeRoots": [ "./node_modules/@types" - ] + ], + "skipLibCheck": true }, "include": [ "src/**/*", diff --git a/extensions/tsconfig.base.json b/extensions/tsconfig.base.json index 9d939dd568a..db32799b3ba 100644 --- a/extensions/tsconfig.base.json +++ b/extensions/tsconfig.base.json @@ -15,6 +15,7 @@ "noImplicitOverride": true, "noUnusedLocals": true, "noUnusedParameters": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true } } From 1b46833855a89a34213a1c48e748293f429240bd Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:38:29 -0800 Subject: [PATCH 2/3] Use npx --- build/lib/tsgo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/lib/tsgo.ts b/build/lib/tsgo.ts index f11b823a268..04173c05fdb 100644 --- a/build/lib/tsgo.ts +++ b/build/lib/tsgo.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import { createReporter } from './reporter.ts'; const root = path.dirname(path.dirname(import.meta.dirname)); -const tsgoPath = path.join(root, 'node_modules', '.bin', 'tsgo'); +const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'; const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; export function spawnTsgo(projectPath: string): Promise { @@ -31,11 +31,11 @@ export function spawnTsgo(projectPath: string): Promise { report = undefined; }; - const args = [tsgoPath, '--project', projectPath, '--pretty', 'false']; + const args = ['tsgo', '--project', projectPath, '--pretty', 'false']; beginReport(false); - const child = cp.spawn(process.execPath, args, { + const child = cp.spawn(npx, args, { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] }); From 9e3e80595036c56c449517dce6926428f767ae1a Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:42:48 -0800 Subject: [PATCH 3/3] Fix for windows --- build/lib/tsgo.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/lib/tsgo.ts b/build/lib/tsgo.ts index 04173c05fdb..3e709ef818b 100644 --- a/build/lib/tsgo.ts +++ b/build/lib/tsgo.ts @@ -37,7 +37,8 @@ export function spawnTsgo(projectPath: string): Promise { const child = cp.spawn(npx, args, { cwd: root, - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['ignore', 'pipe', 'pipe'], + shell: true }); let buffer = '';