Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/tsb/transpiler.ts
4772 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import esbuild from 'esbuild';
7
import ts from 'typescript';
8
import threads from 'node:worker_threads';
9
import Vinyl from 'vinyl';
10
import { cpus } from 'node:os';
11
import { getTargetStringFromTsConfig } from '../tsconfigUtils.ts';
12
13
interface TranspileReq {
14
readonly tsSrcs: string[];
15
readonly options: ts.TranspileOptions;
16
}
17
18
interface TranspileRes {
19
readonly jsSrcs: string[];
20
readonly diagnostics: ts.Diagnostic[][];
21
}
22
23
function transpile(tsSrc: string, options: ts.TranspileOptions): { jsSrc: string; diag: ts.Diagnostic[] } {
24
25
const isAmd = /\n(import|export)/m.test(tsSrc);
26
if (!isAmd && options.compilerOptions?.module === ts.ModuleKind.AMD) {
27
// enforce NONE module-system for not-amd cases
28
options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: ts.ModuleKind.None } } };
29
}
30
const out = ts.transpileModule(tsSrc, options);
31
return {
32
jsSrc: out.outputText,
33
diag: out.diagnostics ?? []
34
};
35
}
36
37
if (!threads.isMainThread) {
38
// WORKER
39
threads.parentPort?.addListener('message', (req: TranspileReq) => {
40
const res: TranspileRes = {
41
jsSrcs: [],
42
diagnostics: []
43
};
44
for (const tsSrc of req.tsSrcs) {
45
const out = transpile(tsSrc, req.options);
46
res.jsSrcs.push(out.jsSrc);
47
res.diagnostics.push(out.diag);
48
}
49
threads.parentPort!.postMessage(res);
50
});
51
}
52
53
class OutputFileNameOracle {
54
55
readonly getOutputFileName: (name: string) => string;
56
57
constructor(cmdLine: ts.ParsedCommandLine, configFilePath: string) {
58
// very complicated logic to re-use TS internal functions to know the output path
59
// given a TS input path and its config
60
type InternalTsApi = typeof ts & {
61
normalizePath(path: string): string;
62
getOutputFileNames(commandLine: ts.ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[];
63
};
64
this.getOutputFileName = (file) => {
65
try {
66
67
// windows: path-sep normalizing
68
file = (ts as InternalTsApi).normalizePath(file);
69
70
if (!cmdLine.options.configFilePath) {
71
// this is needed for the INTERNAL getOutputFileNames-call below...
72
cmdLine.options.configFilePath = configFilePath;
73
}
74
const isDts = file.endsWith('.d.ts');
75
if (isDts) {
76
file = file.slice(0, -5) + '.ts';
77
cmdLine.fileNames.push(file);
78
}
79
const outfile = (ts as InternalTsApi).getOutputFileNames(cmdLine, file, true)[0];
80
if (isDts) {
81
cmdLine.fileNames.pop();
82
}
83
return outfile;
84
85
} catch (err) {
86
console.error(file, cmdLine.fileNames);
87
console.error(err);
88
throw err;
89
}
90
};
91
}
92
}
93
94
class TranspileWorker {
95
96
private static pool = 1;
97
98
readonly id = TranspileWorker.pool++;
99
100
private _worker = new threads.Worker(import.meta.filename);
101
private _pending?: [resolve: Function, reject: Function, file: Vinyl[], options: ts.TranspileOptions, t1: number];
102
private _durations: number[] = [];
103
104
constructor(outFileFn: (fileName: string) => string) {
105
106
this._worker.addListener('message', (res: TranspileRes) => {
107
if (!this._pending) {
108
console.error('RECEIVING data WITHOUT request');
109
return;
110
}
111
112
const [resolve, reject, files, options, t1] = this._pending;
113
114
const outFiles: Vinyl[] = [];
115
const diag: ts.Diagnostic[] = [];
116
117
for (let i = 0; i < res.jsSrcs.length; i++) {
118
// inputs and outputs are aligned across the arrays
119
const file = files[i];
120
const jsSrc = res.jsSrcs[i];
121
const diag = res.diagnostics[i];
122
123
if (diag.length > 0) {
124
diag.push(...diag);
125
continue;
126
}
127
const SuffixTypes = {
128
Dts: 5,
129
Ts: 3,
130
Unknown: 0
131
} as const;
132
const suffixLen = file.path.endsWith('.d.ts') ? SuffixTypes.Dts
133
: file.path.endsWith('.ts') ? SuffixTypes.Ts
134
: SuffixTypes.Unknown;
135
136
// check if output of a DTS-files isn't just "empty" and iff so
137
// skip this file
138
if (suffixLen === SuffixTypes.Dts && _isDefaultEmpty(jsSrc)) {
139
continue;
140
}
141
142
const outBase = options.compilerOptions?.outDir ?? file.base;
143
const outPath = outFileFn(file.path);
144
145
outFiles.push(new Vinyl({
146
path: outPath,
147
base: outBase,
148
contents: Buffer.from(jsSrc),
149
}));
150
}
151
152
this._pending = undefined;
153
this._durations.push(Date.now() - t1);
154
155
if (diag.length > 0) {
156
reject(diag);
157
} else {
158
resolve(outFiles);
159
}
160
});
161
}
162
163
terminate() {
164
// console.log(`Worker#${this.id} ENDS after ${this._durations.length} jobs (total: ${this._durations.reduce((p, c) => p + c, 0)}, avg: ${this._durations.reduce((p, c) => p + c, 0) / this._durations.length})`);
165
this._worker.terminate();
166
}
167
168
get isBusy() {
169
return this._pending !== undefined;
170
}
171
172
next(files: Vinyl[], options: ts.TranspileOptions) {
173
if (this._pending !== undefined) {
174
throw new Error('BUSY');
175
}
176
return new Promise<Vinyl[]>((resolve, reject) => {
177
this._pending = [resolve, reject, files, options, Date.now()];
178
const req: TranspileReq = {
179
options,
180
tsSrcs: files.map(file => String(file.contents))
181
};
182
this._worker.postMessage(req);
183
});
184
}
185
}
186
187
export interface ITranspiler {
188
onOutfile?: (file: Vinyl) => void;
189
join(): Promise<void>;
190
transpile(file: Vinyl): void;
191
}
192
193
export class TscTranspiler implements ITranspiler {
194
195
static P = Math.floor(cpus().length * .5);
196
197
private readonly _outputFileNames: OutputFileNameOracle;
198
199
200
public onOutfile?: (file: Vinyl) => void;
201
202
private _workerPool: TranspileWorker[] = [];
203
private _queue: Vinyl[] = [];
204
private _allJobs: Promise<unknown>[] = [];
205
206
private readonly _logFn: (topic: string, message: string) => void;
207
private readonly _onError: (err: any) => void;
208
private readonly _cmdLine: ts.ParsedCommandLine;
209
210
constructor(
211
logFn: (topic: string, message: string) => void,
212
onError: (err: any) => void,
213
configFilePath: string,
214
cmdLine: ts.ParsedCommandLine
215
) {
216
this._logFn = logFn;
217
this._onError = onError;
218
this._cmdLine = cmdLine;
219
this._logFn('Transpile', `will use ${TscTranspiler.P} transpile worker`);
220
this._outputFileNames = new OutputFileNameOracle(this._cmdLine, configFilePath);
221
}
222
223
async join() {
224
// wait for all penindg jobs
225
this._consumeQueue();
226
await Promise.allSettled(this._allJobs);
227
this._allJobs.length = 0;
228
229
// terminate all worker
230
this._workerPool.forEach(w => w.terminate());
231
this._workerPool.length = 0;
232
}
233
234
235
transpile(file: Vinyl) {
236
237
if (this._cmdLine.options.noEmit) {
238
// not doing ANYTHING here
239
return;
240
}
241
242
const newLen = this._queue.push(file);
243
if (newLen > TscTranspiler.P ** 2) {
244
this._consumeQueue();
245
}
246
}
247
248
private _consumeQueue(): void {
249
250
if (this._queue.length === 0) {
251
// no work...
252
return;
253
}
254
255
// kinda LAZYily create workers
256
if (this._workerPool.length === 0) {
257
for (let i = 0; i < TscTranspiler.P; i++) {
258
this._workerPool.push(new TranspileWorker(file => this._outputFileNames.getOutputFileName(file)));
259
}
260
}
261
262
const freeWorker = this._workerPool.filter(w => !w.isBusy);
263
if (freeWorker.length === 0) {
264
// OK, they will pick up work themselves
265
return;
266
}
267
268
for (const worker of freeWorker) {
269
if (this._queue.length === 0) {
270
break;
271
}
272
273
const job = new Promise(resolve => {
274
275
const consume = () => {
276
const files = this._queue.splice(0, TscTranspiler.P);
277
if (files.length === 0) {
278
// DONE
279
resolve(undefined);
280
return;
281
}
282
// work on the NEXT file
283
// const [inFile, outFn] = req;
284
worker.next(files, { compilerOptions: this._cmdLine.options }).then(outFiles => {
285
if (this.onOutfile) {
286
outFiles.map(this.onOutfile, this);
287
}
288
consume();
289
}).catch(err => {
290
this._onError(err);
291
});
292
};
293
294
consume();
295
});
296
297
this._allJobs.push(job);
298
}
299
}
300
}
301
302
export class ESBuildTranspiler implements ITranspiler {
303
304
private readonly _outputFileNames: OutputFileNameOracle;
305
private _jobs: Promise<any>[] = [];
306
307
onOutfile?: ((file: Vinyl) => void) | undefined;
308
309
private readonly _transformOpts: esbuild.TransformOptions;
310
private readonly _logFn: (topic: string, message: string) => void;
311
private readonly _onError: (err: any) => void;
312
private readonly _cmdLine: ts.ParsedCommandLine;
313
314
constructor(
315
logFn: (topic: string, message: string) => void,
316
onError: (err: any) => void,
317
configFilePath: string,
318
cmdLine: ts.ParsedCommandLine
319
) {
320
this._logFn = logFn;
321
this._onError = onError;
322
this._cmdLine = cmdLine;
323
this._logFn('Transpile', `will use ESBuild to transpile source files`);
324
this._outputFileNames = new OutputFileNameOracle(this._cmdLine, configFilePath);
325
326
const isExtension = configFilePath.includes('extensions');
327
328
const target = getTargetStringFromTsConfig(configFilePath);
329
330
this._transformOpts = {
331
target: [target],
332
format: isExtension ? 'cjs' : 'esm',
333
platform: isExtension ? 'node' : undefined,
334
loader: 'ts',
335
sourcemap: 'inline',
336
tsconfigRaw: JSON.stringify({
337
compilerOptions: {
338
...this._cmdLine.options,
339
...{
340
module: isExtension ? ts.ModuleKind.CommonJS : undefined
341
} satisfies ts.CompilerOptions
342
}
343
}),
344
supported: {
345
'class-static-blocks': false, // SEE https://github.com/evanw/esbuild/issues/3823,
346
'dynamic-import': !isExtension, // see https://github.com/evanw/esbuild/issues/1281
347
'class-field': !isExtension
348
}
349
};
350
}
351
352
async join(): Promise<void> {
353
const jobs = this._jobs.slice();
354
this._jobs.length = 0;
355
await Promise.allSettled(jobs);
356
}
357
358
transpile(file: Vinyl): void {
359
if (!(file.contents instanceof Buffer)) {
360
throw Error('file.contents must be a Buffer');
361
}
362
const t1 = Date.now();
363
this._jobs.push(esbuild.transform(file.contents, {
364
...this._transformOpts,
365
sourcefile: file.path,
366
}).then(result => {
367
368
// check if output of a DTS-files isn't just "empty" and iff so
369
// skip this file
370
if (file.path.endsWith('.d.ts') && _isDefaultEmpty(result.code)) {
371
return;
372
}
373
374
const outBase = this._cmdLine.options.outDir ?? file.base;
375
const outPath = this._outputFileNames.getOutputFileName(file.path);
376
377
this.onOutfile!(new Vinyl({
378
path: outPath,
379
base: outBase,
380
contents: Buffer.from(result.code),
381
}));
382
383
this._logFn('Transpile', `esbuild took ${Date.now() - t1}ms for ${file.path}`);
384
385
}).catch(err => {
386
this._onError(err);
387
}));
388
}
389
}
390
391
function _isDefaultEmpty(src: string): boolean {
392
return src
393
.replace('"use strict";', '')
394
.replace(/\/\/# sourceMappingURL.*^/, '')
395
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
396
.trim().length === 0;
397
}
398
399