Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/next/index.ts
13379 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 * as esbuild from 'esbuild';
7
import * as fs from 'fs';
8
import * as path from 'path';
9
import { promisify } from 'util';
10
11
import glob from 'glob';
12
import gulpWatch from '../lib/watch/index.ts';
13
import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts';
14
import { convertPrivateFields, adjustSourceMap, type ConvertPrivateFieldsResult } from './private-to-property.ts';
15
import { getVersion } from '../lib/getVersion.ts';
16
import { getGitCommitDate } from '../lib/date.ts';
17
import product from '../../product.json' with { type: 'json' };
18
import packageJson from '../../package.json' with { type: 'json' };
19
import { useEsbuildTranspile } from '../buildConfig.ts';
20
import { isWebExtension, type IScannedBuiltinExtension } from '../lib/extensions.ts';
21
22
const globAsync = promisify(glob);
23
24
// ============================================================================
25
// Configuration
26
// ============================================================================
27
28
const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname));
29
const commit = getVersion(REPO_ROOT);
30
const quality = (product as { quality?: string }).quality;
31
const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version;
32
33
// CLI: transpile [--watch] | bundle [--minify] [--nls] [--out <dir>]
34
const command = process.argv[2]; // 'transpile' or 'bundle'
35
36
function getArgValue(name: string): string | undefined {
37
const index = process.argv.indexOf(name);
38
if (index !== -1 && index + 1 < process.argv.length) {
39
return process.argv[index + 1];
40
}
41
return undefined;
42
}
43
44
const options = {
45
watch: process.argv.includes('--watch'),
46
minify: process.argv.includes('--minify'),
47
nls: process.argv.includes('--nls'),
48
manglePrivates: process.argv.includes('--mangle-privates'),
49
excludeTests: process.argv.includes('--exclude-tests'),
50
out: getArgValue('--out'),
51
target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' | 'web'
52
sourceMapBaseUrl: getArgValue('--source-map-base-url'),
53
};
54
55
// Build targets
56
type BuildTarget = 'desktop' | 'server' | 'server-web' | 'web';
57
58
const SRC_DIR = 'src';
59
const OUT_DIR = 'out';
60
const OUT_VSCODE_DIR = 'out-vscode';
61
62
// UTF-8 BOM - added to test files with 'utf8' in the path (matches gulp build behavior)
63
const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]);
64
65
// ============================================================================
66
// Entry Points (from build/buildfile.ts)
67
// ============================================================================
68
69
// Extension host bundles are excluded from private field mangling because they
70
// expose API surface to extensions where encapsulation matters.
71
const extensionHostEntryPoints = [
72
'vs/workbench/api/node/extensionHostProcess',
73
'vs/workbench/api/worker/extensionHostWorkerMain',
74
];
75
76
function isExtensionHostBundle(filePath: string): boolean {
77
const normalized = filePath.replaceAll('\\', '/');
78
return extensionHostEntryPoints.some(ep => normalized.endsWith(`${ep}.js`));
79
}
80
81
// Workers - shared between targets
82
const workerEntryPoints = [
83
'vs/editor/common/services/editorWebWorkerMain',
84
'vs/workbench/api/worker/extensionHostWorkerMain',
85
'vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain',
86
'vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain',
87
'vs/workbench/services/search/worker/localFileSearchMain',
88
'vs/workbench/contrib/output/common/outputLinkComputerMain',
89
'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain',
90
];
91
92
// Desktop-only workers (use electron-browser)
93
const desktopWorkerEntryPoints = [
94
'vs/platform/profiling/electron-browser/profileAnalysisWorkerMain',
95
];
96
97
// Desktop workbench and code entry points
98
const desktopEntryPoints = [
99
'vs/workbench/workbench.desktop.main',
100
'vs/sessions/sessions.desktop.main',
101
'vs/workbench/contrib/debug/node/telemetryApp',
102
'vs/platform/files/node/watcher/watcherMain',
103
'vs/platform/terminal/node/ptyHostMain',
104
'vs/platform/agentHost/node/agentHostMain',
105
'vs/platform/agentHost/node/diffWorkerMain',
106
'vs/workbench/api/node/extensionHostProcess',
107
];
108
109
const codeEntryPoints = [
110
'vs/code/node/cliProcessMain',
111
'vs/code/electron-utility/sharedProcess/sharedProcessMain',
112
'vs/code/electron-browser/workbench/workbench',
113
'vs/sessions/electron-browser/sessions',
114
];
115
116
// Web entry points (used in server-web and vscode-web)
117
const webEntryPoints = [
118
'vs/workbench/workbench.web.main.internal',
119
'vs/code/browser/workbench/workbench',
120
];
121
122
// Additional web-only entry points (CDN build only, not in server-web)
123
const webOnlyEntryPoints = [
124
'vs/sessions/sessions.web.main.internal',
125
];
126
127
const keyboardMapEntryPoints = [
128
'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux',
129
'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin',
130
'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win',
131
];
132
133
// Server entry points (reh)
134
const serverEntryPoints = [
135
'vs/workbench/api/node/extensionHostProcess',
136
'vs/platform/files/node/watcher/watcherMain',
137
'vs/platform/terminal/node/ptyHostMain',
138
'vs/platform/agentHost/node/agentHostMain',
139
'vs/platform/agentHost/node/diffWorkerMain',
140
];
141
142
// Bootstrap files per target
143
const bootstrapEntryPointsDesktop = [
144
'main',
145
'cli',
146
'bootstrap-fork',
147
];
148
149
const bootstrapEntryPointsServer = [
150
'server-main',
151
'server-cli',
152
'bootstrap-fork',
153
];
154
155
/**
156
* Get entry points for a build target.
157
*/
158
function getEntryPointsForTarget(target: BuildTarget): string[] {
159
switch (target) {
160
case 'desktop':
161
return [
162
...workerEntryPoints,
163
...desktopWorkerEntryPoints,
164
...desktopEntryPoints,
165
...codeEntryPoints,
166
];
167
case 'server':
168
return [
169
...serverEntryPoints,
170
];
171
case 'server-web':
172
return [
173
...serverEntryPoints,
174
...workerEntryPoints,
175
...webEntryPoints,
176
...keyboardMapEntryPoints,
177
];
178
case 'web':
179
return [
180
...workerEntryPoints,
181
...webOnlyEntryPoints,
182
'vs/workbench/workbench.web.main.internal', // web workbench only (no browser shell)
183
...keyboardMapEntryPoints,
184
];
185
default:
186
throw new Error(`Unknown target: ${target}`);
187
}
188
}
189
190
/**
191
* Get bootstrap entry points for a build target.
192
*/
193
function getBootstrapEntryPointsForTarget(target: BuildTarget): string[] {
194
switch (target) {
195
case 'desktop':
196
return bootstrapEntryPointsDesktop;
197
case 'server':
198
case 'server-web':
199
return bootstrapEntryPointsServer;
200
case 'web':
201
return []; // Web has no bootstrap files (served by external server)
202
default:
203
throw new Error(`Unknown target: ${target}`);
204
}
205
}
206
207
/**
208
* Get entry points that should bundle CSS (workbench mains).
209
*/
210
function getCssBundleEntryPointsForTarget(target: BuildTarget): Set<string> {
211
switch (target) {
212
case 'desktop':
213
return new Set([
214
'vs/workbench/workbench.desktop.main',
215
'vs/code/electron-browser/workbench/workbench',
216
'vs/sessions/sessions.desktop.main',
217
'vs/sessions/electron-browser/sessions',
218
]);
219
case 'server':
220
return new Set(); // Server has no UI
221
case 'server-web':
222
return new Set([
223
'vs/workbench/workbench.web.main.internal',
224
'vs/code/browser/workbench/workbench',
225
]);
226
case 'web':
227
return new Set([
228
'vs/workbench/workbench.web.main.internal',
229
'vs/sessions/sessions.web.main.internal',
230
]);
231
default:
232
throw new Error(`Unknown target: ${target}`);
233
}
234
}
235
236
// ============================================================================
237
// Resource Patterns (files to copy, not transpile/bundle)
238
// ============================================================================
239
240
// Common resources needed by all targets
241
const commonResourcePatterns = [
242
// Tree-sitter queries
243
'vs/editor/common/languages/highlights/*.scm',
244
'vs/editor/common/languages/injections/*.scm',
245
246
// SVGs referenced from CSS (needed for transpile/dev builds where CSS is copied as-is)
247
'vs/workbench/browser/media/code-icon.svg',
248
'vs/workbench/browser/parts/editor/media/letterpress*.svg',
249
'vs/sessions/contrib/chat/browser/media/*.svg'
250
];
251
252
// Resources for desktop target
253
const desktopResourcePatterns = [
254
...commonResourcePatterns,
255
256
// HTML
257
'vs/code/electron-browser/workbench/workbench.html',
258
'vs/code/electron-browser/workbench/workbench-dev.html',
259
'vs/sessions/electron-browser/sessions.html',
260
'vs/sessions/electron-browser/sessions-dev.html',
261
'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html',
262
'vs/workbench/contrib/webview/browser/pre/*.html',
263
264
// Webview pre scripts
265
'vs/workbench/contrib/webview/browser/pre/*.js',
266
267
// Shell scripts
268
'vs/base/node/*.sh',
269
'vs/workbench/contrib/terminal/common/scripts/*.sh',
270
'vs/workbench/contrib/terminal/common/scripts/*.ps1',
271
'vs/workbench/contrib/terminal/common/scripts/*.psm1',
272
'vs/workbench/contrib/terminal/common/scripts/*.fish',
273
'vs/workbench/contrib/terminal/common/scripts/*.zsh',
274
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psd1',
275
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psm1',
276
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.dll',
277
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.ps1xml',
278
'vs/workbench/contrib/terminal/common/scripts/psreadline/net6plus/*.dll',
279
'vs/workbench/contrib/terminal/common/scripts/psreadline/netstd/*.dll',
280
'vs/workbench/contrib/externalTerminal/**/*.scpt',
281
282
// Media - audio
283
'vs/platform/accessibilitySignal/browser/media/*.mp3',
284
285
// Media - images
286
'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg',
287
'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png',
288
'vs/workbench/contrib/welcomeOnboarding/browser/media/*.svg',
289
'vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}',
290
'vs/workbench/services/extensionManagement/common/media/*.svg',
291
'vs/workbench/services/extensionManagement/common/media/*.png',
292
'vs/workbench/browser/parts/editor/media/*.png',
293
'vs/workbench/contrib/debug/browser/media/*.png',
294
295
// Sessions - built-in prompts and skills
296
'vs/sessions/prompts/*.prompt.md',
297
'vs/sessions/skills/**/SKILL.md',
298
];
299
300
// Resources for server target (minimal - no UI)
301
const serverResourcePatterns = [
302
// Shell scripts for process monitoring
303
'vs/base/node/cpuUsage.sh',
304
'vs/base/node/ps.sh',
305
306
// External Terminal
307
'vs/workbench/contrib/externalTerminal/**/*.scpt',
308
309
// Terminal shell integration
310
'vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1',
311
'vs/workbench/contrib/terminal/common/scripts/CodeTabExpansion.psm1',
312
'vs/workbench/contrib/terminal/common/scripts/GitTabExpansion.psm1',
313
'vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh',
314
'vs/workbench/contrib/terminal/common/scripts/shellIntegration-env.zsh',
315
'vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh',
316
'vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh',
317
'vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh',
318
'vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish',
319
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psd1',
320
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.psm1',
321
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.dll',
322
'vs/workbench/contrib/terminal/common/scripts/psreadline/*.ps1xml',
323
'vs/workbench/contrib/terminal/common/scripts/psreadline/net6plus/*.dll',
324
'vs/workbench/contrib/terminal/common/scripts/psreadline/netstd/*.dll',
325
];
326
327
// Resources for server-web target (server + web UI)
328
const serverWebResourcePatterns = [
329
...serverResourcePatterns,
330
...commonResourcePatterns,
331
332
// Web HTML
333
'vs/code/browser/workbench/workbench.html',
334
'vs/code/browser/workbench/workbench-dev.html',
335
'vs/code/browser/workbench/callback.html',
336
'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html',
337
'vs/workbench/contrib/webview/browser/pre/*.html',
338
339
// Webview pre scripts
340
'vs/workbench/contrib/webview/browser/pre/*.js',
341
342
// Media - audio
343
'vs/platform/accessibilitySignal/browser/media/*.mp3',
344
345
// Media - images
346
'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg',
347
'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png',
348
'vs/workbench/contrib/welcomeOnboarding/browser/media/*.svg',
349
'vs/workbench/contrib/extensions/browser/media/*.svg',
350
'vs/workbench/contrib/extensions/browser/media/*.png',
351
'vs/workbench/services/extensionManagement/common/media/*.svg',
352
'vs/workbench/services/extensionManagement/common/media/*.png',
353
];
354
355
// Resources for standalone web target (browser-only, no server)
356
const webResourcePatterns = [
357
...commonResourcePatterns,
358
359
// Web HTML
360
'vs/code/browser/workbench/workbench.html',
361
'vs/code/browser/workbench/workbench-dev.html',
362
'vs/code/browser/workbench/callback.html',
363
'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html',
364
'vs/workbench/contrib/webview/browser/pre/*.html',
365
366
// Webview pre scripts
367
'vs/workbench/contrib/webview/browser/pre/*.js',
368
369
// Media - audio
370
'vs/platform/accessibilitySignal/browser/media/*.mp3',
371
372
// Media - images
373
'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg',
374
'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png',
375
'vs/workbench/contrib/welcomeOnboarding/browser/media/*.svg',
376
'vs/workbench/contrib/extensions/browser/media/*.svg',
377
'vs/workbench/contrib/extensions/browser/media/*.png',
378
'vs/workbench/services/extensionManagement/common/media/*.svg',
379
'vs/workbench/services/extensionManagement/common/media/*.png',
380
];
381
382
/**
383
* Get resource patterns for a build target.
384
*/
385
function getResourcePatternsForTarget(target: BuildTarget): string[] {
386
switch (target) {
387
case 'desktop':
388
return desktopResourcePatterns;
389
case 'server':
390
return serverResourcePatterns;
391
case 'server-web':
392
return serverWebResourcePatterns;
393
case 'web':
394
return webResourcePatterns;
395
default:
396
throw new Error(`Unknown target: ${target}`);
397
}
398
}
399
400
// ============================================================================
401
// Utilities
402
// ============================================================================
403
404
async function cleanDir(dir: string): Promise<void> {
405
const fullPath = path.join(REPO_ROOT, dir);
406
console.log(`[clean] ${dir}`);
407
await fs.promises.rm(fullPath, { recursive: true, force: true });
408
await fs.promises.mkdir(fullPath, { recursive: true });
409
}
410
411
/**
412
* Scan for built-in extensions in the given directory.
413
* Returns an array of extension entries for the builtinExtensionsScannerService.
414
*/
415
function scanBuiltinExtensions(extensionsRoot: string): Array<IScannedBuiltinExtension> {
416
const scannedExtensions: Array<IScannedBuiltinExtension> = [];
417
const extensionsPath = path.join(REPO_ROOT, extensionsRoot);
418
419
if (!fs.existsSync(extensionsPath)) {
420
return scannedExtensions;
421
}
422
423
for (const extensionFolder of fs.readdirSync(extensionsPath)) {
424
const packageJSONPath = path.join(extensionsPath, extensionFolder, 'package.json');
425
if (!fs.existsSync(packageJSONPath)) {
426
continue;
427
}
428
try {
429
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf8'));
430
if (!isWebExtension(packageJSON)) {
431
continue;
432
}
433
const children = fs.readdirSync(path.join(extensionsPath, extensionFolder));
434
const packageNLSPath = children.filter(child => child === 'package.nls.json')[0];
435
const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsPath, extensionFolder, packageNLSPath), 'utf8')) : undefined;
436
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
437
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
438
439
scannedExtensions.push({
440
extensionPath: extensionFolder,
441
packageJSON,
442
packageNLS,
443
readmePath: readme ? path.join(extensionFolder, readme) : undefined,
444
changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined,
445
});
446
} catch (e) {
447
// Skip invalid extensions
448
}
449
}
450
451
return scannedExtensions;
452
}
453
454
/**
455
* Get the date from the out directory date file, or return the git commit date.
456
*/
457
function readISODate(outDir: string): string {
458
try {
459
return fs.readFileSync(path.join(REPO_ROOT, outDir, 'date'), 'utf8');
460
} catch {
461
return getGitCommitDate();
462
}
463
}
464
465
/**
466
* Only used to make encoding tests happy. The source files don't have a BOM but the
467
* tests expect one... so we add it here.
468
*/
469
function needsBomAdded(filePath: string): boolean {
470
return /([\/\\])test\1.*utf8/.test(filePath);
471
}
472
473
async function copyFile(srcPath: string, destPath: string): Promise<void> {
474
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
475
476
if (needsBomAdded(srcPath)) {
477
const content = await fs.promises.readFile(srcPath);
478
if (content[0] !== 0xef || content[1] !== 0xbb || content[2] !== 0xbf) {
479
await fs.promises.writeFile(destPath, Buffer.concat([UTF8_BOM, content]));
480
return;
481
}
482
}
483
await fs.promises.copyFile(srcPath, destPath);
484
}
485
486
/**
487
* Standalone TypeScript files that need to be compiled separately (not bundled).
488
* These run in special contexts (e.g., Electron preload) where bundling isn't appropriate.
489
* Only needed for desktop target.
490
*/
491
const desktopStandaloneFiles = [
492
'vs/base/parts/sandbox/electron-browser/preload.ts',
493
'vs/base/parts/sandbox/electron-browser/preload-aux.ts',
494
'vs/platform/browserView/electron-browser/preload-browserView.ts',
495
];
496
497
async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: BuildTarget): Promise<void> {
498
// Only desktop needs preload scripts
499
if (target !== 'desktop') {
500
return;
501
}
502
503
console.log(`[standalone] Compiling ${desktopStandaloneFiles.length} standalone files...`);
504
505
const banner = `/*!--------------------------------------------------------
506
* Copyright (C) Microsoft Corporation. All rights reserved.
507
*--------------------------------------------------------*/`;
508
509
await Promise.all(desktopStandaloneFiles.map(async (file) => {
510
const entryPath = path.join(REPO_ROOT, SRC_DIR, file);
511
const outPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js'));
512
513
await esbuild.build({
514
entryPoints: [entryPath],
515
outfile: outPath,
516
bundle: false, // Don't bundle - these are standalone scripts
517
format: 'cjs', // CommonJS for Electron preload
518
platform: 'node',
519
target: ['es2024'],
520
sourcemap: 'linked',
521
sourcesContent: false,
522
minify: doMinify,
523
banner: { js: banner },
524
logLevel: 'warning',
525
});
526
}));
527
528
console.log(`[standalone] Done`);
529
}
530
531
/**
532
* Copy ALL non-TypeScript files from src/ to the output directory.
533
* This matches the old gulp build behavior where `gulp.src('src/**')` streams
534
* every file and non-TS files bypass the compiler via tsFilter.restore.
535
* Used for development/transpile builds only - production bundles use
536
* copyResources() with curated per-target patterns instead.
537
*/
538
async function copyAllNonTsFiles(outDir: string, excludeTests: boolean): Promise<void> {
539
console.log(`[resources] Copying all non-TS files to ${outDir}...`);
540
541
const ignorePatterns = [
542
// Exclude .ts files but keep .d.ts files (they're needed at runtime for type references)
543
'**/*.ts',
544
];
545
if (excludeTests) {
546
ignorePatterns.push('**/test/**');
547
}
548
549
const files = await globAsync('**/*', {
550
cwd: path.join(REPO_ROOT, SRC_DIR),
551
nodir: true,
552
ignore: ignorePatterns,
553
});
554
555
// Re-include .d.ts files that were excluded by the *.ts ignore
556
const dtsFiles = await globAsync('**/*.d.ts', {
557
cwd: path.join(REPO_ROOT, SRC_DIR),
558
ignore: excludeTests ? ['**/test/**'] : [],
559
});
560
561
const allFiles = [...new Set([...files, ...dtsFiles])];
562
563
await Promise.all(allFiles.map(file => {
564
const srcPath = path.join(REPO_ROOT, SRC_DIR, file);
565
const destPath = path.join(REPO_ROOT, outDir, file);
566
return copyFile(srcPath, destPath);
567
}));
568
569
console.log(`[resources] Copied ${allFiles.length} files`);
570
}
571
572
/**
573
* Copy curated resource files for production bundles.
574
* Uses specific per-target patterns matching the old build's vscodeResourceIncludes,
575
* serverResourceIncludes, etc. Only called by bundle() - transpile uses copyAllNonTsFiles().
576
*/
577
async function copyResources(outDir: string, target: BuildTarget): Promise<void> {
578
console.log(`[resources] Copying to ${outDir} for target '${target}'...`);
579
let copied = 0;
580
581
const ignorePatterns = ['**/test/**', '**/*-dev.html'];
582
583
const resourcePatterns = getResourcePatternsForTarget(target);
584
for (const pattern of resourcePatterns) {
585
const files = await globAsync(pattern, {
586
cwd: path.join(REPO_ROOT, SRC_DIR),
587
ignore: ignorePatterns,
588
});
589
590
for (const file of files) {
591
const srcPath = path.join(REPO_ROOT, SRC_DIR, file);
592
const destPath = path.join(REPO_ROOT, outDir, file);
593
594
await copyFile(srcPath, destPath);
595
copied++;
596
}
597
}
598
599
console.log(`[resources] Copied ${copied} files`);
600
}
601
602
// ============================================================================
603
// Plugins
604
// ============================================================================
605
606
function inlineMinimistPlugin(): esbuild.Plugin {
607
return {
608
name: 'inline-minimist',
609
setup(build) {
610
build.onResolve({ filter: /^minimist$/ }, () => ({
611
path: path.join(REPO_ROOT, 'node_modules/minimist/index.js'),
612
external: false,
613
}));
614
},
615
};
616
}
617
618
function cssExternalPlugin(): esbuild.Plugin {
619
// Mark CSS imports as external so they stay as import statements
620
// The CSS files are copied separately and loaded by the browser at runtime
621
return {
622
name: 'css-external',
623
setup(build) {
624
build.onResolve({ filter: /\.css$/ }, (args) => ({
625
path: args.path,
626
external: true,
627
}));
628
},
629
};
630
}
631
632
/**
633
* esbuild plugin that transforms source files to inject build-time configuration.
634
* This runs during onLoad so the transformation happens before esbuild processes the content,
635
* ensuring placeholders like `/*BUILD->INSERT_PRODUCT_CONFIGURATION* /` are replaced
636
* before esbuild strips them as non-legal comments.
637
*/
638
function fileContentMapperPlugin(outDir: string, target: BuildTarget): esbuild.Plugin {
639
// Cache the replacement strings (computed once)
640
let productConfigReplacement: string | undefined;
641
let builtinExtensionsReplacement: string | undefined;
642
643
return {
644
name: 'file-content-mapper',
645
setup(build) {
646
build.onLoad({ filter: /\.ts$/ }, async (args) => {
647
// Skip .d.ts files
648
if (args.path.endsWith('.d.ts')) {
649
return undefined;
650
}
651
652
let contents = await fs.promises.readFile(args.path, 'utf-8');
653
let modified = false;
654
655
// Inject product configuration
656
if (contents.includes('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/')) {
657
if (productConfigReplacement === undefined) {
658
// For server-web, remove webEndpointUrlTemplate
659
const productForTarget = target === 'server-web'
660
? { ...product, webEndpointUrlTemplate: undefined }
661
: product;
662
const productConfiguration = JSON.stringify({
663
...productForTarget,
664
version,
665
commit,
666
date: readISODate(outDir)
667
});
668
// Remove the outer braces since the placeholder is inside an object literal
669
productConfigReplacement = productConfiguration.substring(1, productConfiguration.length - 1);
670
}
671
contents = contents.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfigReplacement!);
672
modified = true;
673
}
674
675
// Inject built-in extensions list
676
if (contents.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) {
677
if (builtinExtensionsReplacement === undefined) {
678
// Web target uses .build/web/extensions (from compileWebExtensionsBuildTask)
679
// Other targets use .build/extensions
680
const extensionsRoot = target === 'web' ? '.build/web/extensions' : '.build/extensions';
681
const builtinExtensions = JSON.stringify(scanBuiltinExtensions(extensionsRoot));
682
// Remove the outer brackets since the placeholder is inside an array literal
683
builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1);
684
}
685
contents = contents.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensionsReplacement!);
686
modified = true;
687
}
688
689
if (modified) {
690
return { contents, loader: 'ts' };
691
}
692
693
// No modifications, let esbuild handle normally
694
return undefined;
695
});
696
},
697
};
698
}
699
700
// ============================================================================
701
// Transpile (Goal 1: TS → JS using esbuild.transform for maximum speed)
702
// ============================================================================
703
704
// Shared transform options for single-file transpilation
705
const transformOptions: esbuild.TransformOptions = {
706
loader: 'ts',
707
format: 'esm',
708
target: 'es2024',
709
sourcemap: 'inline',
710
sourcesContent: false,
711
tsconfigRaw: JSON.stringify({
712
compilerOptions: {
713
experimentalDecorators: true,
714
useDefineForClassFields: false
715
}
716
}),
717
};
718
719
async function transpileFile(srcPath: string, destPath: string): Promise<void> {
720
const source = await fs.promises.readFile(srcPath, 'utf-8');
721
const result = await esbuild.transform(source, {
722
...transformOptions,
723
sourcefile: srcPath,
724
});
725
726
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
727
728
const adjustedCode = adjustEsmUrl(result.code);
729
await fs.promises.writeFile(destPath, adjustedCode);
730
}
731
732
/*
733
* This enables https://github.com/microsoft/esm-url-bundler-plugins to work on both original and transpiled sources.
734
* Usees regex to only replace `.ts?esm` inside quoted URL strings, avoiding false positives.
735
*
736
* E.g.:
737
* - esmModuleLocationBundler: () => new URL("../../../api/worker/extensionHostWorkerMain.ts?esm", import.meta.url)
738
* + esmModuleLocationBundler: () => new URL("../../../api/worker/extensionHostWorkerMain.js?esm", import.meta.url)
739
*/
740
function adjustEsmUrl(code: string): string {
741
const fixedCode = code.replace(/\.ts(\?esm['"])/g, '.js$1');
742
return fixedCode;
743
}
744
745
async function transpile(outDir: string, excludeTests: boolean): Promise<void> {
746
// Find all .ts files
747
const ignorePatterns = ['**/*.d.ts'];
748
if (excludeTests) {
749
ignorePatterns.push('**/test/**');
750
}
751
752
const files = await globAsync('**/*.ts', {
753
cwd: path.join(REPO_ROOT, SRC_DIR),
754
ignore: ignorePatterns,
755
});
756
757
console.log(`[transpile] Found ${files.length} files`);
758
759
// Transpile all files in parallel using esbuild.transform (fastest approach)
760
await Promise.all(files.map(file => {
761
const srcPath = path.join(REPO_ROOT, SRC_DIR, file);
762
const destPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js'));
763
return transpileFile(srcPath, destPath);
764
}));
765
}
766
767
// ============================================================================
768
// Bundle (Goal 2: JS → bundled JS)
769
// ============================================================================
770
771
async function bundle(outDir: string, doMinify: boolean, doNls: boolean, doManglePrivates: boolean, target: BuildTarget, sourceMapBaseUrl?: string): Promise<void> {
772
await cleanDir(outDir);
773
774
// Write build date file (used by packaging to embed in product.json).
775
// Reuse the date from out-build/date if it exists (written by the gulp
776
// writeISODate task) so that all parallel bundle outputs share the same
777
// timestamp - this is required for deterministic builds (e.g. macOS Universal).
778
const outDirPath = path.join(REPO_ROOT, outDir);
779
await fs.promises.mkdir(outDirPath, { recursive: true });
780
let buildDate: string;
781
try {
782
buildDate = await fs.promises.readFile(path.join(REPO_ROOT, 'out-build', 'date'), 'utf8');
783
} catch {
784
buildDate = getGitCommitDate();
785
}
786
await fs.promises.writeFile(path.join(outDirPath, 'date'), buildDate, 'utf8');
787
788
console.log(`[bundle] ${SRC_DIR} → ${outDir} (target: ${target})${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}${doManglePrivates ? ' (mangle-privates)' : ''}`);
789
const t1 = Date.now();
790
791
// Read TSLib for banner
792
const tslibPath = path.join(REPO_ROOT, 'node_modules/tslib/tslib.es6.js');
793
const tslib = await fs.promises.readFile(tslibPath, 'utf-8');
794
const banner = {
795
js: `/*!--------------------------------------------------------
796
* Copyright (C) Microsoft Corporation. All rights reserved.
797
*--------------------------------------------------------*/
798
${tslib}`,
799
css: `/*!--------------------------------------------------------
800
* Copyright (C) Microsoft Corporation. All rights reserved.
801
*--------------------------------------------------------*/`,
802
};
803
804
// Shared TypeScript options for bundling directly from source
805
const tsconfigRaw = JSON.stringify({
806
compilerOptions: {
807
experimentalDecorators: true,
808
useDefineForClassFields: false
809
}
810
});
811
812
// Create shared NLS collector (only used if doNls is true)
813
const nlsCollector = createNLSCollector();
814
const preserveEnglish = false; // Production mode: replace messages with null
815
816
// Get entry points based on target
817
const allEntryPoints = getEntryPointsForTarget(target);
818
const bootstrapEntryPoints = getBootstrapEntryPointsForTarget(target);
819
const bundleCssEntryPoints = getCssBundleEntryPointsForTarget(target);
820
821
// Collect all build results (with write: false)
822
const buildResults: { outPath: string; result: esbuild.BuildResult }[] = [];
823
824
// Create the file content mapper plugin (injects product config, builtin extensions)
825
const contentMapperPlugin = fileContentMapperPlugin(outDir, target);
826
827
// Bundle each entry point directly from TypeScript source
828
await Promise.all(allEntryPoints.map(async (entryPoint) => {
829
const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`);
830
const outPath = path.join(REPO_ROOT, outDir, `${entryPoint}.js`);
831
832
// Use CSS external plugin for entry points that don't need bundled CSS
833
const plugins: esbuild.Plugin[] = bundleCssEntryPoints.has(entryPoint) ? [] : [cssExternalPlugin()];
834
// Add content mapper plugin to inject product config and builtin extensions
835
plugins.push(contentMapperPlugin);
836
if (doNls) {
837
plugins.unshift(nlsPlugin({
838
baseDir: path.join(REPO_ROOT, SRC_DIR),
839
collector: nlsCollector,
840
}));
841
}
842
843
// For entry points that bundle CSS, we need to use outdir instead of outfile
844
// because esbuild can't produce multiple output files (JS + CSS) with outfile
845
const needsCssBundling = bundleCssEntryPoints.has(entryPoint);
846
847
const buildOptions: esbuild.BuildOptions = {
848
entryPoints: needsCssBundling
849
? [{ in: entryPath, out: entryPoint }]
850
: [entryPath],
851
...(needsCssBundling
852
? { outdir: path.join(REPO_ROOT, outDir) }
853
: { outfile: outPath }),
854
bundle: true,
855
format: 'esm',
856
platform: 'neutral',
857
target: ['es2024'],
858
packages: 'external',
859
sourcemap: 'linked',
860
sourcesContent: true,
861
minify: doMinify,
862
treeShaking: true,
863
banner,
864
loader: {
865
'.ttf': 'file',
866
'.svg': 'file',
867
'.png': 'file',
868
'.sh': 'file',
869
},
870
assetNames: 'media/[name]',
871
plugins,
872
write: false, // Don't write yet, we need to post-process
873
logLevel: 'warning',
874
logOverride: {
875
'unsupported-require-call': 'silent',
876
},
877
tsconfigRaw,
878
};
879
880
const result = await esbuild.build(buildOptions);
881
882
buildResults.push({ outPath, result });
883
}));
884
885
// Bundle bootstrap files (with minimist inlined) directly from TypeScript source
886
for (const entry of bootstrapEntryPoints) {
887
const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entry}.ts`);
888
if (!fs.existsSync(entryPath)) {
889
console.log(`[bundle] Skipping ${entry} (not found)`);
890
continue;
891
}
892
893
const outPath = path.join(REPO_ROOT, outDir, `${entry}.js`);
894
895
const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin(), contentMapperPlugin];
896
if (doNls) {
897
bootstrapPlugins.unshift(nlsPlugin({
898
baseDir: path.join(REPO_ROOT, SRC_DIR),
899
collector: nlsCollector,
900
}));
901
}
902
903
const result = await esbuild.build({
904
entryPoints: [entryPath],
905
outfile: outPath,
906
bundle: true,
907
format: 'esm',
908
platform: 'node',
909
target: ['es2024'],
910
packages: 'external',
911
sourcemap: 'linked',
912
sourcesContent: true,
913
minify: doMinify,
914
treeShaking: true,
915
banner,
916
plugins: bootstrapPlugins,
917
write: false, // Don't write yet, we need to post-process
918
logLevel: 'warning',
919
logOverride: {
920
'unsupported-require-call': 'silent',
921
},
922
tsconfigRaw,
923
});
924
925
buildResults.push({ outPath, result });
926
}
927
928
// Finalize NLS: sort entries, assign indices, write metadata files
929
let indexMap = new Map<string, number>();
930
if (doNls) {
931
// Also write NLS files to out-build for backwards compatibility with test runner
932
const nlsResult = await finalizeNLS(
933
nlsCollector,
934
path.join(REPO_ROOT, outDir),
935
[path.join(REPO_ROOT, 'out-build')]
936
);
937
indexMap = nlsResult.indexMap;
938
}
939
940
// Post-process and write all output files
941
let bundled = 0;
942
const mangleStats: { file: string; result: ConvertPrivateFieldsResult }[] = [];
943
// Map from JS file path to pre-mangle content + edits, for source map adjustment
944
const mangleEdits = new Map<string, { preMangleCode: string; edits: readonly import('./private-to-property.ts').TextEdit[] }>();
945
// Map from JS file path to pre-NLS content + edits, for source map adjustment
946
const nlsEdits = new Map<string, { preNLSCode: string; edits: readonly import('./private-to-property.ts').TextEdit[] }>();
947
// Defer .map files until all .js files are processed, because esbuild may
948
// emit the .map file in a different build result than the .js file (e.g.
949
// code-split chunks), and we need the NLS/mangle edits from the .js pass
950
// to be available when adjusting the .map.
951
const deferredMaps: { path: string; text: string; contents: Uint8Array }[] = [];
952
for (const { result } of buildResults) {
953
if (!result.outputFiles) {
954
continue;
955
}
956
957
for (const file of result.outputFiles) {
958
await fs.promises.mkdir(path.dirname(file.path), { recursive: true });
959
960
if (file.path.endsWith('.js') || file.path.endsWith('.css')) {
961
let content = file.text;
962
963
// Convert native #private fields to regular properties BEFORE NLS
964
// post-processing, so that the edit offsets align with esbuild's
965
// source map coordinate system (both reference the raw esbuild output).
966
// Skip extension host bundles - they expose API surface to extensions
967
// where true encapsulation matters more than the perf gain.
968
if (file.path.endsWith('.js') && doManglePrivates && !isExtensionHostBundle(file.path)) {
969
const preMangleCode = content;
970
const mangleResult = convertPrivateFields(content, file.path);
971
content = mangleResult.code;
972
if (mangleResult.editCount > 0) {
973
mangleStats.push({ file: path.relative(path.join(REPO_ROOT, outDir), file.path), result: mangleResult });
974
mangleEdits.set(file.path, { preMangleCode, edits: mangleResult.edits });
975
}
976
}
977
978
// Apply NLS post-processing if enabled (JS only)
979
if (file.path.endsWith('.js') && doNls && indexMap.size > 0) {
980
const preNLSCode = content;
981
const nlsResult = postProcessNLS(content, indexMap, preserveEnglish);
982
content = nlsResult.code;
983
if (nlsResult.edits.length > 0) {
984
nlsEdits.set(file.path, { preNLSCode, edits: nlsResult.edits });
985
}
986
}
987
988
// Rewrite sourceMappingURL to CDN URL if configured
989
if (sourceMapBaseUrl) {
990
const relativePath = path.relative(path.join(REPO_ROOT, outDir), file.path);
991
content = content.replace(
992
/\/\/# sourceMappingURL=.+$/m,
993
`//# sourceMappingURL=${sourceMapBaseUrl}/${relativePath}.map`
994
);
995
content = content.replace(
996
/\/\*# sourceMappingURL=.+\*\/$/m,
997
`/*# sourceMappingURL=${sourceMapBaseUrl}/${relativePath}.map*/`
998
);
999
}
1000
1001
await fs.promises.writeFile(file.path, content);
1002
} else if (file.path.endsWith('.map')) {
1003
// Defer .map processing until all .js files have been handled
1004
deferredMaps.push({ path: file.path, text: file.text, contents: file.contents });
1005
} else {
1006
// Write other files (assets, etc.) as-is
1007
await fs.promises.writeFile(file.path, file.contents);
1008
}
1009
}
1010
bundled++;
1011
}
1012
1013
// Second pass: process deferred .map files now that all mangle/NLS edits
1014
// have been collected from .js processing above.
1015
for (const mapFile of deferredMaps) {
1016
const jsPath = mapFile.path.replace(/\.map$/, '');
1017
const mangle = mangleEdits.get(jsPath);
1018
const nls = nlsEdits.get(jsPath);
1019
1020
if (mangle || nls) {
1021
let mapJson = JSON.parse(mapFile.text);
1022
if (mangle) {
1023
mapJson = adjustSourceMap(mapJson, mangle.preMangleCode, mangle.edits);
1024
}
1025
if (nls) {
1026
mapJson = adjustSourceMap(mapJson, nls.preNLSCode, nls.edits);
1027
}
1028
await fs.promises.writeFile(mapFile.path, JSON.stringify(mapJson));
1029
} else {
1030
await fs.promises.writeFile(mapFile.path, mapFile.contents);
1031
}
1032
}
1033
1034
// Syntax-check JS files that were post-processed (mangle-privates, NLS).
1035
// These steps do raw string surgery on bundled JS so a bug could silently
1036
// produce syntactically broken output. Catch it here at build time.
1037
// Uses esbuild.transform() as a parser since the bundles are ESM.
1038
const postProcessedFiles = new Set([...mangleEdits.keys(), ...nlsEdits.keys()]);
1039
if (postProcessedFiles.size > 0) {
1040
const errors = (await Promise.all([...postProcessedFiles].map(async jsPath => {
1041
try {
1042
const src = await fs.promises.readFile(jsPath, 'utf-8');
1043
await esbuild.transform(src, { loader: 'js', format: 'esm' });
1044
return undefined;
1045
} catch (e: unknown) {
1046
const rel = path.relative(path.join(REPO_ROOT, outDir), jsPath);
1047
const message = e instanceof Error ? e.message : String(e);
1048
return { rel, message };
1049
}
1050
}))).filter(error => error !== undefined).sort((a, b) => a.rel.localeCompare(b.rel));
1051
if (errors.length > 0) {
1052
throw new Error(`[bundle] Syntax errors in post-processed JS files:\n${errors.map(e => `${e.rel}: ${e.message}`).join('\n')}`);
1053
}
1054
console.log(`[bundle] Syntax check passed for ${postProcessedFiles.size} post-processed JS files`);
1055
}
1056
1057
// Log mangle-privates stats
1058
if (doManglePrivates && mangleStats.length > 0) {
1059
let totalClasses = 0, totalFields = 0, totalEdits = 0, totalElapsed = 0;
1060
for (const { file, result } of mangleStats) {
1061
console.log(`[mangle-privates] ${file}: ${result.classCount} classes, ${result.fieldCount} fields, ${result.editCount} edits, ${result.elapsed}ms`);
1062
totalClasses += result.classCount;
1063
totalFields += result.fieldCount;
1064
totalEdits += result.editCount;
1065
totalElapsed += result.elapsed;
1066
}
1067
console.log(`[mangle-privates] Total: ${totalClasses} classes, ${totalFields} fields, ${totalEdits} edits, ${totalElapsed}ms`);
1068
}
1069
1070
// Copy resources (curated per-target patterns for production)
1071
await copyResources(outDir, target);
1072
1073
// Compile standalone TypeScript files (like Electron preload scripts) that cannot be bundled
1074
await compileStandaloneFiles(outDir, doMinify, target);
1075
1076
console.log(`[bundle] Done in ${Date.now() - t1}ms (${bundled} bundles)`);
1077
}
1078
1079
// ============================================================================
1080
// Watch Mode
1081
// ============================================================================
1082
1083
async function watch(): Promise<void> {
1084
if (!useEsbuildTranspile) {
1085
console.log('Starting transpilation...');
1086
console.log('Finished transpilation with 0 errors after 0 ms');
1087
console.log('[watch] esbuild transpile disabled (useEsbuildTranspile=false). Keeping process alive as no-op.');
1088
await new Promise(() => { }); // keep alive
1089
return;
1090
}
1091
1092
console.log('Starting transpilation...');
1093
1094
const outDir = OUT_DIR;
1095
1096
// Initial setup
1097
await cleanDir(outDir);
1098
console.log(`[transpile] ${SRC_DIR} → ${outDir}`);
1099
1100
// Initial full build
1101
const t1 = Date.now();
1102
try {
1103
await transpile(outDir, false);
1104
await copyAllNonTsFiles(outDir, false);
1105
console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`);
1106
} catch (err) {
1107
console.error('[watch] Initial build failed:', err);
1108
console.log(`Finished transpilation with 1 errors after ${Date.now() - t1} ms`);
1109
// Continue watching anyway
1110
}
1111
1112
let pendingTsFiles: Set<string> = new Set();
1113
let pendingCopyFiles: Set<string> = new Set();
1114
1115
const processChanges = async () => {
1116
console.log('Starting transpilation...');
1117
const t1 = Date.now();
1118
const tsFiles = [...pendingTsFiles];
1119
const filesToCopy = [...pendingCopyFiles];
1120
pendingTsFiles = new Set();
1121
pendingCopyFiles = new Set();
1122
1123
try {
1124
// Transform changed TypeScript files in parallel
1125
if (tsFiles.length > 0) {
1126
console.log(`[watch] Transpiling ${tsFiles.length} file(s)...`);
1127
await Promise.all(tsFiles.map(srcPath => {
1128
const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath);
1129
const destPath = path.join(REPO_ROOT, outDir, relativePath.replace(/\.ts$/, '.js'));
1130
return transpileFile(srcPath, destPath);
1131
}));
1132
}
1133
1134
// Copy changed resource files in parallel
1135
if (filesToCopy.length > 0) {
1136
await Promise.all(filesToCopy.map(async (srcPath) => {
1137
const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath);
1138
const destPath = path.join(REPO_ROOT, outDir, relativePath);
1139
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
1140
await fs.promises.copyFile(srcPath, destPath);
1141
console.log(`[watch] Copied ${relativePath}`);
1142
}));
1143
}
1144
1145
if (tsFiles.length > 0 || filesToCopy.length > 0) {
1146
console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`);
1147
}
1148
} catch (err) {
1149
console.error('[watch] Rebuild failed:', err);
1150
console.log(`Finished transpilation with 1 errors after ${Date.now() - t1} ms`);
1151
// Continue watching
1152
}
1153
};
1154
1155
// Watch src directory using existing gulp-watch based watcher
1156
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
1157
const srcDir = path.join(REPO_ROOT, SRC_DIR);
1158
const watchStream = gulpWatch('src/**', { base: srcDir, readDelay: 200 });
1159
1160
watchStream.on('data', (file: { path: string }) => {
1161
if (file.path.endsWith('.ts') && !file.path.endsWith('.d.ts')) {
1162
pendingTsFiles.add(file.path);
1163
} else {
1164
// Copy any non-TS file (matches old gulp build's `src/**` behavior)
1165
pendingCopyFiles.add(file.path);
1166
}
1167
1168
if (pendingTsFiles.size > 0 || pendingCopyFiles.size > 0) {
1169
clearTimeout(debounceTimer);
1170
debounceTimer = setTimeout(processChanges, 200);
1171
}
1172
});
1173
1174
console.log('[watch] Watching src/**/*.{ts,css,...} (Ctrl+C to stop)');
1175
1176
// Keep process alive
1177
process.on('SIGINT', () => {
1178
console.log('\n[watch] Stopping...');
1179
watchStream.end();
1180
process.exit(0);
1181
});
1182
}
1183
1184
// ============================================================================
1185
// Main
1186
// ============================================================================
1187
1188
function printUsage(): void {
1189
console.log(`Usage: npx tsx build/next/index.ts <command> [options]
1190
1191
Commands:
1192
transpile Transpile TypeScript to JavaScript (single-file, fast)
1193
bundle Bundle entry points into optimized bundles
1194
1195
Options for 'transpile':
1196
--watch Watch for changes and rebuild incrementally
1197
--out <dir> Output directory (default: out)
1198
--exclude-tests Exclude test files from transpilation
1199
1200
Options for 'bundle':
1201
--minify Minify the output bundles
1202
--nls Process NLS (localization) strings
1203
--mangle-privates Convert native #private fields to regular properties
1204
--out <dir> Output directory (default: out-vscode)
1205
--target <target> Build target: desktop (default), server, server-web, web
1206
--source-map-base-url <url> Rewrite sourceMappingURL to CDN URL
1207
1208
Examples:
1209
npx tsx build/next/index.ts transpile
1210
npx tsx build/next/index.ts transpile --watch
1211
npx tsx build/next/index.ts transpile --out out-build
1212
npx tsx build/next/index.ts transpile --out out-build --exclude-tests
1213
npx tsx build/next/index.ts bundle
1214
npx tsx build/next/index.ts bundle --minify --nls
1215
npx tsx build/next/index.ts bundle --nls --out out-vscode-min
1216
npx tsx build/next/index.ts bundle --minify --nls --target server --out out-vscode-reh-min
1217
npx tsx build/next/index.ts bundle --minify --nls --target server-web --out out-vscode-reh-web-min
1218
`);
1219
}
1220
1221
async function main(): Promise<void> {
1222
const t1 = Date.now();
1223
1224
try {
1225
switch (command) {
1226
case 'transpile':
1227
if (options.watch) {
1228
await watch();
1229
} else {
1230
const outDir = options.out ?? OUT_DIR;
1231
await cleanDir(outDir);
1232
1233
// Write build date file (used by packaging to embed in product.json)
1234
const outDirPath = path.join(REPO_ROOT, outDir);
1235
await fs.promises.mkdir(outDirPath, { recursive: true });
1236
await fs.promises.writeFile(path.join(outDirPath, 'date'), getGitCommitDate(), 'utf8');
1237
1238
console.log(`[transpile] ${SRC_DIR} → ${outDir}${options.excludeTests ? ' (excluding tests)' : ''}`);
1239
const t1 = Date.now();
1240
await transpile(outDir, options.excludeTests);
1241
await copyAllNonTsFiles(outDir, options.excludeTests);
1242
console.log(`[transpile] Done in ${Date.now() - t1}ms`);
1243
}
1244
break;
1245
1246
case 'bundle':
1247
await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.manglePrivates, options.target as BuildTarget, options.sourceMapBaseUrl);
1248
break;
1249
1250
default:
1251
printUsage();
1252
process.exit(command ? 1 : 0);
1253
}
1254
1255
if (!options.watch) {
1256
console.log(`\n✓ Total: ${Date.now() - t1}ms`);
1257
}
1258
} catch (err) {
1259
console.error('Build failed:', err);
1260
process.exit(1);
1261
}
1262
}
1263
1264
main();
1265
1266