From b08e9e5e4b63bb8c24d4f2e914884377104c0527 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 22 Jan 2026 08:45:58 +0100 Subject: [PATCH 01/53] vibe-wip --- build/esbuild.ts | 516 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 build/esbuild.ts diff --git a/build/esbuild.ts b/build/esbuild.ts new file mode 100644 index 0000000000000..de4743e688a14 --- /dev/null +++ b/build/esbuild.ts @@ -0,0 +1,516 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as esbuild from 'esbuild'; +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; +import glob from 'glob'; +import * as watcher from '@parcel/watcher'; + +const globAsync = promisify(glob); + +// ============================================================================ +// Configuration +// ============================================================================ + +const REPO_ROOT = path.dirname(import.meta.dirname); +const isWatch = process.argv.includes('--watch'); +const isBundle = process.argv.includes('--bundle'); +const isMinify = process.argv.includes('--minify'); + +const SRC_DIR = 'src'; +const OUT_DIR = 'out'; +const OUT_BUILD_DIR = 'out-build'; +const OUT_VSCODE_DIR = 'out-vscode'; + +// ============================================================================ +// Entry Points (from build/buildfile.ts) +// ============================================================================ + +const workerEntryPoints = [ + 'vs/editor/common/services/editorWebWorkerMain', + 'vs/workbench/api/worker/extensionHostWorkerMain', + 'vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain', + 'vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain', + 'vs/workbench/services/search/worker/localFileSearchMain', + 'vs/platform/profiling/electron-browser/profileAnalysisWorkerMain', + 'vs/workbench/contrib/output/common/outputLinkComputerMain', + 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain', +]; + +const desktopEntryPoints = [ + 'vs/workbench/workbench.desktop.main', + 'vs/workbench/contrib/debug/node/telemetryApp', + 'vs/platform/files/node/watcher/watcherMain', + 'vs/platform/terminal/node/ptyHostMain', + 'vs/workbench/api/node/extensionHostProcess', +]; + +const codeEntryPoints = [ + 'vs/code/node/cliProcessMain', + 'vs/code/electron-utility/sharedProcess/sharedProcessMain', + 'vs/code/electron-browser/workbench/workbench', +]; + +const webEntryPoints = [ + 'vs/workbench/workbench.web.main.internal', + 'vs/code/browser/workbench/workbench', +]; + +const keyboardMapEntryPoints = [ + 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux', + 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin', + 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win', +]; + +const bootstrapEntryPoints = [ + 'main', + 'cli', + 'bootstrap-fork', + 'server-main', + 'server-cli', +]; + +// ============================================================================ +// Resource Patterns (files to copy, not transpile/bundle) +// ============================================================================ + +const resourcePatterns = [ + // HTML + 'vs/code/electron-browser/workbench/workbench.html', + 'vs/code/electron-browser/workbench/workbench-dev.html', + 'vs/code/browser/workbench/workbench.html', + 'vs/code/browser/workbench/workbench-dev.html', + 'vs/code/browser/workbench/callback.html', + 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + 'vs/workbench/contrib/webview/browser/pre/*.html', + + // Vendor JavaScript libraries (not transpiled) + 'vs/base/common/marked/marked.js', + 'vs/base/common/semver/semver.js', + 'vs/base/browser/dompurify/dompurify.js', + + // Electron preload (not bundled) + 'vs/base/parts/sandbox/electron-browser/preload.js', + 'vs/base/parts/sandbox/electron-browser/preload-aux.js', + + // Webview pre scripts + 'vs/workbench/contrib/webview/browser/pre/*.js', + + // Shell scripts + 'vs/base/node/*.sh', + 'vs/workbench/contrib/terminal/common/scripts/**/*.sh', + 'vs/workbench/contrib/terminal/common/scripts/**/*.ps1', + 'vs/workbench/contrib/terminal/common/scripts/**/*.psm1', + 'vs/workbench/contrib/terminal/common/scripts/**/*.fish', + 'vs/workbench/contrib/terminal/common/scripts/**/*.zsh', + 'vs/workbench/contrib/externalTerminal/**/*.scpt', + + // Media - audio + 'vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Media - images + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', + 'vs/workbench/contrib/extensions/browser/media/*.svg', + 'vs/workbench/contrib/extensions/browser/media/*.png', + 'vs/workbench/services/extensionManagement/common/media/*.svg', + 'vs/workbench/services/extensionManagement/common/media/*.png', + 'vs/workbench/browser/parts/editor/media/*.png', + 'vs/workbench/contrib/debug/browser/media/*.png', + + // Tree-sitter queries + 'vs/editor/common/languages/highlights/*.scm', + 'vs/editor/common/languages/injections/*.scm', +]; + +// ============================================================================ +// Utilities +// ============================================================================ + +async function cleanDir(dir: string): Promise { + const fullPath = path.join(REPO_ROOT, dir); + console.log(`[clean] ${dir}`); + await fs.promises.rm(fullPath, { recursive: true, force: true }); + await fs.promises.mkdir(fullPath, { recursive: true }); +} + +async function copyCssFiles(outDir: string): Promise { + // Copy all CSS files from src to output (they're imported by JS) + const cssFiles = await globAsync('**/*.css', { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ['**/test/**'], + }); + + for (const file of cssFiles) { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file); + + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + await fs.promises.copyFile(srcPath, destPath); + } + + return cssFiles.length; +} + +async function copyResources(outDir: string, excludeDevFiles = false): Promise { + console.log(`[resources] Copying to ${outDir}...`); + let copied = 0; + + const ignorePatterns = ['**/test/**']; + if (excludeDevFiles) { + ignorePatterns.push('**/*-dev.html'); + } + + for (const pattern of resourcePatterns) { + const files = await globAsync(pattern, { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ignorePatterns, + }); + + for (const file of files) { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file); + + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + await fs.promises.copyFile(srcPath, destPath); + copied++; + } + } + + // Copy CSS files + const cssCount = await copyCssFiles(outDir); + copied += cssCount; + + console.log(`[resources] Copied ${copied} files (${cssCount} CSS)`); +} + +// ============================================================================ +// TS Boilerplate Removal (logic from build/lib/bundle.ts) +// ============================================================================ + +const BOILERPLATE = [ + { start: /^var __extends/, end: /^}\)\(\);$/ }, + { start: /^var __assign/, end: /^};$/ }, + { start: /^var __decorate/, end: /^};$/ }, + { start: /^var __metadata/, end: /^};$/ }, + { start: /^var __param/, end: /^};$/ }, + { start: /^var __awaiter/, end: /^};$/ }, + { start: /^var __generator/, end: /^};$/ }, + { start: /^var __createBinding/, end: /^}\)\);$/ }, + { start: /^var __setModuleDefault/, end: /^}\);$/ }, + { start: /^var __importStar/, end: /^};$/ }, + { start: /^var __addDisposableResource/, end: /^};$/ }, + { start: /^var __disposeResources/, end: /^}\);$/ }, +]; + +function removeAllTSBoilerplate(source: string): string { + const seen = new Array(BOILERPLATE.length).fill(true); + const lines = source.split(/\r\n|\n|\r/); + const newLines: string[] = []; + let isRemoving = false; + let endPattern: RegExp | undefined; + + for (const line of lines) { + if (isRemoving) { + newLines.push(''); + if (endPattern!.test(line)) { + isRemoving = false; + } + } else { + let shouldRemove = false; + for (let j = 0; j < BOILERPLATE.length; j++) { + if (BOILERPLATE[j].start.test(line) && seen[j]) { + shouldRemove = true; + isRemoving = true; + endPattern = BOILERPLATE[j].end; + break; + } + } + newLines.push(shouldRemove ? '' : line); + } + } + return newLines.join('\n'); +} + +// ============================================================================ +// Plugins +// ============================================================================ + +function tsBoilerplatePlugin(): esbuild.Plugin { + return { + name: 'ts-boilerplate-removal', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + const contents = await fs.promises.readFile(args.path, 'utf-8'); + return { contents: removeAllTSBoilerplate(contents) }; + }); + }, + }; +} + +function inlineMinimistPlugin(): esbuild.Plugin { + return { + name: 'inline-minimist', + setup(build) { + build.onResolve({ filter: /^minimist$/ }, () => ({ + path: path.join(REPO_ROOT, 'node_modules/minimist/index.js'), + external: false, + })); + }, + }; +} + +function cssExternalPlugin(): esbuild.Plugin { + // Mark CSS imports as external so they stay as import statements + // The CSS files are copied separately and loaded by the browser at runtime + return { + name: 'css-external', + setup(build) { + build.onResolve({ filter: /\.css$/ }, (args) => ({ + path: args.path, + external: true, + })); + }, + }; +} + +// ============================================================================ +// Transpile (Goal 1: TS → JS) +// ============================================================================ + +async function transpile(): Promise { + const outDir = isBundle ? OUT_BUILD_DIR : OUT_DIR; + + await cleanDir(outDir); + + console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + const t1 = Date.now(); + + // Find all .ts files + const files = await globAsync('**/*.ts', { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ['**/test/**', '**/*.d.ts'], + }); + + // Transpile with esbuild + await esbuild.build({ + entryPoints: files.map(f => path.join(REPO_ROOT, SRC_DIR, f)), + outdir: path.join(REPO_ROOT, outDir), + outbase: path.join(REPO_ROOT, SRC_DIR), + format: 'esm', + target: ['es2024'], + platform: 'neutral', + sourcemap: isBundle ? 'external' : 'inline', + sourcesContent: false, + bundle: false, + logLevel: 'warning', + tsconfigRaw: JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + useDefineForClassFields: false, + } + }), + }); + + // Copy resources + await copyResources(outDir); + + console.log(`[transpile] Done in ${Date.now() - t1}ms (${files.length} files)`); +} + +// ============================================================================ +// Bundle (Goal 2: JS → bundled JS) +// ============================================================================ + +async function bundle(): Promise { + await cleanDir(OUT_VSCODE_DIR); + + console.log(`[bundle] ${OUT_BUILD_DIR} → ${OUT_VSCODE_DIR}`); + const t1 = Date.now(); + + // Read TSLib for banner + const tslibPath = path.join(REPO_ROOT, 'node_modules/tslib/tslib.es6.js'); + const tslib = await fs.promises.readFile(tslibPath, 'utf-8'); + const banner = { + js: `/*!-------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +${tslib}`, + css: `/*!-------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/`, + }; + + // All entry points to bundle + const allEntryPoints = [ + ...workerEntryPoints, + ...desktopEntryPoints, + ...codeEntryPoints, + ...webEntryPoints, + ...keyboardMapEntryPoints, + ]; + + // Bundle each entry point + let bundled = 0; + await Promise.all(allEntryPoints.map(async (entryPoint) => { + const entryPath = path.join(REPO_ROOT, OUT_BUILD_DIR, `${entryPoint}.js`); + const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entryPoint}.js`); + + // Ensure output directory exists + await fs.promises.mkdir(path.dirname(outPath), { recursive: true }); + + await esbuild.build({ + entryPoints: [entryPath], + outfile: outPath, + bundle: true, + format: 'esm', + platform: 'neutral', + target: ['es2024'], + packages: 'external', + sourcemap: 'external', + sourcesContent: false, + minify: isMinify, + treeShaking: true, + banner, + loader: { + '.ttf': 'file', + '.svg': 'file', + '.png': 'file', + '.sh': 'file', + }, + assetNames: 'media/[name]', + plugins: [cssExternalPlugin()], + logLevel: 'warning', + }); + bundled++; + })); + + // Bundle bootstrap files (with minimist inlined) + for (const entry of bootstrapEntryPoints) { + const entryPath = path.join(REPO_ROOT, OUT_BUILD_DIR, `${entry}.js`); + if (!fs.existsSync(entryPath)) { + console.log(`[bundle] Skipping ${entry} (not found)`); + continue; + } + + const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entry}.js`); + + await esbuild.build({ + entryPoints: [entryPath], + outfile: outPath, + bundle: true, + format: 'esm', + platform: 'node', + target: ['es2024'], + packages: 'external', + sourcemap: 'external', + sourcesContent: false, + minify: isMinify, + treeShaking: true, + banner, + plugins: [inlineMinimistPlugin()], + logLevel: 'warning', + }); + bundled++; + } + + // Copy resources (exclude dev files for production) + await copyResources(OUT_VSCODE_DIR, true); + + console.log(`[bundle] Done in ${Date.now() - t1}ms (${bundled} bundles)`); +} + +// ============================================================================ +// Watch Mode +// ============================================================================ + +async function watch(): Promise { + console.log('[watch] Starting...'); + + // Initial transpile + try { + await transpile(); + } catch (err) { + console.error('[watch] Initial build failed:', err); + // Continue watching anyway + } + + let debounce: NodeJS.Timeout | undefined; + + const rebuild = () => { + if (debounce) { + clearTimeout(debounce); + } + debounce = setTimeout(async () => { + console.log('[watch] Rebuilding...'); + const t1 = Date.now(); + try { + await transpile(); + console.log(`[watch] Rebuilt in ${Date.now() - t1}ms`); + } catch (err) { + console.error('[watch] Rebuild failed:', err); + // Continue watching + } + }, 100); + }; + + // Watch src directory + const subscription = await watcher.subscribe( + path.join(REPO_ROOT, SRC_DIR), + (err, events) => { + if (err) { + console.error('[watch] Watcher error:', err); + return; + } + const relevantEvents = events.filter(e => + e.path.endsWith('.ts') && !e.path.includes('/test/') + ); + if (relevantEvents.length > 0) { + console.log(`[watch] ${relevantEvents.length} file(s) changed`); + rebuild(); + } + }, + { ignore: ['**/test/**', '**/node_modules/**'] } + ); + + console.log('[watch] Watching src/**/*.ts (Ctrl+C to stop)'); + + // Keep process alive + process.on('SIGINT', async () => { + console.log('\n[watch] Stopping...'); + await subscription.unsubscribe(); + process.exit(0); + }); +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main(): Promise { + const t1 = Date.now(); + + try { + if (isWatch) { + await watch(); + } else if (isBundle) { + await transpile(); + await bundle(); + } else { + await transpile(); + } + + if (!isWatch) { + console.log(`\n✓ Total: ${Date.now() - t1}ms`); + } + } catch (err) { + console.error('Build failed:', err); + process.exit(1); + } +} + +main(); From d6a69b96a2f6371621604115130dd8dc7236793a Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 22 Jan 2026 09:16:13 +0100 Subject: [PATCH 02/53] vibe-wip --- build/esbuild.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/esbuild.ts b/build/esbuild.ts index de4743e688a14..a8b6f2e36c3b9 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -88,6 +88,9 @@ const resourcePatterns = [ 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', 'vs/workbench/contrib/webview/browser/pre/*.html', + // Fonts + 'vs/base/browser/ui/codicons/codicon/codicon.ttf', + // Vendor JavaScript libraries (not transpiled) 'vs/base/common/marked/marked.js', 'vs/base/common/semver/semver.js', From 7896df9981cc361135fa6ec50cec40fbd08593fc Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 22 Jan 2026 09:57:23 +0100 Subject: [PATCH 03/53] ts boilerplate cleanup not needed --- build/esbuild.ts | 60 ------------------------------------------------ 1 file changed, 60 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index a8b6f2e36c3b9..5e94612f53854 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -191,70 +191,10 @@ async function copyResources(outDir: string, excludeDevFiles = false): Promise(BOILERPLATE.length).fill(true); - const lines = source.split(/\r\n|\n|\r/); - const newLines: string[] = []; - let isRemoving = false; - let endPattern: RegExp | undefined; - - for (const line of lines) { - if (isRemoving) { - newLines.push(''); - if (endPattern!.test(line)) { - isRemoving = false; - } - } else { - let shouldRemove = false; - for (let j = 0; j < BOILERPLATE.length; j++) { - if (BOILERPLATE[j].start.test(line) && seen[j]) { - shouldRemove = true; - isRemoving = true; - endPattern = BOILERPLATE[j].end; - break; - } - } - newLines.push(shouldRemove ? '' : line); - } - } - return newLines.join('\n'); -} - // ============================================================================ // Plugins // ============================================================================ -function tsBoilerplatePlugin(): esbuild.Plugin { - return { - name: 'ts-boilerplate-removal', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async (args) => { - const contents = await fs.promises.readFile(args.path, 'utf-8'); - return { contents: removeAllTSBoilerplate(contents) }; - }); - }, - }; -} - function inlineMinimistPlugin(): esbuild.Plugin { return { name: 'inline-minimist', From b4d5f56debee316df1f47b01173d20f0965f4107 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 22 Jan 2026 15:05:41 +0100 Subject: [PATCH 04/53] Fix esbuild resource copying for development and test builds - Include workbench-dev.html files for development mode (needed when VSCODE_DEV is set) - Add codicon.ttf font to resource patterns - Include test files and CSS in development builds (exclude only for production bundles) - Add comprehensive test fixture patterns for unit tests (json, txt, snap, tst, html, js, jxs, tsx, png, md, zip, pdf, qwoff, wuff, less, and extensionless executables) - Add excludeTests parameter to copyCssFiles and copyResources functions - Production bundles (--bundle) still exclude tests and dev files --- build/esbuild.ts | 69 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index 5e94612f53854..0a748b5b2d4a7 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -130,6 +130,27 @@ const resourcePatterns = [ 'vs/editor/common/languages/injections/*.scm', ]; +// Test fixtures (only copied for development builds, not production) +const testFixturePatterns = [ + '**/test/**/*.json', + '**/test/**/*.txt', + '**/test/**/*.snap', + '**/test/**/*.tst', + '**/test/**/*.html', + '**/test/**/*.js', + '**/test/**/*.jxs', + '**/test/**/*.tsx', + '**/test/**/*.png', + '**/test/**/*.md', + '**/test/**/*.zip', + '**/test/**/*.pdf', + '**/test/**/*.qwoff', + '**/test/**/*.wuff', + '**/test/**/*.less', + // Files without extensions (executables, etc.) + '**/test/**/fixtures/executable/*', +]; + // ============================================================================ // Utilities // ============================================================================ @@ -141,11 +162,11 @@ async function cleanDir(dir: string): Promise { await fs.promises.mkdir(fullPath, { recursive: true }); } -async function copyCssFiles(outDir: string): Promise { +async function copyCssFiles(outDir: string, excludeTests = false): Promise { // Copy all CSS files from src to output (they're imported by JS) const cssFiles = await globAsync('**/*.css', { cwd: path.join(REPO_ROOT, SRC_DIR), - ignore: ['**/test/**'], + ignore: excludeTests ? ['**/test/**'] : [], }); for (const file of cssFiles) { @@ -159,11 +180,14 @@ async function copyCssFiles(outDir: string): Promise { return cssFiles.length; } -async function copyResources(outDir: string, excludeDevFiles = false): Promise { +async function copyResources(outDir: string, excludeDevFiles = false, excludeTests = false): Promise { console.log(`[resources] Copying to ${outDir}...`); let copied = 0; - const ignorePatterns = ['**/test/**']; + const ignorePatterns: string[] = []; + if (excludeTests) { + ignorePatterns.push('**/test/**'); + } if (excludeDevFiles) { ignorePatterns.push('**/*-dev.html'); } @@ -184,8 +208,26 @@ async function copyResources(outDir: string, excludeDevFiles = false): Promise { console.log(`[transpile] ${SRC_DIR} → ${outDir}`); const t1 = Date.now(); - // Find all .ts files + // Find all .ts files (exclude tests only when bundling for production) + const ignorePatterns = ['**/*.d.ts']; + if (isBundle) { + ignorePatterns.push('**/test/**'); + } + const files = await globAsync('**/*.ts', { cwd: path.join(REPO_ROOT, SRC_DIR), - ignore: ['**/test/**', '**/*.d.ts'], + ignore: ignorePatterns, }); // Transpile with esbuild @@ -259,8 +306,8 @@ async function transpile(): Promise { }), }); - // Copy resources - await copyResources(outDir); + // Copy resources (exclude tests only when bundling for production) + await copyResources(outDir, false, isBundle); console.log(`[transpile] Done in ${Date.now() - t1}ms (${files.length} files)`); } @@ -361,8 +408,8 @@ ${tslib}`, bundled++; } - // Copy resources (exclude dev files for production) - await copyResources(OUT_VSCODE_DIR, true); + // Copy resources (exclude dev files and tests for production) + await copyResources(OUT_VSCODE_DIR, true, true); console.log(`[bundle] Done in ${Date.now() - t1}ms (${bundled} bundles)`); } From 07eb6b45d619487bcb7e8ca1d2d934337bc4533f Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 23 Jan 2026 14:07:31 +0100 Subject: [PATCH 05/53] fix: suppress log warnings for unsupported require calls in esbuild configuration --- build/esbuild.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/esbuild.ts b/build/esbuild.ts index 0a748b5b2d4a7..846c97010d40c 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -304,6 +304,7 @@ async function transpile(): Promise { useDefineForClassFields: false, } }), + logOverride: { 'unsupported-require-call': 'silent' }, }); // Copy resources (exclude tests only when bundling for production) @@ -375,6 +376,7 @@ ${tslib}`, assetNames: 'media/[name]', plugins: [cssExternalPlugin()], logLevel: 'warning', + logOverride: { 'unsupported-require-call': 'silent' }, }); bundled++; })); @@ -404,6 +406,7 @@ ${tslib}`, banner, plugins: [inlineMinimistPlugin()], logLevel: 'warning', + logOverride: { 'unsupported-require-call': 'silent' }, }); bundled++; } From b14c8c9752fe07d2da2a8617327a543daf053e61 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 23 Jan 2026 14:28:14 +0100 Subject: [PATCH 06/53] fix: remove logOverride for unsupported require calls in esbuild configuration --- build/esbuild.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index 846c97010d40c..0a748b5b2d4a7 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -304,7 +304,6 @@ async function transpile(): Promise { useDefineForClassFields: false, } }), - logOverride: { 'unsupported-require-call': 'silent' }, }); // Copy resources (exclude tests only when bundling for production) @@ -376,7 +375,6 @@ ${tslib}`, assetNames: 'media/[name]', plugins: [cssExternalPlugin()], logLevel: 'warning', - logOverride: { 'unsupported-require-call': 'silent' }, }); bundled++; })); @@ -406,7 +404,6 @@ ${tslib}`, banner, plugins: [inlineMinimistPlugin()], logLevel: 'warning', - logOverride: { 'unsupported-require-call': 'silent' }, }); bundled++; } From 3138cd62128b664651b497d7645d7ac562f0d0c3 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 23 Jan 2026 14:53:39 +0100 Subject: [PATCH 07/53] refactor: improve transpile process and add incremental build support --- build/esbuild.ts | 112 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index 0a748b5b2d4a7..f4e6802222071 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -267,17 +267,10 @@ function cssExternalPlugin(): esbuild.Plugin { // Transpile (Goal 1: TS → JS) // ============================================================================ -async function transpile(): Promise { - const outDir = isBundle ? OUT_BUILD_DIR : OUT_DIR; - - await cleanDir(outDir); - - console.log(`[transpile] ${SRC_DIR} → ${outDir}`); - const t1 = Date.now(); - - // Find all .ts files (exclude tests only when bundling for production) +async function createTranspileContext(outDir: string, excludeTests: boolean): Promise { + // Find all .ts files const ignorePatterns = ['**/*.d.ts']; - if (isBundle) { + if (excludeTests) { ignorePatterns.push('**/test/**'); } @@ -286,8 +279,9 @@ async function transpile(): Promise { ignore: ignorePatterns, }); - // Transpile with esbuild - await esbuild.build({ + console.log(`[transpile] Found ${files.length} files`); + + return esbuild.context({ entryPoints: files.map(f => path.join(REPO_ROOT, SRC_DIR, f)), outdir: path.join(REPO_ROOT, outDir), outbase: path.join(REPO_ROOT, SRC_DIR), @@ -298,18 +292,35 @@ async function transpile(): Promise { sourcesContent: false, bundle: false, logLevel: 'warning', + logOverride: { + 'unsupported-require-call': 'silent', + }, tsconfigRaw: JSON.stringify({ compilerOptions: { experimentalDecorators: true, - useDefineForClassFields: false, + useDefineForClassFields: false } }), }); +} + +async function transpile(): Promise { + const outDir = isBundle ? OUT_BUILD_DIR : OUT_DIR; + + await cleanDir(outDir); + + console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + const t1 = Date.now(); + + // Create context and build + const ctx = await createTranspileContext(outDir, isBundle); + await ctx.rebuild(); + await ctx.dispose(); // Copy resources (exclude tests only when bundling for production) await copyResources(outDir, false, isBundle); - console.log(`[transpile] Done in ${Date.now() - t1}ms (${files.length} files)`); + console.log(`[transpile] Done in ${Date.now() - t1}ms`); } // ============================================================================ @@ -421,26 +432,60 @@ ${tslib}`, async function watch(): Promise { console.log('[watch] Starting...'); - // Initial transpile + const outDir = OUT_DIR; + + // Initial setup + await cleanDir(outDir); + console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + + // Create context for incremental builds + const ctx = await createTranspileContext(outDir, false); + + // Initial build + const t1 = Date.now(); try { - await transpile(); + await ctx.rebuild(); + await copyResources(outDir, false, false); + console.log(`[transpile] Done in ${Date.now() - t1}ms`); } catch (err) { console.error('[watch] Initial build failed:', err); // Continue watching anyway } let debounce: NodeJS.Timeout | undefined; + let pendingTsRebuild = false; + let pendingCopyFiles: string[] = []; - const rebuild = () => { + const processChanges = () => { if (debounce) { clearTimeout(debounce); } debounce = setTimeout(async () => { - console.log('[watch] Rebuilding...'); const t1 = Date.now(); + const hasTsChanges = pendingTsRebuild; + const filesToCopy = [...pendingCopyFiles]; + pendingTsRebuild = false; + pendingCopyFiles = []; + try { - await transpile(); - console.log(`[watch] Rebuilt in ${Date.now() - t1}ms`); + if (hasTsChanges) { + console.log('[watch] Rebuilding TypeScript...'); + await ctx.cancel(); + await ctx.rebuild(); + } + + // Copy changed resource files + for (const srcPath of filesToCopy) { + const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath); + const destPath = path.join(REPO_ROOT, outDir, relativePath); + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + await fs.promises.copyFile(srcPath, destPath); + console.log(`[watch] Copied ${relativePath}`); + } + + if (hasTsChanges || filesToCopy.length > 0) { + console.log(`[watch] Done in ${Date.now() - t1}ms`); + } } catch (err) { console.error('[watch] Rebuild failed:', err); // Continue watching @@ -448,6 +493,9 @@ async function watch(): Promise { }, 100); }; + // Extensions to watch and copy (non-TypeScript resources) + const copyExtensions = ['.css', '.html', '.js', '.json', '.ttf', '.svg', '.png', '.mp3', '.scm', '.sh', '.ps1', '.psm1', '.fish', '.zsh', '.scpt']; + // Watch src directory const subscription = await watcher.subscribe( path.join(REPO_ROOT, SRC_DIR), @@ -456,23 +504,33 @@ async function watch(): Promise { console.error('[watch] Watcher error:', err); return; } - const relevantEvents = events.filter(e => - e.path.endsWith('.ts') && !e.path.includes('/test/') - ); - if (relevantEvents.length > 0) { - console.log(`[watch] ${relevantEvents.length} file(s) changed`); - rebuild(); + + for (const event of events) { + if (event.path.includes('/test/')) { + continue; + } + + if (event.path.endsWith('.ts') && !event.path.endsWith('.d.ts')) { + pendingTsRebuild = true; + } else if (copyExtensions.some(ext => event.path.endsWith(ext))) { + pendingCopyFiles.push(event.path); + } + } + + if (pendingTsRebuild || pendingCopyFiles.length > 0) { + processChanges(); } }, { ignore: ['**/test/**', '**/node_modules/**'] } ); - console.log('[watch] Watching src/**/*.ts (Ctrl+C to stop)'); + console.log('[watch] Watching src/**/*.{ts,css,...} (Ctrl+C to stop)'); // Keep process alive process.on('SIGINT', async () => { console.log('\n[watch] Stopping...'); await subscription.unsubscribe(); + await ctx.dispose(); process.exit(0); }); } From e4c62aa6404cef18fcb820e26f0d4e2db64f02e1 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 30 Jan 2026 12:15:59 +0100 Subject: [PATCH 08/53] updates --- build/esbuild.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index f4e6802222071..841a16ae2bcc8 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -288,7 +288,8 @@ async function createTranspileContext(outDir: string, excludeTests: boolean): Pr format: 'esm', target: ['es2024'], platform: 'neutral', - sourcemap: isBundle ? 'external' : 'inline', + // sourcemap: isBundle ? 'external' : 'inline', + sourcemap: 'external', sourcesContent: false, bundle: false, logLevel: 'warning', @@ -470,7 +471,7 @@ async function watch(): Promise { try { if (hasTsChanges) { console.log('[watch] Rebuilding TypeScript...'); - await ctx.cancel(); + // await ctx.cancel(); await ctx.rebuild(); } From ddbeee8a573fb1f2e5727df9b51060553f948a3c Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 30 Jan 2026 15:47:33 +0100 Subject: [PATCH 09/53] feat: enhance TypeScript transpilation and bundling process with esbuild.transform --- build/esbuild.ts | 190 ++++++++++++++++++++++++----------------------- 1 file changed, 99 insertions(+), 91 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index 841a16ae2bcc8..3117fdd9eee6d 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -23,7 +23,6 @@ const isMinify = process.argv.includes('--minify'); const SRC_DIR = 'src'; const OUT_DIR = 'out'; -const OUT_BUILD_DIR = 'out-build'; const OUT_VSCODE_DIR = 'out-vscode'; // ============================================================================ @@ -140,6 +139,7 @@ const testFixturePatterns = [ '**/test/**/*.js', '**/test/**/*.jxs', '**/test/**/*.tsx', + '**/test/**/*.css', '**/test/**/*.png', '**/test/**/*.md', '**/test/**/*.zip', @@ -264,10 +264,36 @@ function cssExternalPlugin(): esbuild.Plugin { } // ============================================================================ -// Transpile (Goal 1: TS → JS) +// Transpile (Goal 1: TS → JS using esbuild.transform for maximum speed) // ============================================================================ -async function createTranspileContext(outDir: string, excludeTests: boolean): Promise { +// Shared transform options for single-file transpilation +const transformOptions: esbuild.TransformOptions = { + loader: 'ts', + format: 'esm', + target: 'es2024', + sourcemap: 'inline', + sourcesContent: false, + tsconfigRaw: JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + useDefineForClassFields: false + } + }), +}; + +async function transpileFile(srcPath: string, destPath: string, relativePath: string): Promise { + const source = await fs.promises.readFile(srcPath, 'utf-8'); + const result = await esbuild.transform(source, { + ...transformOptions, + sourcefile: relativePath, + }); + + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + await fs.promises.writeFile(destPath, result.code); +} + +async function transpile(outDir: string, excludeTests: boolean): Promise { // Find all .ts files const ignorePatterns = ['**/*.d.ts']; if (excludeTests) { @@ -281,47 +307,12 @@ async function createTranspileContext(outDir: string, excludeTests: boolean): Pr console.log(`[transpile] Found ${files.length} files`); - return esbuild.context({ - entryPoints: files.map(f => path.join(REPO_ROOT, SRC_DIR, f)), - outdir: path.join(REPO_ROOT, outDir), - outbase: path.join(REPO_ROOT, SRC_DIR), - format: 'esm', - target: ['es2024'], - platform: 'neutral', - // sourcemap: isBundle ? 'external' : 'inline', - sourcemap: 'external', - sourcesContent: false, - bundle: false, - logLevel: 'warning', - logOverride: { - 'unsupported-require-call': 'silent', - }, - tsconfigRaw: JSON.stringify({ - compilerOptions: { - experimentalDecorators: true, - useDefineForClassFields: false - } - }), - }); -} - -async function transpile(): Promise { - const outDir = isBundle ? OUT_BUILD_DIR : OUT_DIR; - - await cleanDir(outDir); - - console.log(`[transpile] ${SRC_DIR} → ${outDir}`); - const t1 = Date.now(); - - // Create context and build - const ctx = await createTranspileContext(outDir, isBundle); - await ctx.rebuild(); - await ctx.dispose(); - - // Copy resources (exclude tests only when bundling for production) - await copyResources(outDir, false, isBundle); - - console.log(`[transpile] Done in ${Date.now() - t1}ms`); + // Transpile all files in parallel using esbuild.transform (fastest approach) + await Promise.all(files.map(file => { + const srcPath = path.join(REPO_ROOT, SRC_DIR, file); + const destPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js')); + return transpileFile(srcPath, destPath, file); + })); } // ============================================================================ @@ -331,7 +322,7 @@ async function transpile(): Promise { async function bundle(): Promise { await cleanDir(OUT_VSCODE_DIR); - console.log(`[bundle] ${OUT_BUILD_DIR} → ${OUT_VSCODE_DIR}`); + console.log(`[bundle] ${SRC_DIR} → ${OUT_VSCODE_DIR}`); const t1 = Date.now(); // Read TSLib for banner @@ -347,6 +338,14 @@ ${tslib}`, *--------------------------------------------------------*/`, }; + // Shared TypeScript options for bundling directly from source + const tsconfigRaw = JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + useDefineForClassFields: false + } + }); + // All entry points to bundle const allEntryPoints = [ ...workerEntryPoints, @@ -356,10 +355,10 @@ ${tslib}`, ...keyboardMapEntryPoints, ]; - // Bundle each entry point + // Bundle each entry point directly from TypeScript source let bundled = 0; await Promise.all(allEntryPoints.map(async (entryPoint) => { - const entryPath = path.join(REPO_ROOT, OUT_BUILD_DIR, `${entryPoint}.js`); + const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`); const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entryPoint}.js`); // Ensure output directory exists @@ -387,13 +386,17 @@ ${tslib}`, assetNames: 'media/[name]', plugins: [cssExternalPlugin()], logLevel: 'warning', + logOverride: { + 'unsupported-require-call': 'silent', + }, + tsconfigRaw, }); bundled++; })); - // Bundle bootstrap files (with minimist inlined) + // Bundle bootstrap files (with minimist inlined) directly from TypeScript source for (const entry of bootstrapEntryPoints) { - const entryPath = path.join(REPO_ROOT, OUT_BUILD_DIR, `${entry}.js`); + const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entry}.ts`); if (!fs.existsSync(entryPath)) { console.log(`[bundle] Skipping ${entry} (not found)`); continue; @@ -416,6 +419,10 @@ ${tslib}`, banner, plugins: [inlineMinimistPlugin()], logLevel: 'warning', + logOverride: { + 'unsupported-require-call': 'silent', + }, + tsconfigRaw, }); bundled++; } @@ -439,13 +446,10 @@ async function watch(): Promise { await cleanDir(outDir); console.log(`[transpile] ${SRC_DIR} → ${outDir}`); - // Create context for incremental builds - const ctx = await createTranspileContext(outDir, false); - - // Initial build + // Initial full build const t1 = Date.now(); try { - await ctx.rebuild(); + await transpile(outDir, false); await copyResources(outDir, false, false); console.log(`[transpile] Done in ${Date.now() - t1}ms`); } catch (err) { @@ -453,45 +457,45 @@ async function watch(): Promise { // Continue watching anyway } - let debounce: NodeJS.Timeout | undefined; - let pendingTsRebuild = false; - let pendingCopyFiles: string[] = []; - - const processChanges = () => { - if (debounce) { - clearTimeout(debounce); - } - debounce = setTimeout(async () => { - const t1 = Date.now(); - const hasTsChanges = pendingTsRebuild; - const filesToCopy = [...pendingCopyFiles]; - pendingTsRebuild = false; - pendingCopyFiles = []; - - try { - if (hasTsChanges) { - console.log('[watch] Rebuilding TypeScript...'); - // await ctx.cancel(); - await ctx.rebuild(); - } + let pendingTsFiles: Set = new Set(); + let pendingCopyFiles: Set = new Set(); + + const processChanges = async () => { + const t1 = Date.now(); + const tsFiles = [...pendingTsFiles]; + const filesToCopy = [...pendingCopyFiles]; + pendingTsFiles = new Set(); + pendingCopyFiles = new Set(); + + try { + // Transform changed TypeScript files in parallel + if (tsFiles.length > 0) { + console.log(`[watch] Transpiling ${tsFiles.length} file(s)...`); + await Promise.all(tsFiles.map(srcPath => { + const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath); + const destPath = path.join(REPO_ROOT, outDir, relativePath.replace(/\.ts$/, '.js')); + return transpileFile(srcPath, destPath, relativePath); + })); + } - // Copy changed resource files - for (const srcPath of filesToCopy) { + // Copy changed resource files in parallel + if (filesToCopy.length > 0) { + await Promise.all(filesToCopy.map(async (srcPath) => { const relativePath = path.relative(path.join(REPO_ROOT, SRC_DIR), srcPath); const destPath = path.join(REPO_ROOT, outDir, relativePath); await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); await fs.promises.copyFile(srcPath, destPath); console.log(`[watch] Copied ${relativePath}`); - } + })); + } - if (hasTsChanges || filesToCopy.length > 0) { - console.log(`[watch] Done in ${Date.now() - t1}ms`); - } - } catch (err) { - console.error('[watch] Rebuild failed:', err); - // Continue watching + if (tsFiles.length > 0 || filesToCopy.length > 0) { + console.log(`[watch] Done in ${Date.now() - t1}ms`); } - }, 100); + } catch (err) { + console.error('[watch] Rebuild failed:', err); + // Continue watching + } }; // Extensions to watch and copy (non-TypeScript resources) @@ -512,13 +516,13 @@ async function watch(): Promise { } if (event.path.endsWith('.ts') && !event.path.endsWith('.d.ts')) { - pendingTsRebuild = true; + pendingTsFiles.add(event.path); } else if (copyExtensions.some(ext => event.path.endsWith(ext))) { - pendingCopyFiles.push(event.path); + pendingCopyFiles.add(event.path); } } - if (pendingTsRebuild || pendingCopyFiles.length > 0) { + if (pendingTsFiles.size > 0 || pendingCopyFiles.size > 0) { processChanges(); } }, @@ -531,7 +535,6 @@ async function watch(): Promise { process.on('SIGINT', async () => { console.log('\n[watch] Stopping...'); await subscription.unsubscribe(); - await ctx.dispose(); process.exit(0); }); } @@ -547,10 +550,15 @@ async function main(): Promise { if (isWatch) { await watch(); } else if (isBundle) { - await transpile(); await bundle(); } else { - await transpile(); + const outDir = OUT_DIR; + await cleanDir(outDir); + console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + const t1 = Date.now(); + await transpile(outDir, false); + await copyResources(outDir, false, false); + console.log(`[transpile] Done in ${Date.now() - t1}ms`); } if (!isWatch) { From 5aa871acfd96c992adaec5eed16516ee9f6f0499 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 30 Jan 2026 16:21:04 +0100 Subject: [PATCH 10/53] feat: add UTF-8 BOM handling for test files in copy process --- build/esbuild.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/build/esbuild.ts b/build/esbuild.ts index 3117fdd9eee6d..78e66b6847fc1 100644 --- a/build/esbuild.ts +++ b/build/esbuild.ts @@ -25,6 +25,9 @@ const SRC_DIR = 'src'; const OUT_DIR = 'out'; const OUT_VSCODE_DIR = 'out-vscode'; +// UTF-8 BOM - added to test files with 'utf8' in the path (matches gulp build behavior) +const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]); + // ============================================================================ // Entry Points (from build/buildfile.ts) // ============================================================================ @@ -162,6 +165,27 @@ async function cleanDir(dir: string): Promise { await fs.promises.mkdir(fullPath, { recursive: true }); } +/** + * Only used to make encoding tests happy. The source files don't have a BOM but the + * tests expect one... so we add it here. + */ +function needsBomAdded(filePath: string): boolean { + return /([\/\\])test\1.*utf8/.test(filePath); +} + +async function copyFile(srcPath: string, destPath: string): Promise { + await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); + + if (needsBomAdded(srcPath)) { + const content = await fs.promises.readFile(srcPath); + if (content[0] !== 0xef || content[1] !== 0xbb || content[2] !== 0xbf) { + await fs.promises.writeFile(destPath, Buffer.concat([UTF8_BOM, content])); + return; + } + } + await fs.promises.copyFile(srcPath, destPath); +} + async function copyCssFiles(outDir: string, excludeTests = false): Promise { // Copy all CSS files from src to output (they're imported by JS) const cssFiles = await globAsync('**/*.css', { @@ -173,8 +197,7 @@ async function copyCssFiles(outDir: string, excludeTests = false): Promise Date: Fri, 30 Jan 2026 18:23:41 +0100 Subject: [PATCH 11/53] feat(nls): add NLS plugin for esbuild and analysis utilities - Introduced a new NLS plugin for esbuild to handle localization strings. - Implemented NLSCollector for managing localization entries across builds. - Added functionality to analyze TypeScript files for localize() and localize2() calls. - Created utility functions for parsing localization keys and values. - Enhanced the patching process for JavaScript files to replace localization calls with indices. - Refactored existing code to utilize the new analysis and transformation utilities. --- build/{esbuild.ts => esbuild/index.ts} | 65 ++++- build/esbuild/nls-plugin.ts | 285 ++++++++++++++++++++++ build/lib/nls-analysis.ts | 317 +++++++++++++++++++++++++ build/lib/nls.ts | 277 +-------------------- 4 files changed, 663 insertions(+), 281 deletions(-) rename build/{esbuild.ts => esbuild/index.ts} (90%) create mode 100644 build/esbuild/nls-plugin.ts create mode 100644 build/lib/nls-analysis.ts diff --git a/build/esbuild.ts b/build/esbuild/index.ts similarity index 90% rename from build/esbuild.ts rename to build/esbuild/index.ts index 78e66b6847fc1..f9ff737e53ae5 100644 --- a/build/esbuild.ts +++ b/build/esbuild/index.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import { promisify } from 'util'; import glob from 'glob'; import * as watcher from '@parcel/watcher'; +import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts'; const globAsync = promisify(glob); @@ -16,7 +17,7 @@ const globAsync = promisify(glob); // Configuration // ============================================================================ -const REPO_ROOT = path.dirname(import.meta.dirname); +const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname)); const isWatch = process.argv.includes('--watch'); const isBundle = process.argv.includes('--bundle'); const isMinify = process.argv.includes('--minify'); @@ -367,6 +368,10 @@ ${tslib}`, } }); + // Create shared NLS collector + const nlsCollector = createNLSCollector(); + const preserveEnglish = false; // Production mode: replace messages with null + // All entry points to bundle const allEntryPoints = [ ...workerEntryPoints, @@ -376,16 +381,15 @@ ${tslib}`, ...keyboardMapEntryPoints, ]; + // Collect all build results (with write: false) + const buildResults: { outPath: string; result: esbuild.BuildResult }[] = []; + // Bundle each entry point directly from TypeScript source - let bundled = 0; await Promise.all(allEntryPoints.map(async (entryPoint) => { const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`); const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entryPoint}.js`); - // Ensure output directory exists - await fs.promises.mkdir(path.dirname(outPath), { recursive: true }); - - await esbuild.build({ + const result = await esbuild.build({ entryPoints: [entryPath], outfile: outPath, bundle: true, @@ -405,14 +409,22 @@ ${tslib}`, '.sh': 'file', }, assetNames: 'media/[name]', - plugins: [cssExternalPlugin()], + plugins: [ + nlsPlugin({ + baseDir: path.join(REPO_ROOT, SRC_DIR), + collector: nlsCollector, + }), + cssExternalPlugin() + ], + write: false, // Don't write yet, we need to post-process logLevel: 'warning', logOverride: { 'unsupported-require-call': 'silent', }, tsconfigRaw, }); - bundled++; + + buildResults.push({ outPath, result }); })); // Bundle bootstrap files (with minimist inlined) directly from TypeScript source @@ -425,7 +437,7 @@ ${tslib}`, const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entry}.js`); - await esbuild.build({ + const result = await esbuild.build({ entryPoints: [entryPath], outfile: outPath, bundle: true, @@ -438,13 +450,46 @@ ${tslib}`, minify: isMinify, treeShaking: true, banner, - plugins: [inlineMinimistPlugin()], + plugins: [ + nlsPlugin({ + baseDir: path.join(REPO_ROOT, SRC_DIR), + collector: nlsCollector, + }), + inlineMinimistPlugin() + ], + write: false, // Don't write yet, we need to post-process logLevel: 'warning', logOverride: { 'unsupported-require-call': 'silent', }, tsconfigRaw, }); + + buildResults.push({ outPath, result }); + } + + // Finalize NLS: sort entries, assign indices, write metadata files + const { indexMap } = await finalizeNLS(nlsCollector, path.join(REPO_ROOT, OUT_VSCODE_DIR)); + + // Post-process and write all output files + let bundled = 0; + for (const { result } of buildResults) { + if (!result.outputFiles) { + continue; + } + + for (const file of result.outputFiles) { + await fs.promises.mkdir(path.dirname(file.path), { recursive: true }); + + if (file.path.endsWith('.js') && indexMap.size > 0) { + // Post-process JS files to replace NLS placeholders with indices + const processed = postProcessNLS(file.text, indexMap, preserveEnglish); + await fs.promises.writeFile(file.path, processed); + } else { + // Write other files (source maps, etc.) as-is + await fs.promises.writeFile(file.path, file.contents); + } + } bundled++; } diff --git a/build/esbuild/nls-plugin.ts b/build/esbuild/nls-plugin.ts new file mode 100644 index 0000000000000..645d387dddee5 --- /dev/null +++ b/build/esbuild/nls-plugin.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as esbuild from 'esbuild'; +import * as path from 'path'; +import * as fs from 'fs'; +import { + TextModel, + analyzeLocalizeCalls, + parseLocalizeKeyOrValue +} from '../lib/nls-analysis.ts'; + +// ============================================================================ +// Types +// ============================================================================ + +interface NLSEntry { + moduleId: string; + key: string | { key: string; comment: string[] }; + message: string; + placeholder: string; +} + +export interface NLSPluginOptions { + /** + * Base path for computing module IDs (e.g., 'src') + */ + baseDir: string; + + /** + * Shared collector for NLS entries across multiple builds. + * Create with createNLSCollector() and pass to multiple plugin instances. + */ + collector: NLSCollector; +} + +/** + * Collector for NLS entries across multiple esbuild builds. + */ +export interface NLSCollector { + entries: Map; + add(entry: NLSEntry): void; +} + +/** + * Creates a shared NLS collector that can be passed to multiple plugin instances. + */ +export function createNLSCollector(): NLSCollector { + const entries = new Map(); + return { + entries, + add(entry: NLSEntry) { + entries.set(entry.placeholder, entry); + } + }; +} + +/** + * Finalizes NLS collection and writes output files. + * Call this after all esbuild builds have completed. + */ +export async function finalizeNLS( + collector: NLSCollector, + outDir: string +): Promise<{ indexMap: Map; messageCount: number }> { + if (collector.entries.size === 0) { + return { indexMap: new Map(), messageCount: 0 }; + } + + // Sort entries by moduleId, then by key for stable indices + const sortedEntries = [...collector.entries.values()].sort((a, b) => { + const aKey = typeof a.key === 'string' ? a.key : a.key.key; + const bKey = typeof b.key === 'string' ? b.key : b.key.key; + const moduleCompare = a.moduleId.localeCompare(b.moduleId); + if (moduleCompare !== 0) { + return moduleCompare; + } + return aKey.localeCompare(bKey); + }); + + // Create index map + const indexMap = new Map(); + sortedEntries.forEach((entry, idx) => { + indexMap.set(entry.placeholder, idx); + }); + + // Build NLS metadata + const allMessages: string[] = []; + const moduleToKeys: Map = new Map(); + const moduleToMessages: Map = new Map(); + + for (const entry of sortedEntries) { + allMessages.push(entry.message); + + if (!moduleToKeys.has(entry.moduleId)) { + moduleToKeys.set(entry.moduleId, []); + moduleToMessages.set(entry.moduleId, []); + } + moduleToKeys.get(entry.moduleId)!.push(entry.key); + moduleToMessages.get(entry.moduleId)!.push(entry.message); + } + + // nls.keys.json: [["moduleId", ["key1", "key2"]], ...] + const nlsKeysJson: [string, string[]][] = []; + for (const [moduleId, keys] of moduleToKeys) { + nlsKeysJson.push([moduleId, keys.map(k => typeof k === 'string' ? k : k.key)]); + } + + // nls.metadata.json: { keys: {...}, messages: {...} } + const nlsMetadataJson = { + keys: Object.fromEntries(moduleToKeys), + messages: Object.fromEntries(moduleToMessages) + }; + + // Write NLS files + await fs.promises.mkdir(outDir, { recursive: true }); + + await Promise.all([ + fs.promises.writeFile( + path.join(outDir, 'nls.messages.json'), + JSON.stringify(allMessages) + ), + fs.promises.writeFile( + path.join(outDir, 'nls.keys.json'), + JSON.stringify(nlsKeysJson) + ), + fs.promises.writeFile( + path.join(outDir, 'nls.metadata.json'), + JSON.stringify(nlsMetadataJson, null, '\t') + ), + fs.promises.writeFile( + path.join(outDir, 'nls.messages.js'), + `/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ +globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` + ) + ]); + + console.log(`[nls] Extracted ${allMessages.length} messages from ${moduleToKeys.size} modules`); + + return { indexMap, messageCount: allMessages.length }; +} + +/** + * Post-processes a JavaScript file to replace NLS placeholders with indices. + */ +export function postProcessNLS( + content: string, + indexMap: Map, + preserveEnglish: boolean +): string { + return replaceInOutput(content, indexMap, preserveEnglish); +} + +// ============================================================================ +// Transformation +// ============================================================================ + +function transformToPlaceholders( + source: string, + moduleId: string +): { code: string; entries: NLSEntry[] } { + const localizeCalls = analyzeLocalizeCalls(source, 'localize'); + const localize2Calls = analyzeLocalizeCalls(source, 'localize2'); + const allCalls = [...localizeCalls, ...localize2Calls].sort( + (a, b) => a.keySpan.start.line - b.keySpan.start.line || + a.keySpan.start.character - b.keySpan.start.character + ); + + if (allCalls.length === 0) { + return { code: source, entries: [] }; + } + + const entries: NLSEntry[] = []; + const model = new TextModel(source); + + // Process in reverse order to preserve positions + for (const call of allCalls.reverse()) { + const keyParsed = parseLocalizeKeyOrValue(call.key) as string | { key: string; comment: string[] }; + const messageParsed = parseLocalizeKeyOrValue(call.value); + const keyString = typeof keyParsed === 'string' ? keyParsed : keyParsed.key; + + // Create a deterministic placeholder + const placeholder = `%%NLS:${moduleId}#${keyString}%%`; + + entries.push({ + moduleId, + key: keyParsed, + message: String(messageParsed), + placeholder + }); + + // Replace the key with the placeholder string + model.apply(call.keySpan, `"${placeholder}"`); + } + + // Reverse entries to match source order + entries.reverse(); + + return { code: model.toString(), entries }; +} + +function replaceInOutput( + content: string, + indexMap: Map, + preserveEnglish: boolean +): string { + // Replace all placeholders in a single pass using a regex that matches any placeholder + // Pattern: "%%NLS:moduleId#key%%" + + if (preserveEnglish) { + // Just replace the placeholder with the index + return content.replace(/"%%NLS:([^%]+)%%"/g, (match, inner) => { + const placeholder = `%%NLS:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + return String(index); + } + // Placeholder not found in map, leave as-is (shouldn't happen) + return match; + }); + } else { + // Replace placeholder with index AND replace message with null + // Pattern: "%%NLS:...%%", "message" (or 'message' or `message`) + return content.replace( + /"%%NLS:([^%]+)%%"(\s*,\s*)(?:"[^"]*"|'[^']*'|`[^`]*`)/g, + (match, inner, comma) => { + const placeholder = `%%NLS:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + return `${index}${comma}null`; + } + // Placeholder not found in map, leave as-is (shouldn't happen) + return match; + } + ); + } +} + +// ============================================================================ +// Plugin +// ============================================================================ + +export function nlsPlugin(options: NLSPluginOptions): esbuild.Plugin { + const { collector } = options; + + return { + name: 'nls', + setup(build) { + // Transform TypeScript files to replace localize() calls with placeholders + build.onLoad({ filter: /\.ts$/ }, async (args) => { + // Skip .d.ts files + if (args.path.endsWith('.d.ts')) { + return undefined; + } + + const source = await fs.promises.readFile(args.path, 'utf-8'); + + // Compute module ID (e.g., "vs/editor/editor" from "src/vs/editor/editor.ts") + const relativePath = path.relative(options.baseDir, args.path); + const moduleId = relativePath + .replace(/\\/g, '/') + .replace(/\.ts$/, ''); + + // Transform localize() calls to placeholders + const { code, entries: fileEntries } = transformToPlaceholders(source, moduleId); + + // Collect entries + for (const entry of fileEntries) { + collector.add(entry); + } + + if (fileEntries.length > 0) { + return { contents: code, loader: 'ts' }; + } + + // No NLS calls, return undefined to let esbuild handle normally + return undefined; + }); + } + }; +} diff --git a/build/lib/nls-analysis.ts b/build/lib/nls-analysis.ts new file mode 100644 index 0000000000000..2b00da68c7a23 --- /dev/null +++ b/build/lib/nls-analysis.ts @@ -0,0 +1,317 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ISpan { + start: ts.LineAndCharacter; + end: ts.LineAndCharacter; +} + +export interface ILocalizeCall { + keySpan: ISpan; + key: string; + valueSpan: ISpan; + value: string; +} + +// ============================================================================ +// AST Collection +// ============================================================================ + +export const CollectStepResult = Object.freeze({ + Yes: 'Yes', + YesAndRecurse: 'YesAndRecurse', + No: 'No', + NoAndRecurse: 'NoAndRecurse' +}); + +export type CollectStepResult = typeof CollectStepResult[keyof typeof CollectStepResult]; + +export function collect(node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { + const result: ts.Node[] = []; + + function loop(node: ts.Node) { + const stepResult = fn(node); + + if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) { + result.push(node); + } + + if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) { + ts.forEachChild(node, loop); + } + } + + loop(node); + return result; +} + +export function isImportNode(node: ts.Node): boolean { + return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; +} + +export function isCallExpressionWithinTextSpanCollectStep(textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { + if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { + return CollectStepResult.No; + } + + return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; +} + +// ============================================================================ +// Language Service Host +// ============================================================================ + +export class SingleFileServiceHost implements ts.LanguageServiceHost { + private file: ts.IScriptSnapshot; + private lib: ts.IScriptSnapshot; + private options: ts.CompilerOptions; + private filename: string; + + constructor(options: ts.CompilerOptions, filename: string, contents: string) { + this.options = options; + this.filename = filename; + this.file = ts.ScriptSnapshot.fromString(contents); + this.lib = ts.ScriptSnapshot.fromString(''); + } + + getCompilationSettings = () => this.options; + getScriptFileNames = () => [this.filename]; + getScriptVersion = () => '1'; + getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib; + getCurrentDirectory = () => ''; + getDefaultLibFileName = () => 'lib.d.ts'; + + readFile(path: string): string | undefined { + if (path === this.filename) { + return this.file.getText(0, this.file.getLength()); + } + return undefined; + } + + fileExists(path: string): boolean { + return path === this.filename; + } +} + +// ============================================================================ +// Analysis +// ============================================================================ + +/** + * Analyzes TypeScript source code to find localize() or localize2() calls. + */ +export function analyzeLocalizeCalls( + contents: string, + functionName: 'localize' | 'localize2' +): ILocalizeCall[] { + const filename = 'file.ts'; + const options: ts.CompilerOptions = { noResolve: true }; + const serviceHost = new SingleFileServiceHost(options, filename, contents); + const service = ts.createLanguageService(serviceHost); + const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); + + // Find all imports + const imports = collect(sourceFile, n => isImportNode(n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse); + + // import nls = require('vs/nls'); + const importEqualsDeclarations = imports + .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) + .map(n => n as ts.ImportEqualsDeclaration) + .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) + .filter(d => { + const text = (d.moduleReference as ts.ExternalModuleReference).expression.getText(); + return text.endsWith(`/nls'`) || text.endsWith(`/nls"`) || text.endsWith(`/nls.js'`) || text.endsWith(`/nls.js"`); + }); + + // import ... from 'vs/nls'; + const importDeclarations = imports + .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration) + .map(n => n as ts.ImportDeclaration) + .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) + .filter(d => { + const text = d.moduleSpecifier.getText(); + return text.endsWith(`/nls'`) || text.endsWith(`/nls"`) || text.endsWith(`/nls.js'`) || text.endsWith(`/nls.js"`); + }) + .filter(d => !!d.importClause && !!d.importClause.namedBindings); + + // `nls.localize(...)` calls via namespace import + const nlsLocalizeCallExpressions: ts.CallExpression[] = []; + + const namespaceImports = importDeclarations + .filter(d => d.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport) + .map(d => (d.importClause!.namedBindings as ts.NamespaceImport).name); + + const importEqualsNames = importEqualsDeclarations.map(d => d.name); + + for (const name of [...namespaceImports, ...importEqualsNames]) { + const refs = service.getReferencesAtPosition(filename, name.pos + 1) ?? []; + for (const ref of refs) { + if (ref.isWriteAccess) { + continue; + } + const calls = collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n)); + const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined; + if (lastCall && + lastCall.expression.kind === ts.SyntaxKind.PropertyAccessExpression && + (lastCall.expression as ts.PropertyAccessExpression).name.getText() === functionName) { + nlsLocalizeCallExpressions.push(lastCall); + } + } + } + + // `localize` named imports + const namedImports = importDeclarations + .filter(d => d.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) + .flatMap(d => Array.from((d.importClause!.namedBindings! as ts.NamedImports).elements)); + + const localizeCallExpressions: ts.CallExpression[] = []; + + // Direct named import: import { localize } from 'vs/nls' + for (const namedImport of namedImports) { + const isTarget = namedImport.name.getText() === functionName || + (namedImport.propertyName && namedImport.propertyName.getText() === functionName); + + if (!isTarget) { + continue; + } + + const searchName = namedImport.propertyName ? namedImport.name : namedImport.name; + const refs = service.getReferencesAtPosition(filename, searchName.pos + 1) ?? []; + + for (const ref of refs) { + if (ref.isWriteAccess) { + continue; + } + const calls = collect(sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ref.textSpan, n)); + const lastCall = calls[calls.length - 1] as ts.CallExpression | undefined; + if (lastCall) { + localizeCallExpressions.push(lastCall); + } + } + } + + // Combine and deduplicate + const allCalls = [...nlsLocalizeCallExpressions, ...localizeCallExpressions]; + const seen = new Set(); + const uniqueCalls = allCalls.filter(call => { + const start = call.getStart(); + if (seen.has(start)) { + return false; + } + seen.add(start); + return true; + }); + + // Convert to ILocalizeCall + return uniqueCalls + .filter(e => e.arguments.length > 1) + .sort((a, b) => a.arguments[0].getStart() - b.arguments[0].getStart()) + .map(e => { + const args = e.arguments; + return { + keySpan: { + start: ts.getLineAndCharacterOfPosition(sourceFile, args[0].getStart()), + end: ts.getLineAndCharacterOfPosition(sourceFile, args[0].getEnd()) + }, + key: args[0].getText(), + valueSpan: { + start: ts.getLineAndCharacterOfPosition(sourceFile, args[1].getStart()), + end: ts.getLineAndCharacterOfPosition(sourceFile, args[1].getEnd()) + }, + value: args[1].getText() + }; + }); +} + +// ============================================================================ +// Text Model for patching +// ============================================================================ + +export class TextModel { + private lines: string[]; + private lineEndings: string[]; + + constructor(contents: string) { + const regex = /\r\n|\r|\n/g; + let index = 0; + let match: RegExpExecArray | null; + + this.lines = []; + this.lineEndings = []; + + while (match = regex.exec(contents)) { + this.lines.push(contents.substring(index, match.index)); + this.lineEndings.push(match[0]); + index = regex.lastIndex; + } + + if (contents.length > 0) { + this.lines.push(contents.substring(index, contents.length)); + this.lineEndings.push(''); + } + } + + get(index: number): string { + return this.lines[index]; + } + + set(index: number, line: string): void { + this.lines[index] = line; + } + + get lineCount(): number { + return this.lines.length; + } + + /** + * Applies patch(es) to the model. + * Multiple patches must be ordered. + * Does not support patches spanning multiple lines. + */ + apply(span: ISpan, content: string): void { + const startLineNumber = span.start.line; + const endLineNumber = span.end.line; + + const startLine = this.lines[startLineNumber] || ''; + const endLine = this.lines[endLineNumber] || ''; + + this.lines[startLineNumber] = [ + startLine.substring(0, span.start.character), + content, + endLine.substring(span.end.character) + ].join(''); + + for (let i = startLineNumber + 1; i <= endLineNumber; i++) { + this.lines[i] = ''; + } + } + + toString(): string { + let result = ''; + for (let i = 0; i < this.lines.length; i++) { + result += this.lines[i] + this.lineEndings[i]; + } + return result; + } +} + +// ============================================================================ +// Utilities +// ============================================================================ + +/** + * Parses a localize key or value expression. + * sourceExpression can be "foo", 'foo', `foo` or { key: 'foo', comment: [...] } + */ +export function parseLocalizeKeyOrValue(sourceExpression: string): string | { key: string; comment?: string[] } { + // eslint-disable-next-line no-eval + return eval(`(${sourceExpression})`); +} diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 39cc07d9d01ee..943f154ddf29e 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -10,45 +10,10 @@ import File from 'vinyl'; import sm from 'source-map'; import path from 'path'; import sort from 'gulp-sort'; +import { type ISpan, analyzeLocalizeCalls, TextModel, parseLocalizeKeyOrValue } from './nls-analysis.js'; type FileWithSourcemap = File & { sourceMap: sm.RawSourceMap }; -const CollectStepResult = Object.freeze({ - Yes: 'Yes', - YesAndRecurse: 'YesAndRecurse', - No: 'No', - NoAndRecurse: 'NoAndRecurse' -}); - -type CollectStepResult = typeof CollectStepResult[keyof typeof CollectStepResult]; - -function collect(ts: typeof import('typescript'), node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { - const result: ts.Node[] = []; - - function loop(node: ts.Node) { - const stepResult = fn(node); - - if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) { - result.push(node); - } - - if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) { - ts.forEachChild(node, loop); - } - } - - loop(node); - return result; -} - -function clone(object: T): T { - const result: Record = {}; - for (const id in object) { - result[id] = object[id]; - } - return result as T; -} - /** * Returns a stream containing the patched JavaScript and source maps. */ @@ -117,10 +82,6 @@ globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), return eventStream.duplex(input, output); } -function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean { - return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; -} - const _nls = (() => { const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {}; @@ -138,22 +99,6 @@ const _nls = (() => { nlsKeys?: ILocalizeKey[]; } - interface ISpan { - start: ts.LineAndCharacter; - end: ts.LineAndCharacter; - } - - interface ILocalizeCall { - keySpan: ISpan; - key: string; - valueSpan: ISpan; - value: string; - } - - interface ILocalizeAnalysisResult { - localizeCalls: ILocalizeCall[]; - } - interface IPatch { span: ISpan; content: string; @@ -176,212 +121,11 @@ const _nls = (() => { return { line: position.line - 1, character: position.column }; } - class SingleFileServiceHost implements ts.LanguageServiceHost { - - private file: ts.IScriptSnapshot; - private lib: ts.IScriptSnapshot; - private options: ts.CompilerOptions; - private filename: string; - - constructor(ts: typeof import('typescript'), options: ts.CompilerOptions, filename: string, contents: string) { - this.options = options; - this.filename = filename; - this.file = ts.ScriptSnapshot.fromString(contents); - this.lib = ts.ScriptSnapshot.fromString(''); - } - - getCompilationSettings = () => this.options; - getScriptFileNames = () => [this.filename]; - getScriptVersion = () => '1'; - getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib; - getCurrentDirectory = () => ''; - getDefaultLibFileName = () => 'lib.d.ts'; - - readFile(path: string, _encoding?: string): string | undefined { - if (path === this.filename) { - return this.file.getText(0, this.file.getLength()); - } - return undefined; - } - fileExists(path: string): boolean { - return path === this.filename; - } - } - - function isCallExpressionWithinTextSpanCollectStep(ts: typeof import('typescript'), textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { - if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { - return CollectStepResult.No; - } - - return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; - } - - function analyze( - ts: typeof import('typescript'), - contents: string, - functionName: 'localize' | 'localize2', - options: ts.CompilerOptions = {} - ): ILocalizeAnalysisResult { - const filename = 'file.ts'; - const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents); - const service = ts.createLanguageService(serviceHost); - const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); - - // all imports - const imports = lazy(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); - - // import nls = require('vs/nls'); - const importEqualsDeclarations = imports - .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) - .map(n => n as ts.ImportEqualsDeclaration) - .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) - .filter(d => (d.moduleReference as ts.ExternalModuleReference).expression.getText().endsWith(`/nls.js'`)); - - // import ... from 'vs/nls'; - const importDeclarations = imports - .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration) - .map(n => n as ts.ImportDeclaration) - .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) - .filter(d => d.moduleSpecifier.getText().endsWith(`/nls.js'`)) - .filter(d => !!d.importClause && !!d.importClause.namedBindings); - - // `nls.localize(...)` calls - const nlsLocalizeCallExpressions = importDeclarations - .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) - .map(d => (d.importClause!.namedBindings as ts.NamespaceImport).name) - .concat(importEqualsDeclarations.map(d => d.name)) - - // find read-only references to `nls` - .map(n => service.getReferencesAtPosition(filename, n.pos + 1) ?? []) - .flatten() - .filter(r => !r.isWriteAccess) - - // find the deepest call expressions AST nodes that contain those references - .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => lazy(a).last()) - .filter(n => !!n) - .map(n => n as ts.CallExpression) - - // only `localize` calls - .filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && (n.expression as ts.PropertyAccessExpression).name.getText() === functionName); - - // `localize` named imports - const allLocalizeImportDeclarations = importDeclarations - .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) - .map(d => (d.importClause!.namedBindings! as ts.NamedImports).elements) - .flatten(); - - // `localize` read-only references - const localizeReferences = allLocalizeImportDeclarations - .filter(d => d.name.getText() === functionName) - .map(n => service.getReferencesAtPosition(filename, n.pos + 1) ?? []) - .flatten() - .filter(r => !r.isWriteAccess); - - // custom named `localize` read-only references - const namedLocalizeReferences = allLocalizeImportDeclarations - .filter(d => !!d.propertyName && d.propertyName.getText() === functionName) - .map(n => service.getReferencesAtPosition(filename, n.name.pos + 1) ?? []) - .flatten() - .filter(r => !r.isWriteAccess); - - // find the deepest call expressions AST nodes that contain those references - const localizeCallExpressions = localizeReferences - .concat(namedLocalizeReferences) - .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => lazy(a).last()) - .filter(n => !!n) - .map(n => n as ts.CallExpression); - - // collect everything - const localizeCalls = nlsLocalizeCallExpressions - .concat(localizeCallExpressions) - .map(e => e.arguments) - .filter(a => a.length > 1) - .sort((a, b) => a[0].getStart() - b[0].getStart()) - .map(a => ({ - keySpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getEnd()) }, - key: a[0].getText(), - valueSpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getEnd()) }, - value: a[1].getText() - })); - - return { - localizeCalls: localizeCalls.toArray() - }; - } - - class TextModel { - - private lines: string[]; - private lineEndings: string[]; - - constructor(contents: string) { - const regex = /\r\n|\r|\n/g; - let index = 0; - let match: RegExpExecArray | null; - - this.lines = []; - this.lineEndings = []; - - while (match = regex.exec(contents)) { - this.lines.push(contents.substring(index, match.index)); - this.lineEndings.push(match[0]); - index = regex.lastIndex; - } - - if (contents.length > 0) { - this.lines.push(contents.substring(index, contents.length)); - this.lineEndings.push(''); - } - } - - public get(index: number): string { - return this.lines[index]; - } - - public set(index: number, line: string): void { - this.lines[index] = line; - } - - public get lineCount(): number { - return this.lines.length; - } - - /** - * Applies patch(es) to the model. - * Multiple patches must be ordered. - * Does not support patches spanning multiple lines. - */ - public apply(patch: IPatch): void { - const startLineNumber = patch.span.start.line; - const endLineNumber = patch.span.end.line; - - const startLine = this.lines[startLineNumber] || ''; - const endLine = this.lines[endLineNumber] || ''; - - this.lines[startLineNumber] = [ - startLine.substring(0, patch.span.start.character), - patch.content, - endLine.substring(patch.span.end.character) - ].join(''); - - for (let i = startLineNumber + 1; i <= endLineNumber; i++) { - this.lines[i] = ''; - } - } - - public toString(): string { - return lazy(this.lines).zip(this.lineEndings) - .flatten().toArray().join(''); - } - } - function patchJavascript(patches: IPatch[], contents: string): string { const model = new TextModel(contents); // patch the localize calls - lazy(patches).reverse().each(p => model.apply(p)); + lazy(patches).reverse().each(p => model.apply(p.span, p.content)); return model.toString(); } @@ -431,24 +175,16 @@ const _nls = (() => { return JSON.parse(smg.toString()); } - function parseLocalizeKeyOrValue(sourceExpression: string) { - // sourceValue can be "foo", 'foo', `foo` or { .... } - // in its evalulated form - // we want to return either the string or the object - // eslint-disable-next-line no-eval - return eval(`(${sourceExpression})`); - } - - function patch(ts: typeof import('typescript'), typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult { - const { localizeCalls } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); + function patch(typescript: string, javascript: string, sourcemap: sm.RawSourceMap, options: { preserveEnglish: boolean }): INlsPatchResult { + const localizeCalls = analyzeLocalizeCalls(typescript, 'localize'); + const localize2Calls = analyzeLocalizeCalls(typescript, 'localize2'); if (localizeCalls.length === 0 && localize2Calls.length === 0) { return { javascript, sourcemap }; } const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); - const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); + const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value) as string).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value) as string)); const smc = new sm.SourceMapConsumer(sourcemap); const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); @@ -505,7 +241,6 @@ const _nls = (() => { .replace(/\\/g, '/'); const { javascript, sourcemap, nlsKeys, nlsMessages } = patch( - ts, typescript, javascriptFile.contents!.toString(), javascriptFile.sourceMap, From f87458c76573f051e88338a5688483c43257d5dd Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 30 Jan 2026 18:29:48 +0100 Subject: [PATCH 12/53] feat: refactor CLI command handling and enhance bundling options for NLS and minification --- build/esbuild/index.ts | 119 ++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/build/esbuild/index.ts b/build/esbuild/index.ts index f9ff737e53ae5..7c0e9efc62206 100644 --- a/build/esbuild/index.ts +++ b/build/esbuild/index.ts @@ -18,9 +18,14 @@ const globAsync = promisify(glob); // ============================================================================ const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname)); -const isWatch = process.argv.includes('--watch'); -const isBundle = process.argv.includes('--bundle'); -const isMinify = process.argv.includes('--minify'); + +// CLI: transpile [--watch] | bundle [--minify] [--nls] +const command = process.argv[2]; // 'transpile' or 'bundle' +const options = { + watch: process.argv.includes('--watch'), + minify: process.argv.includes('--minify'), + nls: process.argv.includes('--nls'), +}; const SRC_DIR = 'src'; const OUT_DIR = 'out'; @@ -341,10 +346,10 @@ async function transpile(outDir: string, excludeTests: boolean): Promise { // Bundle (Goal 2: JS → bundled JS) // ============================================================================ -async function bundle(): Promise { +async function bundle(doMinify: boolean, doNls: boolean): Promise { await cleanDir(OUT_VSCODE_DIR); - console.log(`[bundle] ${SRC_DIR} → ${OUT_VSCODE_DIR}`); + console.log(`[bundle] ${SRC_DIR} → ${OUT_VSCODE_DIR}${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); const t1 = Date.now(); // Read TSLib for banner @@ -368,7 +373,7 @@ ${tslib}`, } }); - // Create shared NLS collector + // Create shared NLS collector (only used if doNls is true) const nlsCollector = createNLSCollector(); const preserveEnglish = false; // Production mode: replace messages with null @@ -389,6 +394,14 @@ ${tslib}`, const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`); const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entryPoint}.js`); + const plugins: esbuild.Plugin[] = [cssExternalPlugin()]; + if (doNls) { + plugins.unshift(nlsPlugin({ + baseDir: path.join(REPO_ROOT, SRC_DIR), + collector: nlsCollector, + })); + } + const result = await esbuild.build({ entryPoints: [entryPath], outfile: outPath, @@ -399,7 +412,7 @@ ${tslib}`, packages: 'external', sourcemap: 'external', sourcesContent: false, - minify: isMinify, + minify: doMinify, treeShaking: true, banner, loader: { @@ -409,13 +422,7 @@ ${tslib}`, '.sh': 'file', }, assetNames: 'media/[name]', - plugins: [ - nlsPlugin({ - baseDir: path.join(REPO_ROOT, SRC_DIR), - collector: nlsCollector, - }), - cssExternalPlugin() - ], + plugins, write: false, // Don't write yet, we need to post-process logLevel: 'warning', logOverride: { @@ -437,6 +444,14 @@ ${tslib}`, const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entry}.js`); + const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin()]; + if (doNls) { + bootstrapPlugins.unshift(nlsPlugin({ + baseDir: path.join(REPO_ROOT, SRC_DIR), + collector: nlsCollector, + })); + } + const result = await esbuild.build({ entryPoints: [entryPath], outfile: outPath, @@ -447,16 +462,10 @@ ${tslib}`, packages: 'external', sourcemap: 'external', sourcesContent: false, - minify: isMinify, + minify: doMinify, treeShaking: true, banner, - plugins: [ - nlsPlugin({ - baseDir: path.join(REPO_ROOT, SRC_DIR), - collector: nlsCollector, - }), - inlineMinimistPlugin() - ], + plugins: bootstrapPlugins, write: false, // Don't write yet, we need to post-process logLevel: 'warning', logOverride: { @@ -469,7 +478,11 @@ ${tslib}`, } // Finalize NLS: sort entries, assign indices, write metadata files - const { indexMap } = await finalizeNLS(nlsCollector, path.join(REPO_ROOT, OUT_VSCODE_DIR)); + let indexMap = new Map(); + if (doNls) { + const nlsResult = await finalizeNLS(nlsCollector, path.join(REPO_ROOT, OUT_VSCODE_DIR)); + indexMap = nlsResult.indexMap; + } // Post-process and write all output files let bundled = 0; @@ -481,7 +494,7 @@ ${tslib}`, for (const file of result.outputFiles) { await fs.promises.mkdir(path.dirname(file.path), { recursive: true }); - if (file.path.endsWith('.js') && indexMap.size > 0) { + if (doNls && file.path.endsWith('.js') && indexMap.size > 0) { // Post-process JS files to replace NLS placeholders with indices const processed = postProcessNLS(file.text, indexMap, preserveEnglish); await fs.promises.writeFile(file.path, processed); @@ -609,25 +622,57 @@ async function watch(): Promise { // Main // ============================================================================ +function printUsage(): void { + console.log(`Usage: npx tsx build/esbuild/index.ts [options] + +Commands: + transpile Transpile TypeScript to JavaScript (single-file, fast) + bundle Bundle entry points into optimized bundles + +Options for 'transpile': + --watch Watch for changes and rebuild incrementally + +Options for 'bundle': + --minify Minify the output bundles + --nls Process NLS (localization) strings + +Examples: + npx tsx build/esbuild/index.ts transpile + npx tsx build/esbuild/index.ts transpile --watch + npx tsx build/esbuild/index.ts bundle + npx tsx build/esbuild/index.ts bundle --minify --nls +`); +} + async function main(): Promise { const t1 = Date.now(); try { - if (isWatch) { - await watch(); - } else if (isBundle) { - await bundle(); - } else { - const outDir = OUT_DIR; - await cleanDir(outDir); - console.log(`[transpile] ${SRC_DIR} → ${outDir}`); - const t1 = Date.now(); - await transpile(outDir, false); - await copyResources(outDir, false, false); - console.log(`[transpile] Done in ${Date.now() - t1}ms`); + switch (command) { + case 'transpile': + if (options.watch) { + await watch(); + } else { + const outDir = OUT_DIR; + await cleanDir(outDir); + console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + const t1 = Date.now(); + await transpile(outDir, false); + await copyResources(outDir, false, false); + console.log(`[transpile] Done in ${Date.now() - t1}ms`); + } + break; + + case 'bundle': + await bundle(options.minify, options.nls); + break; + + default: + printUsage(); + process.exit(command ? 1 : 0); } - if (!isWatch) { + if (!options.watch) { console.log(`\n✓ Total: ${Date.now() - t1}ms`); } } catch (err) { From 7642c07690722526ca65591e253d8e2db6b02f53 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 30 Jan 2026 18:31:27 +0100 Subject: [PATCH 13/53] feat(nls): implement NLS plugin for esbuild with message extraction and processing --- build/{esbuild => next}/index.ts | 10 +++++----- build/{esbuild => next}/nls-plugin.ts | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename build/{esbuild => next}/index.ts (98%) rename build/{esbuild => next}/nls-plugin.ts (100%) diff --git a/build/esbuild/index.ts b/build/next/index.ts similarity index 98% rename from build/esbuild/index.ts rename to build/next/index.ts index 7c0e9efc62206..33053f544714a 100644 --- a/build/esbuild/index.ts +++ b/build/next/index.ts @@ -623,7 +623,7 @@ async function watch(): Promise { // ============================================================================ function printUsage(): void { - console.log(`Usage: npx tsx build/esbuild/index.ts [options] + console.log(`Usage: npx tsx build/next/index.ts [options] Commands: transpile Transpile TypeScript to JavaScript (single-file, fast) @@ -637,10 +637,10 @@ Options for 'bundle': --nls Process NLS (localization) strings Examples: - npx tsx build/esbuild/index.ts transpile - npx tsx build/esbuild/index.ts transpile --watch - npx tsx build/esbuild/index.ts bundle - npx tsx build/esbuild/index.ts bundle --minify --nls + npx tsx build/next/index.ts transpile + npx tsx build/next/index.ts transpile --watch + npx tsx build/next/index.ts bundle + npx tsx build/next/index.ts bundle --minify --nls `); } diff --git a/build/esbuild/nls-plugin.ts b/build/next/nls-plugin.ts similarity index 100% rename from build/esbuild/nls-plugin.ts rename to build/next/nls-plugin.ts From 73dcb4734daa1918b601c7d4c01305d06822130d Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 2 Feb 2026 18:04:20 +0100 Subject: [PATCH 14/53] feat: implement esbuild-based bundling tasks and enhance NLS processing --- build/gulpfile.vscode.ts | 66 ++++++++++++++++++++++++ build/lib/nls.ts | 2 +- build/next/index.ts | 105 ++++++++++++++++++++++++++++++++------- build/next/nls-plugin.ts | 65 +++++++++++++++++------- 4 files changed, 202 insertions(+), 36 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 358bb3acad3cc..e3999dddfde20 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -33,6 +33,7 @@ import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, import { promisify } from 'util'; import globCallback from 'glob'; import rceditCallback from 'rcedit'; +import * as cp from 'child_process'; const glob = promisify(globCallback); @@ -151,6 +152,35 @@ const bundleVSCodeTask = task.define('bundle-vscode', task.series( )); gulp.task(bundleVSCodeTask); +// esbuild-based bundle tasks (drop-in replacement for bundle-vscode / minify-vscode) +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promise { + return new Promise((resolve, reject) => { + // const tsxPath = path.join(root, 'build/node_modules/tsx/dist/cli.mjs'); + const scriptPath = path.join(root, 'build/next/index.ts'); + const args = [scriptPath, 'bundle', '--out', outDir]; + if (minify) { + args.push('--minify'); + } + if (nls) { + args.push('--nls'); + } + + const proc = cp.spawn(process.execPath, args, { + cwd: root, + stdio: 'inherit' + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`esbuild bundle failed with exit code ${code}`)); + } + }); + }); +} + const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const minifyVSCodeTask = task.define('minify-vscode', task.series( bundleVSCodeTask, @@ -509,6 +539,7 @@ BUILD_TARGETS.forEach(buildTarget => { const arch = buildTarget.arch; const opts = buildTarget.opts; + // Traditional gulp-based builds: vscode-{platform}-{arch} and vscode-{platform}-{arch}-min const [vscode, vscodeMin] = ['', 'min'].map(minified => { const sourceFolderName = `out-vscode${dashed(minified)}`; const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; @@ -539,9 +570,44 @@ BUILD_TARGETS.forEach(buildTarget => { return vscodeTask; }); + // esbuild-based builds: vscode-{platform}-{arch}-esbuild and vscode-{platform}-{arch}-esbuild-min + // These skip TypeScript compilation and bundle directly from source + const [vscodeEsbuild, vscodeEsbuildMin] = ['', 'min'].map(minified => { + const sourceFolderName = `out-vscode${dashed(minified)}`; + const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; + + const esbuildBundleTask = task.define( + `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, + () => runEsbuildBundle(sourceFolderName, !!minified, true) + ); + + const tasks = [ + compileNativeExtensionsBuildTask, + util.rimraf(path.join(buildRoot, destinationFolderName)), + packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) + ]; + + if (platform === 'win32') { + tasks.push(patchWin32DependenciesTask(destinationFolderName)); + } + + const vscodeEsbuildTask = task.define(`vscode${dashed(platform)}${dashed(arch)}-esbuild${dashed(minified)}`, task.series( + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + esbuildBundleTask, + ...tasks + )); + gulp.task(vscodeEsbuildTask); + + return vscodeEsbuildTask; + }); + if (process.platform === platform && process.arch === arch) { gulp.task(task.define('vscode', task.series(vscode))); gulp.task(task.define('vscode-min', task.series(vscodeMin))); + gulp.task(task.define('vscode-esbuild', task.series(vscodeEsbuild))); + gulp.task(task.define('vscode-esbuild-min', task.series(vscodeEsbuildMin))); } }); diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 943f154ddf29e..1c054c97399c2 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -10,7 +10,7 @@ import File from 'vinyl'; import sm from 'source-map'; import path from 'path'; import sort from 'gulp-sort'; -import { type ISpan, analyzeLocalizeCalls, TextModel, parseLocalizeKeyOrValue } from './nls-analysis.js'; +import { type ISpan, analyzeLocalizeCalls, TextModel, parseLocalizeKeyOrValue } from './nls-analysis.ts'; type FileWithSourcemap = File & { sourceMap: sm.RawSourceMap }; diff --git a/build/next/index.ts b/build/next/index.ts index 33053f544714a..38af528e4f01c 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -19,12 +19,22 @@ const globAsync = promisify(glob); const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname)); -// CLI: transpile [--watch] | bundle [--minify] [--nls] +// CLI: transpile [--watch] | bundle [--minify] [--nls] [--out ] const command = process.argv[2]; // 'transpile' or 'bundle' + +function getArgValue(name: string): string | undefined { + const index = process.argv.indexOf(name); + if (index !== -1 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + return undefined; +} + const options = { watch: process.argv.includes('--watch'), minify: process.argv.includes('--minify'), nls: process.argv.includes('--nls'), + out: getArgValue('--out'), }; const SRC_DIR = 'src'; @@ -104,10 +114,6 @@ const resourcePatterns = [ 'vs/base/common/semver/semver.js', 'vs/base/browser/dompurify/dompurify.js', - // Electron preload (not bundled) - 'vs/base/parts/sandbox/electron-browser/preload.js', - 'vs/base/parts/sandbox/electron-browser/preload-aux.js', - // Webview pre scripts 'vs/workbench/contrib/webview/browser/pre/*.js', @@ -192,6 +198,44 @@ async function copyFile(srcPath: string, destPath: string): Promise { await fs.promises.copyFile(srcPath, destPath); } +/** + * Standalone TypeScript files that need to be compiled separately (not bundled). + * These run in special contexts (e.g., Electron preload) where bundling isn't appropriate. + */ +const standaloneFiles = [ + 'vs/base/parts/sandbox/electron-browser/preload.ts', + 'vs/base/parts/sandbox/electron-browser/preload-aux.ts', +]; + +async function compileStandaloneFiles(outDir: string, doMinify: boolean): Promise { + console.log(`[standalone] Compiling ${standaloneFiles.length} standalone files...`); + + const banner = `/*!-------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/`; + + await Promise.all(standaloneFiles.map(async (file) => { + const entryPath = path.join(REPO_ROOT, SRC_DIR, file); + const outPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js')); + + await esbuild.build({ + entryPoints: [entryPath], + outfile: outPath, + bundle: false, // Don't bundle - these are standalone scripts + format: 'cjs', // CommonJS for Electron preload + platform: 'node', + target: ['es2024'], + sourcemap: 'external', + sourcesContent: false, + minify: doMinify, + banner: { js: banner }, + logLevel: 'warning', + }); + })); + + console.log(`[standalone] Done`); +} + async function copyCssFiles(outDir: string, excludeTests = false): Promise { // Copy all CSS files from src to output (they're imported by JS) const cssFiles = await globAsync('**/*.css', { @@ -346,10 +390,10 @@ async function transpile(outDir: string, excludeTests: boolean): Promise { // Bundle (Goal 2: JS → bundled JS) // ============================================================================ -async function bundle(doMinify: boolean, doNls: boolean): Promise { - await cleanDir(OUT_VSCODE_DIR); +async function bundle(outDir: string, doMinify: boolean, doNls: boolean): Promise { + await cleanDir(outDir); - console.log(`[bundle] ${SRC_DIR} → ${OUT_VSCODE_DIR}${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); + console.log(`[bundle] ${SRC_DIR} → ${outDir}${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); const t1 = Date.now(); // Read TSLib for banner @@ -386,15 +430,25 @@ ${tslib}`, ...keyboardMapEntryPoints, ]; + // Entry points that should bundle CSS (workbench mains and code entry points) + // Workers and other entry points don't need CSS + const bundleCssEntryPoints = new Set([ + 'vs/workbench/workbench.desktop.main', + 'vs/workbench/workbench.web.main.internal', + 'vs/code/electron-browser/workbench/workbench', + 'vs/code/browser/workbench/workbench', + ]); + // Collect all build results (with write: false) const buildResults: { outPath: string; result: esbuild.BuildResult }[] = []; // Bundle each entry point directly from TypeScript source await Promise.all(allEntryPoints.map(async (entryPoint) => { const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`); - const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entryPoint}.js`); + const outPath = path.join(REPO_ROOT, outDir, `${entryPoint}.js`); - const plugins: esbuild.Plugin[] = [cssExternalPlugin()]; + // Use CSS external plugin for entry points that don't need bundled CSS + const plugins: esbuild.Plugin[] = bundleCssEntryPoints.has(entryPoint) ? [] : [cssExternalPlugin()]; if (doNls) { plugins.unshift(nlsPlugin({ baseDir: path.join(REPO_ROOT, SRC_DIR), @@ -402,9 +456,17 @@ ${tslib}`, })); } - const result = await esbuild.build({ - entryPoints: [entryPath], - outfile: outPath, + // For entry points that bundle CSS, we need to use outdir instead of outfile + // because esbuild can't produce multiple output files (JS + CSS) with outfile + const needsCssBundling = bundleCssEntryPoints.has(entryPoint); + + const buildOptions: esbuild.BuildOptions = { + entryPoints: needsCssBundling + ? [{ in: entryPath, out: entryPoint }] + : [entryPath], + ...(needsCssBundling + ? { outdir: path.join(REPO_ROOT, outDir) } + : { outfile: outPath }), bundle: true, format: 'esm', platform: 'neutral', @@ -429,7 +491,9 @@ ${tslib}`, 'unsupported-require-call': 'silent', }, tsconfigRaw, - }); + }; + + const result = await esbuild.build(buildOptions); buildResults.push({ outPath, result }); })); @@ -442,7 +506,7 @@ ${tslib}`, continue; } - const outPath = path.join(REPO_ROOT, OUT_VSCODE_DIR, `${entry}.js`); + const outPath = path.join(REPO_ROOT, outDir, `${entry}.js`); const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin()]; if (doNls) { @@ -480,7 +544,7 @@ ${tslib}`, // Finalize NLS: sort entries, assign indices, write metadata files let indexMap = new Map(); if (doNls) { - const nlsResult = await finalizeNLS(nlsCollector, path.join(REPO_ROOT, OUT_VSCODE_DIR)); + const nlsResult = await finalizeNLS(nlsCollector, path.join(REPO_ROOT, outDir)); indexMap = nlsResult.indexMap; } @@ -507,7 +571,10 @@ ${tslib}`, } // Copy resources (exclude dev files and tests for production) - await copyResources(OUT_VSCODE_DIR, true, true); + await copyResources(outDir, true, true); + + // Compile standalone TypeScript files (like Electron preload scripts) that cannot be bundled + await compileStandaloneFiles(outDir, doMinify); console.log(`[bundle] Done in ${Date.now() - t1}ms (${bundled} bundles)`); } @@ -635,12 +702,14 @@ Options for 'transpile': Options for 'bundle': --minify Minify the output bundles --nls Process NLS (localization) strings + --out Output directory (default: out-vscode) Examples: npx tsx build/next/index.ts transpile npx tsx build/next/index.ts transpile --watch npx tsx build/next/index.ts bundle npx tsx build/next/index.ts bundle --minify --nls + npx tsx build/next/index.ts bundle --nls --out out-vscode-min `); } @@ -664,7 +733,7 @@ async function main(): Promise { break; case 'bundle': - await bundle(options.minify, options.nls); + await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls); break; default: diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 645d387dddee5..1b048269b6f28 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -165,9 +165,13 @@ function transformToPlaceholders( ): { code: string; entries: NLSEntry[] } { const localizeCalls = analyzeLocalizeCalls(source, 'localize'); const localize2Calls = analyzeLocalizeCalls(source, 'localize2'); - const allCalls = [...localizeCalls, ...localize2Calls].sort( - (a, b) => a.keySpan.start.line - b.keySpan.start.line || - a.keySpan.start.character - b.keySpan.start.character + + // Tag calls with their type so we can handle them differently later + const taggedLocalize = localizeCalls.map(call => ({ call, isLocalize2: false })); + const taggedLocalize2 = localize2Calls.map(call => ({ call, isLocalize2: true })); + const allCalls = [...taggedLocalize, ...taggedLocalize2].sort( + (a, b) => a.call.keySpan.start.line - b.call.keySpan.start.line || + a.call.keySpan.start.character - b.call.keySpan.start.character ); if (allCalls.length === 0) { @@ -178,13 +182,16 @@ function transformToPlaceholders( const model = new TextModel(source); // Process in reverse order to preserve positions - for (const call of allCalls.reverse()) { + for (const { call, isLocalize2 } of allCalls.reverse()) { const keyParsed = parseLocalizeKeyOrValue(call.key) as string | { key: string; comment: string[] }; const messageParsed = parseLocalizeKeyOrValue(call.value); const keyString = typeof keyParsed === 'string' ? keyParsed : keyParsed.key; - // Create a deterministic placeholder - const placeholder = `%%NLS:${moduleId}#${keyString}%%`; + // Use different placeholder prefix for localize vs localize2 + // localize: message will be replaced with null + // localize2: message will be preserved (only key replaced) + const prefix = isLocalize2 ? 'NLS2' : 'NLS'; + const placeholder = `%%${prefix}:${moduleId}#${keyString}%%`; entries.push({ moduleId, @@ -208,14 +215,21 @@ function replaceInOutput( indexMap: Map, preserveEnglish: boolean ): string { - // Replace all placeholders in a single pass using a regex that matches any placeholder - // Pattern: "%%NLS:moduleId#key%%" + // Replace all placeholders in a single pass using regex + // Two types of placeholders: + // - %%NLS:moduleId#key%% for localize() - message replaced with null + // - %%NLS2:moduleId#key%% for localize2() - message preserved if (preserveEnglish) { - // Just replace the placeholder with the index - return content.replace(/"%%NLS:([^%]+)%%"/g, (match, inner) => { - const placeholder = `%%NLS:${inner}%%`; - const index = indexMap.get(placeholder); + // Just replace the placeholder with the index (both NLS and NLS2) + return content.replace(/"%%NLS2?:([^%]+)%%"/g, (match, inner) => { + // Try NLS first, then NLS2 + let placeholder = `%%NLS:${inner}%%`; + let index = indexMap.get(placeholder); + if (index === undefined) { + placeholder = `%%NLS2:${inner}%%`; + index = indexMap.get(placeholder); + } if (index !== undefined) { return String(index); } @@ -223,20 +237,37 @@ function replaceInOutput( return match; }); } else { - // Replace placeholder with index AND replace message with null - // Pattern: "%%NLS:...%%", "message" (or 'message' or `message`) - return content.replace( - /"%%NLS:([^%]+)%%"(\s*,\s*)(?:"[^"]*"|'[^']*'|`[^`]*`)/g, + // For NLS (localize): replace placeholder with index AND replace message with null + // For NLS2 (localize2): replace placeholder with index, keep message + // Note: Use (?:[^"\\]|\\.)* to properly handle escaped quotes like \" or \\ + + // First handle NLS (localize) - replace both key and message + content = content.replace( + /"%%NLS:([^%]+)%%"(\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, (match, inner, comma) => { const placeholder = `%%NLS:${inner}%%`; const index = indexMap.get(placeholder); if (index !== undefined) { return `${index}${comma}null`; } - // Placeholder not found in map, leave as-is (shouldn't happen) return match; } ); + + // Then handle NLS2 (localize2) - replace only key, keep message + content = content.replace( + /"%%NLS2:([^%]+)%%"/g, + (match, inner) => { + const placeholder = `%%NLS2:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + return String(index); + } + return match; + } + ); + + return content; } } From 286945b656be8d84e1d5c57be110c5050e0cf1d3 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 12:18:39 +0100 Subject: [PATCH 15/53] feat: enhance esbuild bundling with target support for desktop, server, and server-web --- build/gulpfile.vscode.ts | 16 ++- build/next/index.ts | 247 ++++++++++++++++++++++++++++++++------- 2 files changed, 217 insertions(+), 46 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index e3999dddfde20..cdde9c10000e0 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -153,11 +153,11 @@ const bundleVSCodeTask = task.define('bundle-vscode', task.series( gulp.task(bundleVSCodeTask); // esbuild-based bundle tasks (drop-in replacement for bundle-vscode / minify-vscode) -function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promise { +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: 'desktop' | 'server' | 'server-web' = 'desktop'): Promise { return new Promise((resolve, reject) => { // const tsxPath = path.join(root, 'build/node_modules/tsx/dist/cli.mjs'); const scriptPath = path.join(root, 'build/next/index.ts'); - const args = [scriptPath, 'bundle', '--out', outDir]; + const args = [scriptPath, 'bundle', '--out', outDir, '--target', target]; if (minify) { args.push('--minify'); } @@ -199,6 +199,18 @@ const coreCI = task.define('core-ci', task.series( )); gulp.task(coreCI); +const coreCIEsbuild = task.define('core-ci-esbuild', task.series( + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + task.parallel( + task.define('esbuild-vscode-min', () => runEsbuildBundle('out-vscode-min', true, true, 'desktop')), + task.define('esbuild-vscode-reh-min', () => runEsbuildBundle('out-vscode-reh-min', true, true, 'server')), + task.define('esbuild-vscode-reh-web-min', () => runEsbuildBundle('out-vscode-reh-web-min', true, true, 'server-web')), + ) +)); +gulp.task(coreCIEsbuild); + const coreCIPR = task.define('core-ci-pr', task.series( gulp.task('compile-build-without-mangling') as task.Task, task.parallel( diff --git a/build/next/index.ts b/build/next/index.ts index 38af528e4f01c..97d9a9268874e 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -35,8 +35,12 @@ const options = { minify: process.argv.includes('--minify'), nls: process.argv.includes('--nls'), out: getArgValue('--out'), + target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' }; +// Build targets +type BuildTarget = 'desktop' | 'server' | 'server-web'; + const SRC_DIR = 'src'; const OUT_DIR = 'out'; const OUT_VSCODE_DIR = 'out-vscode'; @@ -48,17 +52,23 @@ const UTF8_BOM = Buffer.from([0xef, 0xbb, 0xbf]); // Entry Points (from build/buildfile.ts) // ============================================================================ +// Workers - shared between targets const workerEntryPoints = [ 'vs/editor/common/services/editorWebWorkerMain', 'vs/workbench/api/worker/extensionHostWorkerMain', 'vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain', 'vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain', 'vs/workbench/services/search/worker/localFileSearchMain', - 'vs/platform/profiling/electron-browser/profileAnalysisWorkerMain', 'vs/workbench/contrib/output/common/outputLinkComputerMain', 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain', ]; +// Desktop-only workers (use electron-browser) +const desktopWorkerEntryPoints = [ + 'vs/platform/profiling/electron-browser/profileAnalysisWorkerMain', +]; + +// Desktop workbench and code entry points const desktopEntryPoints = [ 'vs/workbench/workbench.desktop.main', 'vs/workbench/contrib/debug/node/telemetryApp', @@ -73,6 +83,7 @@ const codeEntryPoints = [ 'vs/code/electron-browser/workbench/workbench', ]; +// Web entry points (used in server-web and vscode-web) const webEntryPoints = [ 'vs/workbench/workbench.web.main.internal', 'vs/code/browser/workbench/workbench', @@ -84,19 +95,118 @@ const keyboardMapEntryPoints = [ 'vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win', ]; -const bootstrapEntryPoints = [ +// Server entry points (reh) +const serverEntryPoints = [ + 'vs/workbench/api/node/extensionHostProcess', + 'vs/platform/files/node/watcher/watcherMain', + 'vs/platform/terminal/node/ptyHostMain', +]; + +// Bootstrap files per target +const bootstrapEntryPointsDesktop = [ 'main', 'cli', 'bootstrap-fork', +]; + +const bootstrapEntryPointsServer = [ 'server-main', 'server-cli', + 'bootstrap-fork', ]; +/** + * Get entry points for a build target. + */ +function getEntryPointsForTarget(target: BuildTarget): string[] { + switch (target) { + case 'desktop': + return [ + ...workerEntryPoints, + ...desktopWorkerEntryPoints, + ...desktopEntryPoints, + ...codeEntryPoints, + ...webEntryPoints, // Desktop also includes web for remote development + ...keyboardMapEntryPoints, + ]; + case 'server': + return [ + ...serverEntryPoints, + ]; + case 'server-web': + return [ + ...serverEntryPoints, + ...workerEntryPoints, + ...webEntryPoints, + ...keyboardMapEntryPoints, + ]; + default: + throw new Error(`Unknown target: ${target}`); + } +} + +/** + * Get bootstrap entry points for a build target. + */ +function getBootstrapEntryPointsForTarget(target: BuildTarget): string[] { + switch (target) { + case 'desktop': + return bootstrapEntryPointsDesktop; + case 'server': + case 'server-web': + return bootstrapEntryPointsServer; + default: + throw new Error(`Unknown target: ${target}`); + } +} + +/** + * Get entry points that should bundle CSS (workbench mains). + */ +function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { + switch (target) { + case 'desktop': + return new Set([ + 'vs/workbench/workbench.desktop.main', + 'vs/workbench/workbench.web.main.internal', + 'vs/code/electron-browser/workbench/workbench', + 'vs/code/browser/workbench/workbench', + ]); + case 'server': + return new Set(); // Server has no UI + case 'server-web': + return new Set([ + 'vs/workbench/workbench.web.main.internal', + 'vs/code/browser/workbench/workbench', + ]); + default: + throw new Error(`Unknown target: ${target}`); + } +} + // ============================================================================ // Resource Patterns (files to copy, not transpile/bundle) // ============================================================================ -const resourcePatterns = [ +// Common resources needed by all targets +const commonResourcePatterns = [ + // Fonts + 'vs/base/browser/ui/codicons/codicon/codicon.ttf', + + // Vendor JavaScript libraries (not transpiled) + 'vs/base/common/marked/marked.js', + 'vs/base/common/semver/semver.js', + 'vs/base/browser/dompurify/dompurify.js', + + // Tree-sitter queries + 'vs/editor/common/languages/highlights/*.scm', + 'vs/editor/common/languages/injections/*.scm', +]; + +// Resources for desktop target +const desktopResourcePatterns = [ + ...commonResourcePatterns, + // HTML 'vs/code/electron-browser/workbench/workbench.html', 'vs/code/electron-browser/workbench/workbench-dev.html', @@ -106,14 +216,6 @@ const resourcePatterns = [ 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', 'vs/workbench/contrib/webview/browser/pre/*.html', - // Fonts - 'vs/base/browser/ui/codicons/codicon/codicon.ttf', - - // Vendor JavaScript libraries (not transpiled) - 'vs/base/common/marked/marked.js', - 'vs/base/common/semver/semver.js', - 'vs/base/browser/dompurify/dompurify.js', - // Webview pre scripts 'vs/workbench/contrib/webview/browser/pre/*.js', @@ -138,12 +240,72 @@ const resourcePatterns = [ 'vs/workbench/services/extensionManagement/common/media/*.png', 'vs/workbench/browser/parts/editor/media/*.png', 'vs/workbench/contrib/debug/browser/media/*.png', +]; - // Tree-sitter queries - 'vs/editor/common/languages/highlights/*.scm', - 'vs/editor/common/languages/injections/*.scm', +// Resources for server target (minimal - no UI) +const serverResourcePatterns = [ + // Shell scripts for process monitoring + 'vs/base/node/cpuUsage.sh', + 'vs/base/node/ps.sh', + + // External Terminal + 'vs/workbench/contrib/externalTerminal/**/*.scpt', + + // Terminal shell integration + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1', + 'vs/workbench/contrib/terminal/common/scripts/CodeTabExpansion.psm1', + 'vs/workbench/contrib/terminal/common/scripts/GitTabExpansion.psm1', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-env.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh', + 'vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish', ]; +// Resources for server-web target (server + web UI) +const serverWebResourcePatterns = [ + ...serverResourcePatterns, + ...commonResourcePatterns, + + // Web HTML + 'vs/code/browser/workbench/workbench.html', + 'vs/code/browser/workbench/workbench-dev.html', + 'vs/code/browser/workbench/callback.html', + 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + 'vs/workbench/contrib/webview/browser/pre/*.html', + + // Webview pre scripts + 'vs/workbench/contrib/webview/browser/pre/*.js', + + // Media - audio + 'vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Media - images + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', + 'vs/workbench/contrib/extensions/browser/media/*.svg', + 'vs/workbench/contrib/extensions/browser/media/*.png', + 'vs/workbench/services/extensionManagement/common/media/*.svg', + 'vs/workbench/services/extensionManagement/common/media/*.png', +]; + +/** + * Get resource patterns for a build target. + */ +function getResourcePatternsForTarget(target: BuildTarget): string[] { + switch (target) { + case 'desktop': + return desktopResourcePatterns; + case 'server': + return serverResourcePatterns; + case 'server-web': + return serverWebResourcePatterns; + default: + throw new Error(`Unknown target: ${target}`); + } +} + // Test fixtures (only copied for development builds, not production) const testFixturePatterns = [ '**/test/**/*.json', @@ -201,20 +363,26 @@ async function copyFile(srcPath: string, destPath: string): Promise { /** * Standalone TypeScript files that need to be compiled separately (not bundled). * These run in special contexts (e.g., Electron preload) where bundling isn't appropriate. + * Only needed for desktop target. */ -const standaloneFiles = [ +const desktopStandaloneFiles = [ 'vs/base/parts/sandbox/electron-browser/preload.ts', 'vs/base/parts/sandbox/electron-browser/preload-aux.ts', ]; -async function compileStandaloneFiles(outDir: string, doMinify: boolean): Promise { - console.log(`[standalone] Compiling ${standaloneFiles.length} standalone files...`); +async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: BuildTarget): Promise { + // Only desktop needs preload scripts + if (target !== 'desktop') { + return; + } + + console.log(`[standalone] Compiling ${desktopStandaloneFiles.length} standalone files...`); const banner = `/*!-------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/`; - await Promise.all(standaloneFiles.map(async (file) => { + await Promise.all(desktopStandaloneFiles.map(async (file) => { const entryPath = path.join(REPO_ROOT, SRC_DIR, file); const outPath = path.join(REPO_ROOT, outDir, file.replace(/\.ts$/, '.js')); @@ -253,8 +421,8 @@ async function copyCssFiles(outDir: string, excludeTests = false): Promise { - console.log(`[resources] Copying to ${outDir}...`); +async function copyResources(outDir: string, target: BuildTarget, excludeDevFiles = false, excludeTests = false): Promise { + console.log(`[resources] Copying to ${outDir} for target '${target}'...`); let copied = 0; const ignorePatterns: string[] = []; @@ -265,6 +433,7 @@ async function copyResources(outDir: string, excludeDevFiles = false, excludeTes ignorePatterns.push('**/*-dev.html'); } + const resourcePatterns = getResourcePatternsForTarget(target); for (const pattern of resourcePatterns) { const files = await globAsync(pattern, { cwd: path.join(REPO_ROOT, SRC_DIR), @@ -390,10 +559,10 @@ async function transpile(outDir: string, excludeTests: boolean): Promise { // Bundle (Goal 2: JS → bundled JS) // ============================================================================ -async function bundle(outDir: string, doMinify: boolean, doNls: boolean): Promise { +async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: BuildTarget): Promise { await cleanDir(outDir); - console.log(`[bundle] ${SRC_DIR} → ${outDir}${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); + console.log(`[bundle] ${SRC_DIR} → ${outDir} (target: ${target})${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); const t1 = Date.now(); // Read TSLib for banner @@ -421,23 +590,10 @@ ${tslib}`, const nlsCollector = createNLSCollector(); const preserveEnglish = false; // Production mode: replace messages with null - // All entry points to bundle - const allEntryPoints = [ - ...workerEntryPoints, - ...desktopEntryPoints, - ...codeEntryPoints, - ...webEntryPoints, - ...keyboardMapEntryPoints, - ]; - - // Entry points that should bundle CSS (workbench mains and code entry points) - // Workers and other entry points don't need CSS - const bundleCssEntryPoints = new Set([ - 'vs/workbench/workbench.desktop.main', - 'vs/workbench/workbench.web.main.internal', - 'vs/code/electron-browser/workbench/workbench', - 'vs/code/browser/workbench/workbench', - ]); + // Get entry points based on target + const allEntryPoints = getEntryPointsForTarget(target); + const bootstrapEntryPoints = getBootstrapEntryPointsForTarget(target); + const bundleCssEntryPoints = getCssBundleEntryPointsForTarget(target); // Collect all build results (with write: false) const buildResults: { outPath: string; result: esbuild.BuildResult }[] = []; @@ -571,10 +727,10 @@ ${tslib}`, } // Copy resources (exclude dev files and tests for production) - await copyResources(outDir, true, true); + await copyResources(outDir, target, true, true); // Compile standalone TypeScript files (like Electron preload scripts) that cannot be bundled - await compileStandaloneFiles(outDir, doMinify); + await compileStandaloneFiles(outDir, doMinify, target); console.log(`[bundle] Done in ${Date.now() - t1}ms (${bundled} bundles)`); } @@ -596,7 +752,7 @@ async function watch(): Promise { const t1 = Date.now(); try { await transpile(outDir, false); - await copyResources(outDir, false, false); + await copyResources(outDir, 'desktop', false, false); console.log(`[transpile] Done in ${Date.now() - t1}ms`); } catch (err) { console.error('[watch] Initial build failed:', err); @@ -703,6 +859,7 @@ Options for 'bundle': --minify Minify the output bundles --nls Process NLS (localization) strings --out Output directory (default: out-vscode) + --target Build target: desktop (default), server, server-web Examples: npx tsx build/next/index.ts transpile @@ -710,6 +867,8 @@ Examples: npx tsx build/next/index.ts bundle npx tsx build/next/index.ts bundle --minify --nls npx tsx build/next/index.ts bundle --nls --out out-vscode-min + npx tsx build/next/index.ts bundle --minify --nls --target server --out out-vscode-reh-min + npx tsx build/next/index.ts bundle --minify --nls --target server-web --out out-vscode-reh-web-min `); } @@ -727,13 +886,13 @@ async function main(): Promise { console.log(`[transpile] ${SRC_DIR} → ${outDir}`); const t1 = Date.now(); await transpile(outDir, false); - await copyResources(outDir, false, false); + await copyResources(outDir, 'desktop', false, false); console.log(`[transpile] Done in ${Date.now() - t1}ms`); } break; case 'bundle': - await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls); + await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.target as BuildTarget); break; default: From 35f4232d25a0ea51c39dad86ec1c0aeba1dfa706 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 12:21:44 +0100 Subject: [PATCH 16/53] TRY use esbuild for core-ci --- build/gulpfile.vscode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index cdde9c10000e0..06fcd7b0f1b23 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -189,7 +189,7 @@ const minifyVSCodeTask = task.define('minify-vscode', task.series( )); gulp.task(minifyVSCodeTask); -const coreCI = task.define('core-ci', task.series( +const coreCI = task.define('core-ci-OLD', task.series( gulp.task('compile-build-with-mangling') as task.Task, task.parallel( gulp.task('minify-vscode') as task.Task, @@ -199,7 +199,7 @@ const coreCI = task.define('core-ci', task.series( )); gulp.task(coreCI); -const coreCIEsbuild = task.define('core-ci-esbuild', task.series( +const coreCIEsbuild = task.define('core-ci', task.series( cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, From ebe5d39c37484eb3b9e6b69ca95bcc883601acfb Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 12:46:04 +0100 Subject: [PATCH 17/53] better error handling --- build/gulpfile.vscode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 06fcd7b0f1b23..2c78ca9a3e647 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -175,7 +175,7 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: if (code === 0) { resolve(); } else { - reject(new Error(`esbuild bundle failed with exit code ${code}`)); + reject(new Error(`esbuild bundle failed with exit code ${code} (outDir: ${outDir}, minify: ${minify}, nls: ${nls}, target: ${target})`)); } }); }); From fa93faf9aaf8030c06b6d68baa7c71f0946a1633 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 15:44:46 +0100 Subject: [PATCH 18/53] feat: add copyCodiconsTask to core CI and esbuild tasks --- build/gulpfile.vscode.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 2c78ca9a3e647..7e4237e41a5a5 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -30,6 +30,7 @@ import { createAsar } from './lib/asar.ts'; import minimist from 'minimist'; import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts'; +import { copyCodiconsTask } from './lib/compilation.ts'; import { promisify } from 'util'; import globCallback from 'glob'; import rceditCallback from 'rcedit'; @@ -200,6 +201,7 @@ const coreCI = task.define('core-ci-OLD', task.series( gulp.task(coreCI); const coreCIEsbuild = task.define('core-ci', task.series( + copyCodiconsTask, cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, @@ -604,6 +606,7 @@ BUILD_TARGETS.forEach(buildTarget => { } const vscodeEsbuildTask = task.define(`vscode${dashed(platform)}${dashed(arch)}-esbuild${dashed(minified)}`, task.series( + copyCodiconsTask, cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, From fddb80a0c78f7b353c93ac805ec8d08b98dd819c Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 16:17:36 +0100 Subject: [PATCH 19/53] no out-build needed --- build/azure-pipelines/product-compile.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index bc13d980df2dd..13a0df9326272 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -103,9 +103,9 @@ jobs: - script: | set -e - [ -d "out-build" ] || { echo "ERROR: out-build folder is missing" >&2; exit 1; } - [ -n "$(find out-build -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: out-build folder is empty" >&2; exit 1; } - echo "out-build exists and is not empty" + # [ -d "out-build" ] || { echo "ERROR: out-build folder is missing" >&2; exit 1; } + # [ -n "$(find out-build -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: out-build folder is empty" >&2; exit 1; } + # echo "out-build exists and is not empty" ls -d out-vscode-* >/dev/null 2>&1 || { echo "ERROR: No out-vscode-* folders found" >&2; exit 1; } for folder in out-vscode-*; do From 07e68d11dde93060728adb24e95e9edc7550700c Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 18:18:05 +0100 Subject: [PATCH 20/53] fix: update date handling in jsonEditor for product.json and add build date file in bundle process --- build/gulpfile.reh.ts | 2 +- build/gulpfile.vscode.ts | 2 +- build/next/index.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 27149338d9f7e..b935764033e55 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -321,7 +321,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN let productJsonContents = ''; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor({ commit, date: readISODate('out-build'), version })) + .pipe(jsonEditor({ commit, date: readISODate(sourceFolderName), version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 7e4237e41a5a5..7f38ed3c3740f 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -315,7 +315,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d let productJsonContents: string; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor({ commit, date: readISODate('out-build'), checksums, version })) + .pipe(jsonEditor({ commit, date: readISODate(out), checksums, version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); diff --git a/build/next/index.ts b/build/next/index.ts index 97d9a9268874e..8ddbc8f5579d6 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -562,6 +562,11 @@ async function transpile(outDir: string, excludeTests: boolean): Promise { async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: BuildTarget): Promise { await cleanDir(outDir); + // Write build date file (used by packaging to embed in product.json) + const outDirPath = path.join(REPO_ROOT, outDir); + await fs.promises.mkdir(outDirPath, { recursive: true }); + await fs.promises.writeFile(path.join(outDirPath, 'date'), new Date().toISOString(), 'utf8'); + console.log(`[bundle] ${SRC_DIR} → ${outDir} (target: ${target})${doMinify ? ' (minify)' : ''}${doNls ? ' (nls)' : ''}`); const t1 = Date.now(); From cacf5f84fcb4cac341a1f750ee2f363b3028d14f Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 3 Feb 2026 20:36:55 +0100 Subject: [PATCH 21/53] feat: enhance finalizeNLS to support writing to multiple output directories for backwards compatibility --- build/next/index.ts | 7 ++++++- build/next/nls-plugin.ts | 20 ++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index 8ddbc8f5579d6..44700222849b9 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -705,7 +705,12 @@ ${tslib}`, // Finalize NLS: sort entries, assign indices, write metadata files let indexMap = new Map(); if (doNls) { - const nlsResult = await finalizeNLS(nlsCollector, path.join(REPO_ROOT, outDir)); + // Also write NLS files to out-build for backwards compatibility with test runner + const nlsResult = await finalizeNLS( + nlsCollector, + path.join(REPO_ROOT, outDir), + [path.join(REPO_ROOT, 'out-build')] + ); indexMap = nlsResult.indexMap; } diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 1b048269b6f28..e34b1ca7f6a81 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -63,7 +63,8 @@ export function createNLSCollector(): NLSCollector { */ export async function finalizeNLS( collector: NLSCollector, - outDir: string + outDir: string, + alsoWriteTo?: string[] ): Promise<{ indexMap: Map; messageCount: number }> { if (collector.entries.size === 0) { return { indexMap: new Map(), messageCount: 0 }; @@ -115,29 +116,32 @@ export async function finalizeNLS( }; // Write NLS files - await fs.promises.mkdir(outDir, { recursive: true }); + const allOutDirs = [outDir, ...(alsoWriteTo ?? [])]; + for (const dir of allOutDirs) { + await fs.promises.mkdir(dir, { recursive: true }); + } - await Promise.all([ + await Promise.all(allOutDirs.flatMap(dir => [ fs.promises.writeFile( - path.join(outDir, 'nls.messages.json'), + path.join(dir, 'nls.messages.json'), JSON.stringify(allMessages) ), fs.promises.writeFile( - path.join(outDir, 'nls.keys.json'), + path.join(dir, 'nls.keys.json'), JSON.stringify(nlsKeysJson) ), fs.promises.writeFile( - path.join(outDir, 'nls.metadata.json'), + path.join(dir, 'nls.metadata.json'), JSON.stringify(nlsMetadataJson, null, '\t') ), fs.promises.writeFile( - path.join(outDir, 'nls.messages.js'), + path.join(dir, 'nls.messages.js'), `/*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` ) - ]); + ])); console.log(`[nls] Extracted ${allMessages.length} messages from ${moduleToKeys.size} modules`); From ed6fcf28c24668947d6006f97435b9263755d07d Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 4 Feb 2026 07:38:05 +0100 Subject: [PATCH 22/53] feat: add esbuild transpile task with support for excluding test files and customizable output directory --- build/gulpfile.vscode.ts | 27 +++++++++++++++++++++++++++ build/next/index.ts | 19 +++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 7f38ed3c3740f..81fc22f9f0e2e 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -154,6 +154,30 @@ const bundleVSCodeTask = task.define('bundle-vscode', task.series( gulp.task(bundleVSCodeTask); // esbuild-based bundle tasks (drop-in replacement for bundle-vscode / minify-vscode) +function runEsbuildTranspile(outDir: string, excludeTests: boolean): Promise { + return new Promise((resolve, reject) => { + const scriptPath = path.join(root, 'build/next/index.ts'); + const args = [scriptPath, 'transpile', '--out', outDir]; + if (excludeTests) { + args.push('--exclude-tests'); + } + + const proc = cp.spawn(process.execPath, args, { + cwd: root, + stdio: 'inherit' + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`esbuild transpile failed with exit code ${code} (outDir: ${outDir})`)); + } + }); + }); +} + function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: 'desktop' | 'server' | 'server-web' = 'desktop'): Promise { return new Promise((resolve, reject) => { // const tsxPath = path.join(root, 'build/node_modules/tsx/dist/cli.mjs'); @@ -205,6 +229,9 @@ const coreCIEsbuild = task.define('core-ci', task.series( cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, + // Transpile individual files to out-build first (for unit tests) + task.define('esbuild-out-build', () => runEsbuildTranspile('out-build', false)), + // Then bundle for shipping (bundles also write NLS files to out-build) task.parallel( task.define('esbuild-vscode-min', () => runEsbuildBundle('out-vscode-min', true, true, 'desktop')), task.define('esbuild-vscode-reh-min', () => runEsbuildBundle('out-vscode-reh-min', true, true, 'server')), diff --git a/build/next/index.ts b/build/next/index.ts index 44700222849b9..a4cb6e8611981 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -34,6 +34,7 @@ const options = { watch: process.argv.includes('--watch'), minify: process.argv.includes('--minify'), nls: process.argv.includes('--nls'), + excludeTests: process.argv.includes('--exclude-tests'), out: getArgValue('--out'), target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' }; @@ -864,6 +865,8 @@ Commands: Options for 'transpile': --watch Watch for changes and rebuild incrementally + --out Output directory (default: out) + --exclude-tests Exclude test files from transpilation Options for 'bundle': --minify Minify the output bundles @@ -874,6 +877,8 @@ Options for 'bundle': Examples: npx tsx build/next/index.ts transpile npx tsx build/next/index.ts transpile --watch + npx tsx build/next/index.ts transpile --out out-build + npx tsx build/next/index.ts transpile --out out-build --exclude-tests npx tsx build/next/index.ts bundle npx tsx build/next/index.ts bundle --minify --nls npx tsx build/next/index.ts bundle --nls --out out-vscode-min @@ -891,12 +896,18 @@ async function main(): Promise { if (options.watch) { await watch(); } else { - const outDir = OUT_DIR; + const outDir = options.out ?? OUT_DIR; await cleanDir(outDir); - console.log(`[transpile] ${SRC_DIR} → ${outDir}`); + + // Write build date file (used by packaging to embed in product.json) + const outDirPath = path.join(REPO_ROOT, outDir); + await fs.promises.mkdir(outDirPath, { recursive: true }); + await fs.promises.writeFile(path.join(outDirPath, 'date'), new Date().toISOString(), 'utf8'); + + console.log(`[transpile] ${SRC_DIR} → ${outDir}${options.excludeTests ? ' (excluding tests)' : ''}`); const t1 = Date.now(); - await transpile(outDir, false); - await copyResources(outDir, 'desktop', false, false); + await transpile(outDir, options.excludeTests); + await copyResources(outDir, 'desktop', false, options.excludeTests); console.log(`[transpile] Done in ${Date.now() - t1}ms`); } break; From 7cf0cec19821590576502e50c8f335b8e1d2ce9e Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 4 Feb 2026 09:42:00 +0100 Subject: [PATCH 23/53] feat: enhance build process with product configuration and built-in extensions scanning --- build/next/index.ts | 113 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index a4cb6e8611981..7500c56bb7c87 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -10,6 +10,9 @@ import { promisify } from 'util'; import glob from 'glob'; import * as watcher from '@parcel/watcher'; import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts'; +import { getVersion } from '../lib/getVersion.ts'; +import product from '../../product.json' with { type: 'json' }; +import packageJson from '../../package.json' with { type: 'json' }; const globAsync = promisify(glob); @@ -18,6 +21,9 @@ const globAsync = promisify(glob); // ============================================================================ const REPO_ROOT = path.dirname(path.dirname(import.meta.dirname)); +const commit = getVersion(REPO_ROOT); +const quality = (product as { quality?: string }).quality; +const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; // CLI: transpile [--watch] | bundle [--minify] [--nls] [--out ] const command = process.argv[2]; // 'transpile' or 'bundle' @@ -340,6 +346,94 @@ async function cleanDir(dir: string): Promise { await fs.promises.mkdir(fullPath, { recursive: true }); } +/** + * Scan for built-in extensions in the given directory. + * Returns an array of extension entries for the builtinExtensionsScannerService. + */ +function scanBuiltinExtensions(extensionsRoot: string): Array<{ extensionPath: string; packageJSON: unknown }> { + const result: Array<{ extensionPath: string; packageJSON: unknown }> = []; + const extensionsPath = path.join(REPO_ROOT, extensionsRoot); + + if (!fs.existsSync(extensionsPath)) { + return result; + } + + for (const entry of fs.readdirSync(extensionsPath, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const packageJsonPath = path.join(extensionsPath, entry.name, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + result.push({ + extensionPath: entry.name, + packageJSON + }); + } catch (e) { + // Skip invalid extensions + } + } + } + + return result; +} + +/** + * Get the date from the out directory date file, or return current date. + */ +function readISODate(outDir: string): string { + try { + return fs.readFileSync(path.join(REPO_ROOT, outDir, 'date'), 'utf8'); + } catch { + return new Date().toISOString(); + } +} + +/** + * Creates a file content mapper for builds. + * This transforms JS files to inject build-time configuration. + * The placeholders get bundled into all entry points that import these modules. + */ +function createFileContentMapper(outDir: string, target: BuildTarget): (filePath: string, content: string) => string { + // Cache the replacement strings (computed once) + let productConfigReplacement: string | undefined; + let builtinExtensionsReplacement: string | undefined; + + return (_filePath: string, content: string): string => { + // Inject product configuration (placeholder gets bundled into many files) + if (content.includes('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/')) { + if (productConfigReplacement === undefined) { + // For server-web, remove webEndpointUrlTemplate + const productForTarget = target === 'server-web' + ? { ...product, webEndpointUrlTemplate: undefined } + : product; + const productConfiguration = JSON.stringify({ + ...productForTarget, + version, + commit, + date: readISODate(outDir) + }); + // Remove the outer braces since the placeholder is inside an object literal + productConfigReplacement = productConfiguration.substring(1, productConfiguration.length - 1); + } + content = content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfigReplacement!); + } + + // Inject built-in extensions list (placeholder gets bundled into files importing the scanner) + if (content.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) { + if (builtinExtensionsReplacement === undefined) { + const builtinExtensions = JSON.stringify(scanBuiltinExtensions('.build/extensions')); + // Remove the outer brackets since the placeholder is inside an array literal + builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1); + } + content = content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensionsReplacement!); + } + + return content; + }; +} + /** * Only used to make encoding tests happy. The source files don't have a BOM but the * tests expect one... so we add it here. @@ -715,6 +809,9 @@ ${tslib}`, indexMap = nlsResult.indexMap; } + // Create file content mapper for web builds (injects product config, builtin extensions) + const fileContentMapper = createFileContentMapper(outDir, target); + // Post-process and write all output files let bundled = 0; for (const { result } of buildResults) { @@ -725,10 +822,18 @@ ${tslib}`, for (const file of result.outputFiles) { await fs.promises.mkdir(path.dirname(file.path), { recursive: true }); - if (doNls && file.path.endsWith('.js') && indexMap.size > 0) { - // Post-process JS files to replace NLS placeholders with indices - const processed = postProcessNLS(file.text, indexMap, preserveEnglish); - await fs.promises.writeFile(file.path, processed); + if (file.path.endsWith('.js')) { + let content = file.text; + + // Apply NLS post-processing if enabled + if (doNls && indexMap.size > 0) { + content = postProcessNLS(content, indexMap, preserveEnglish); + } + + // Apply file content mapping (product config, builtin extensions) + content = fileContentMapper(file.path, content); + + await fs.promises.writeFile(file.path, content); } else { // Write other files (source maps, etc.) as-is await fs.promises.writeFile(file.path, file.contents); From ca10b50e6b86abf3ab19f2bd08bbb4a60e6708df Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 4 Feb 2026 11:39:01 +0100 Subject: [PATCH 24/53] fix: update regex in replaceInOutput to handle both single and double quotes for NLS placeholders --- build/next/nls-plugin.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index e34b1ca7f6a81..0601f9c2fdbf2 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -223,10 +223,11 @@ function replaceInOutput( // Two types of placeholders: // - %%NLS:moduleId#key%% for localize() - message replaced with null // - %%NLS2:moduleId#key%% for localize2() - message preserved + // Note: esbuild may use single or double quotes, so we handle both if (preserveEnglish) { // Just replace the placeholder with the index (both NLS and NLS2) - return content.replace(/"%%NLS2?:([^%]+)%%"/g, (match, inner) => { + return content.replace(/["']%%NLS2?:([^%]+)%%["']/g, (match, inner) => { // Try NLS first, then NLS2 let placeholder = `%%NLS:${inner}%%`; let index = indexMap.get(placeholder); @@ -244,10 +245,11 @@ function replaceInOutput( // For NLS (localize): replace placeholder with index AND replace message with null // For NLS2 (localize2): replace placeholder with index, keep message // Note: Use (?:[^"\\]|\\.)* to properly handle escaped quotes like \" or \\ + // Note: esbuild may use single or double quotes, so we handle both // First handle NLS (localize) - replace both key and message content = content.replace( - /"%%NLS:([^%]+)%%"(\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, + /["']%%NLS:([^%]+)%%["'](\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, (match, inner, comma) => { const placeholder = `%%NLS:${inner}%%`; const index = indexMap.get(placeholder); @@ -260,7 +262,7 @@ function replaceInOutput( // Then handle NLS2 (localize2) - replace only key, keep message content = content.replace( - /"%%NLS2:([^%]+)%%"/g, + /["']%%NLS2:([^%]+)%%["']/g, (match, inner) => { const placeholder = `%%NLS2:${inner}%%`; const index = indexMap.get(placeholder); From 02e60e2be0bf2797746e5e24bc61bf91439ce928 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 4 Feb 2026 16:51:45 +0100 Subject: [PATCH 25/53] feat: add esbuild-based bundling tasks for standalone web target --- build/gulpfile.vscode.web.ts | 39 ++++++++++++++++++++++++--- build/next/index.ts | 51 +++++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index 3f1cc1fdc51f0..7ff53eefc52b8 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -5,6 +5,7 @@ import gulp from 'gulp'; import * as path from 'path'; +import * as cp from 'child_process'; import es from 'event-stream'; import * as util from './lib/util.ts'; import { getVersion } from './lib/getVersion.ts'; @@ -30,6 +31,34 @@ const commit = getVersion(REPO_ROOT); const quality = (product as { quality?: string }).quality; const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; +// esbuild-based bundle for standalone web +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promise { + return new Promise((resolve, reject) => { + const scriptPath = path.join(REPO_ROOT, 'build/next/index.ts'); + const args = [scriptPath, 'bundle', '--out', outDir, '--target', 'web']; + if (minify) { + args.push('--minify'); + } + if (nls) { + args.push('--nls'); + } + + const proc = cp.spawn(process.execPath, args, { + cwd: REPO_ROOT, + stdio: 'inherit' + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`esbuild web bundle failed with exit code ${code} (outDir: ${outDir}, minify: ${minify}, nls: ${nls})`)); + } + }); + }); +} + export const vscodeWebResourceIncludes = [ // NLS @@ -110,7 +139,7 @@ export const createVSCodeWebFileContentMapper = (extensionsRoot: string, product }; }; -const bundleVSCodeWebTask = task.define('bundle-vscode-web', task.series( +const bundleVSCodeWebTask = task.define('bundle-vscode-web-OLD', task.series( util.rimraf('out-vscode-web'), optimize.bundleTask( { @@ -125,13 +154,17 @@ const bundleVSCodeWebTask = task.define('bundle-vscode-web', task.series( ) )); -const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( +const minifyVSCodeWebTask = task.define('minify-vscode-web-OLD', task.series( bundleVSCodeWebTask, util.rimraf('out-vscode-web-min'), optimize.minifyTask('out-vscode-web', `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) )); gulp.task(minifyVSCodeWebTask); +// esbuild-based tasks (new) +const esbuildBundleVSCodeWebTask = task.define('esbuild-vscode-web', () => runEsbuildBundle('out-vscode-web', false, true)); +const esbuildBundleVSCodeWebMinTask = task.define('esbuild-vscode-web-min', () => runEsbuildBundle('out-vscode-web-min', true, true)); + function packageTask(sourceFolderName: string, destinationFolderName: string) { const destination = path.join(BUILD_ROOT, destinationFolderName); @@ -198,7 +231,7 @@ const dashed = (str: string) => (str ? `-${str}` : ``); const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( compileWebExtensionsBuildTask, - minified ? minifyVSCodeWebTask : bundleVSCodeWebTask, + minified ? esbuildBundleVSCodeWebMinTask : esbuildBundleVSCodeWebTask, util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), packageTask(sourceFolderName, destinationFolderName) )); diff --git a/build/next/index.ts b/build/next/index.ts index 7500c56bb7c87..7de811b854b41 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -42,11 +42,11 @@ const options = { nls: process.argv.includes('--nls'), excludeTests: process.argv.includes('--exclude-tests'), out: getArgValue('--out'), - target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' + target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' | 'web' }; // Build targets -type BuildTarget = 'desktop' | 'server' | 'server-web'; +type BuildTarget = 'desktop' | 'server' | 'server-web' | 'web'; const SRC_DIR = 'src'; const OUT_DIR = 'out'; @@ -147,6 +147,12 @@ function getEntryPointsForTarget(target: BuildTarget): string[] { ...webEntryPoints, ...keyboardMapEntryPoints, ]; + case 'web': + return [ + ...workerEntryPoints, + 'vs/workbench/workbench.web.main.internal', // web workbench only (no browser shell) + ...keyboardMapEntryPoints, + ]; default: throw new Error(`Unknown target: ${target}`); } @@ -162,6 +168,8 @@ function getBootstrapEntryPointsForTarget(target: BuildTarget): string[] { case 'server': case 'server-web': return bootstrapEntryPointsServer; + case 'web': + return []; // Web has no bootstrap files (served by external server) default: throw new Error(`Unknown target: ${target}`); } @@ -186,6 +194,10 @@ function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { 'vs/workbench/workbench.web.main.internal', 'vs/code/browser/workbench/workbench', ]); + case 'web': + return new Set([ + 'vs/workbench/workbench.web.main.internal', + ]); default: throw new Error(`Unknown target: ${target}`); } @@ -297,6 +309,32 @@ const serverWebResourcePatterns = [ 'vs/workbench/services/extensionManagement/common/media/*.png', ]; +// Resources for standalone web target (browser-only, no server) +const webResourcePatterns = [ + ...commonResourcePatterns, + + // Web HTML + 'vs/code/browser/workbench/workbench.html', + 'vs/code/browser/workbench/workbench-dev.html', + 'vs/code/browser/workbench/callback.html', + 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + 'vs/workbench/contrib/webview/browser/pre/*.html', + + // Webview pre scripts + 'vs/workbench/contrib/webview/browser/pre/*.js', + + // Media - audio + 'vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Media - images + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', + 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', + 'vs/workbench/contrib/extensions/browser/media/*.svg', + 'vs/workbench/contrib/extensions/browser/media/*.png', + 'vs/workbench/services/extensionManagement/common/media/*.svg', + 'vs/workbench/services/extensionManagement/common/media/*.png', +]; + /** * Get resource patterns for a build target. */ @@ -308,6 +346,8 @@ function getResourcePatternsForTarget(target: BuildTarget): string[] { return serverResourcePatterns; case 'server-web': return serverWebResourcePatterns; + case 'web': + return webResourcePatterns; default: throw new Error(`Unknown target: ${target}`); } @@ -423,7 +463,10 @@ function createFileContentMapper(outDir: string, target: BuildTarget): (filePath // Inject built-in extensions list (placeholder gets bundled into files importing the scanner) if (content.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) { if (builtinExtensionsReplacement === undefined) { - const builtinExtensions = JSON.stringify(scanBuiltinExtensions('.build/extensions')); + // Web target uses .build/web/extensions (from compileWebExtensionsBuildTask) + // Other targets use .build/extensions + const extensionsRoot = target === 'web' ? '.build/web/extensions' : '.build/extensions'; + const builtinExtensions = JSON.stringify(scanBuiltinExtensions(extensionsRoot)); // Remove the outer brackets since the placeholder is inside an array literal builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1); } @@ -977,7 +1020,7 @@ Options for 'bundle': --minify Minify the output bundles --nls Process NLS (localization) strings --out Output directory (default: out-vscode) - --target Build target: desktop (default), server, server-web + --target Build target: desktop (default), server, server-web, web Examples: npx tsx build/next/index.ts transpile From 467980a4bd312f29b29255372fadb1d9e48f02fb Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 4 Feb 2026 18:20:59 +0100 Subject: [PATCH 26/53] remove query arg from codicon.css --- src/vs/base/browser/ui/codicons/codicon/codicon.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.css b/src/vs/base/browser/ui/codicons/codicon/codicon.css index 02154e77b6895..d7f257db9341e 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon.css +++ b/src/vs/base/browser/ui/codicons/codicon/codicon.css @@ -6,7 +6,7 @@ @font-face { font-family: "codicon"; font-display: block; - src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype"); + src: url("./codicon.ttf") format("truetype"); } .codicon[class*='codicon-'] { From ae8e3c31783531f536759b9476a0d7ee76bdc4d8 Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 5 Feb 2026 09:24:08 +0100 Subject: [PATCH 27/53] feat: add copyCodiconsTask to vscode-web CI build process --- build/gulpfile.vscode.web.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index 7ff53eefc52b8..cdaf57000fac1 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -19,6 +19,7 @@ import { getProductionDependencies } from './lib/dependencies.ts'; import vfs from 'vinyl-fs'; import packageJson from '../package.json' with { type: 'json' }; import { compileBuildWithManglingTask } from './gulpfile.compile.ts'; +import { copyCodiconsTask } from './lib/compilation.ts'; import * as extensions from './lib/extensions.ts'; import jsonEditor from 'gulp-json-editor'; import buildfile from './buildfile.ts'; @@ -230,6 +231,7 @@ const dashed = (str: string) => (str ? `-${str}` : ``); const destinationFolderName = `vscode-web`; const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( + copyCodiconsTask, compileWebExtensionsBuildTask, minified ? esbuildBundleVSCodeWebMinTask : esbuildBundleVSCodeWebTask, util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), From 772a2d094e5f4860486e8e9bd5e38dbd3799cebc Mon Sep 17 00:00:00 2001 From: Johannes Date: Thu, 5 Feb 2026 18:38:36 +0100 Subject: [PATCH 28/53] Refactor build system: replace createFileContentMapper with fileContentMapperPlugin for better integration with esbuild --- build/next/index.ts | 128 ++++++++++++++++++++++++------------------ build/next/working.md | 107 +++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 54 deletions(-) create mode 100644 build/next/working.md diff --git a/build/next/index.ts b/build/next/index.ts index 7de811b854b41..bd499363c62d2 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -430,53 +430,6 @@ function readISODate(outDir: string): string { } } -/** - * Creates a file content mapper for builds. - * This transforms JS files to inject build-time configuration. - * The placeholders get bundled into all entry points that import these modules. - */ -function createFileContentMapper(outDir: string, target: BuildTarget): (filePath: string, content: string) => string { - // Cache the replacement strings (computed once) - let productConfigReplacement: string | undefined; - let builtinExtensionsReplacement: string | undefined; - - return (_filePath: string, content: string): string => { - // Inject product configuration (placeholder gets bundled into many files) - if (content.includes('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/')) { - if (productConfigReplacement === undefined) { - // For server-web, remove webEndpointUrlTemplate - const productForTarget = target === 'server-web' - ? { ...product, webEndpointUrlTemplate: undefined } - : product; - const productConfiguration = JSON.stringify({ - ...productForTarget, - version, - commit, - date: readISODate(outDir) - }); - // Remove the outer braces since the placeholder is inside an object literal - productConfigReplacement = productConfiguration.substring(1, productConfiguration.length - 1); - } - content = content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfigReplacement!); - } - - // Inject built-in extensions list (placeholder gets bundled into files importing the scanner) - if (content.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) { - if (builtinExtensionsReplacement === undefined) { - // Web target uses .build/web/extensions (from compileWebExtensionsBuildTask) - // Other targets use .build/extensions - const extensionsRoot = target === 'web' ? '.build/web/extensions' : '.build/extensions'; - const builtinExtensions = JSON.stringify(scanBuiltinExtensions(extensionsRoot)); - // Remove the outer brackets since the placeholder is inside an array literal - builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1); - } - content = content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensionsReplacement!); - } - - return content; - }; -} - /** * Only used to make encoding tests happy. The source files don't have a BOM but the * tests expect one... so we add it here. @@ -641,6 +594,74 @@ function cssExternalPlugin(): esbuild.Plugin { }; } +/** + * esbuild plugin that transforms source files to inject build-time configuration. + * This runs during onLoad so the transformation happens before esbuild processes the content, + * ensuring placeholders like `/*BUILD->INSERT_PRODUCT_CONFIGURATION* /` are replaced + * before esbuild strips them as non-legal comments. + */ +function fileContentMapperPlugin(outDir: string, target: BuildTarget): esbuild.Plugin { + // Cache the replacement strings (computed once) + let productConfigReplacement: string | undefined; + let builtinExtensionsReplacement: string | undefined; + + return { + name: 'file-content-mapper', + setup(build) { + build.onLoad({ filter: /\.ts$/ }, async (args) => { + // Skip .d.ts files + if (args.path.endsWith('.d.ts')) { + return undefined; + } + + let contents = await fs.promises.readFile(args.path, 'utf-8'); + let modified = false; + + // Inject product configuration + if (contents.includes('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/')) { + if (productConfigReplacement === undefined) { + // For server-web, remove webEndpointUrlTemplate + const productForTarget = target === 'server-web' + ? { ...product, webEndpointUrlTemplate: undefined } + : product; + const productConfiguration = JSON.stringify({ + ...productForTarget, + version, + commit, + date: readISODate(outDir) + }); + // Remove the outer braces since the placeholder is inside an object literal + productConfigReplacement = productConfiguration.substring(1, productConfiguration.length - 1); + } + contents = contents.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfigReplacement!); + modified = true; + } + + // Inject built-in extensions list + if (contents.includes('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/')) { + if (builtinExtensionsReplacement === undefined) { + // Web target uses .build/web/extensions (from compileWebExtensionsBuildTask) + // Other targets use .build/extensions + const extensionsRoot = target === 'web' ? '.build/web/extensions' : '.build/extensions'; + const builtinExtensions = JSON.stringify(scanBuiltinExtensions(extensionsRoot)); + // Remove the outer brackets since the placeholder is inside an array literal + builtinExtensionsReplacement = builtinExtensions.substring(1, builtinExtensions.length - 1); + } + contents = contents.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensionsReplacement!); + modified = true; + } + + if (modified) { + return { contents, loader: 'ts' }; + } + + // No modifications, let esbuild handle normally + return undefined; + }); + }, + }; +} + // ============================================================================ // Transpile (Goal 1: TS → JS using esbuild.transform for maximum speed) // ============================================================================ @@ -741,6 +762,9 @@ ${tslib}`, // Collect all build results (with write: false) const buildResults: { outPath: string; result: esbuild.BuildResult }[] = []; + // Create the file content mapper plugin (injects product config, builtin extensions) + const contentMapperPlugin = fileContentMapperPlugin(outDir, target); + // Bundle each entry point directly from TypeScript source await Promise.all(allEntryPoints.map(async (entryPoint) => { const entryPath = path.join(REPO_ROOT, SRC_DIR, `${entryPoint}.ts`); @@ -748,6 +772,8 @@ ${tslib}`, // Use CSS external plugin for entry points that don't need bundled CSS const plugins: esbuild.Plugin[] = bundleCssEntryPoints.has(entryPoint) ? [] : [cssExternalPlugin()]; + // Add content mapper plugin to inject product config and builtin extensions + plugins.push(contentMapperPlugin); if (doNls) { plugins.unshift(nlsPlugin({ baseDir: path.join(REPO_ROOT, SRC_DIR), @@ -807,7 +833,7 @@ ${tslib}`, const outPath = path.join(REPO_ROOT, outDir, `${entry}.js`); - const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin()]; + const bootstrapPlugins: esbuild.Plugin[] = [inlineMinimistPlugin(), contentMapperPlugin]; if (doNls) { bootstrapPlugins.unshift(nlsPlugin({ baseDir: path.join(REPO_ROOT, SRC_DIR), @@ -852,9 +878,6 @@ ${tslib}`, indexMap = nlsResult.indexMap; } - // Create file content mapper for web builds (injects product config, builtin extensions) - const fileContentMapper = createFileContentMapper(outDir, target); - // Post-process and write all output files let bundled = 0; for (const { result } of buildResults) { @@ -873,9 +896,6 @@ ${tslib}`, content = postProcessNLS(content, indexMap, preserveEnglish); } - // Apply file content mapping (product config, builtin extensions) - content = fileContentMapper(file.path, content); - await fs.promises.writeFile(file.path, content); } else { // Write other files (source maps, etc.) as-is diff --git a/build/next/working.md b/build/next/working.md new file mode 100644 index 0000000000000..dcd2ee4d68e50 --- /dev/null +++ b/build/next/working.md @@ -0,0 +1,107 @@ +# Working Notes: New esbuild-based Build System + +> These notes are for AI agents to help with context in new or summarized sessions. + +## Important: Validating Changes + +**The `VS Code - Build` task is NOT needed to validate changes in the `build/` folder!** + +Build scripts in `build/` are TypeScript files that run directly with `tsx` (e.g., `npx tsx build/next/index.ts`). They are not compiled by the main VS Code build. + +To test changes: +```bash +# Test transpile +npx tsx build/next/index.ts transpile --out out-test + +# Test bundle (server-web target to test the auth fix) +npx tsx build/next/index.ts bundle --nls --target server-web --out out-vscode-reh-web-test + +# Verify product config was injected +grep -l "serverLicense" out-vscode-reh-web-test/vs/code/browser/workbench/workbench.js +``` + +--- + +## Architecture Overview + +### Files + +- **[index.ts](index.ts)** - Main build orchestrator + - `transpile` command: Fast TS → JS using `esbuild.transform()` + - `bundle` command: TS → bundled JS using `esbuild.build()` +- **[nls-plugin.ts](nls-plugin.ts)** - NLS (localization) esbuild plugin + +### Integration with Old Build + +In [gulpfile.vscode.ts](../gulpfile.vscode.ts#L228-L242), the `core-ci` task uses these new scripts: +- `runEsbuildTranspile()` → transpile command +- `runEsbuildBundle()` → bundle command + +Old gulp-based bundling renamed to `core-ci-OLD`. + +--- + +## Key Learnings + +### 1. Comment Stripping by esbuild + +**Problem:** esbuild strips comments like `/*BUILD->INSERT_PRODUCT_CONFIGURATION*/` during bundling. + +**Solution:** Use an `onLoad` plugin to transform source files BEFORE esbuild processes them. See `fileContentMapperPlugin()` in index.ts. + +**Why post-processing doesn't work:** By the time we post-process the bundled output, the comment placeholder has already been stripped. + +### 2. Authorization Error: "Unauthorized client refused" + +**Root cause:** Missing product configuration in browser bundle. + +**Flow:** +1. Browser loads with empty product config (placeholder was stripped) +2. `productService.serverLicense` is empty/undefined +3. Browser's `SignService.vsda()` can't decrypt vsda WASM (needs serverLicense as key) +4. Browser's `sign()` returns original challenge instead of signed value +5. Server validates signature → fails +6. Server is in built mode (no `VSCODE_DEV`) → rejects connection + +**Fix:** The `fileContentMapperPlugin` now runs during `onLoad`, replacing placeholders before esbuild strips them. + +### 3. Build-Time Placeholders + +Two placeholders that need injection: + +| Placeholder | Location | Purpose | +|-------------|----------|---------| +| `/*BUILD->INSERT_PRODUCT_CONFIGURATION*/` | `src/vs/platform/product/common/product.ts` | Product config (commit, version, serverLicense, etc.) | +| `/*BUILD->INSERT_BUILTIN_EXTENSIONS*/` | `src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts` | List of built-in extensions | + +### 4. Server-web Target Specifics + +- Removes `webEndpointUrlTemplate` from product config (see `tweakProductForServerWeb` in old build) +- Uses `.build/extensions` for builtin extensions (not `.build/web/extensions`) + +--- + +## Testing the Fix + +```bash +# Build server-web with new system +npx tsx build/next/index.ts bundle --nls --target server-web --out out-vscode-reh-web-min + +# Package it (uses gulp task) +npm run gulp vscode-reh-web-darwin-arm64-min + +# Run server +./vscode-server-darwin-arm64-web/bin/code-server-oss --connection-token dev-token + +# Open browser - should connect without "Unauthorized client refused" +``` + +--- + +## Open Items / Future Work + +1. **`BUILD_INSERT_PACKAGE_CONFIGURATION`** - Server bootstrap files ([bootstrap-meta.ts](../../src/bootstrap-meta.ts)) have this marker for package.json injection. Currently handled by [inlineMeta.ts](../lib/inlineMeta.ts) in the old build's packaging step. + +2. **Mangling** - The new build doesn't do TypeScript-based mangling yet. Old `core-ci` with mangling is now `core-ci-OLD`. + +3. **Entry point duplication** - Entry points are duplicated between [buildfile.ts](../buildfile.ts) and [index.ts](index.ts). Consider consolidating. From 878df8b75520013cd844b32285fa19e6d0b2286f Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 6 Feb 2026 11:36:52 +0100 Subject: [PATCH 29/53] Refactor build system to support esbuild with new transpile and typecheck tasks --- .vscode/tasks.json | 50 ++++++++++++++++++++++++++++++++++++---- build/gulpfile.ts | 2 +- build/lib/compilation.ts | 10 +++++--- build/next/index.ts | 9 +++++--- build/next/working.md | 20 ++++++++++++++++ package.json | 3 +++ 6 files changed, 83 insertions(+), 11 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f601633b570d4..8ea2442130836 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,10 +1,38 @@ { "version": "2.0.0", "tasks": [ + { + "type": "npm", + "script": "watch-client-transpiled", + "label": "Core - Transpile", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "buildWatchers", + "close": false + }, + "problemMatcher": { + "owner": "esbuild", + "applyTo": "closedDocuments", + "fileLocation": [ + "absolute" + ], + "pattern": { + "regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$", + "file": 1, + "location": 2, + "message": 3 + }, + "background": { + "beginsPattern": "Starting transpilation...", + "endsPattern": "Finished transpilation with" + } + } + }, { "type": "npm", "script": "watch-clientd", - "label": "Core - Build", + "label": "Core - Typecheck", "isBackground": true, "presentation": { "reveal": "never", @@ -60,7 +88,8 @@ { "label": "VS Code - Build", "dependsOn": [ - "Core - Build", + "Core - Transpile", + "Core - Typecheck", "Ext - Build" ], "group": { @@ -69,10 +98,22 @@ }, "problemMatcher": [] }, + { + "type": "npm", + "script": "kill-watch-client-transpiled", + "label": "Kill Core - Transpile", + "group": "build", + "presentation": { + "reveal": "never", + "group": "buildKillers", + "close": true + }, + "problemMatcher": "$tsc" + }, { "type": "npm", "script": "kill-watch-clientd", - "label": "Kill Core - Build", + "label": "Kill Core - Typecheck", "group": "build", "presentation": { "reveal": "never", @@ -96,7 +137,8 @@ { "label": "Kill VS Code - Build", "dependsOn": [ - "Kill Core - Build", + "Kill Core - Transpile", + "Kill Core - Typecheck", "Kill Ext - Build" ], "group": "build", diff --git a/build/gulpfile.ts b/build/gulpfile.ts index a57218b844517..9ee544f006998 100644 --- a/build/gulpfile.ts +++ b/build/gulpfile.ts @@ -32,7 +32,7 @@ gulp.task(transpileClientTask); const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out', false))); gulp.task(compileClientTask); -const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask))); +const watchClientTask = task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)); gulp.task(watchClientTask); // All diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index f440dc28dd0c2..2484e884a5512 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -49,14 +49,18 @@ interface ICompileTaskOptions { readonly emitError: boolean; readonly transpileOnly: boolean | { esbuild: boolean }; readonly preserveEnglish: boolean; + readonly noEmit?: boolean; } -export function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { +export function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish, noEmit }: ICompileTaskOptions) { const projectPath = path.join(import.meta.dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; if (!build) { overrideOptions.inlineSourceMap = true; } + if (noEmit) { + overrideOptions.noEmit = true; + } const compilation = tsb.create(projectPath, overrideOptions, { verbose: false, @@ -163,10 +167,10 @@ export function compileTask(src: string, out: string, build: boolean, options: { return task; } -export function watchTask(out: string, build: boolean, srcPath: string = 'src'): task.StreamTask { +export function watchTask(out: string, build: boolean, srcPath: string = 'src', options?: { noEmit?: boolean }): task.StreamTask { const task = () => { - const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false }); + const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false, noEmit: options?.noEmit }); const src = gulp.src(`${srcPath}/**`, { base: srcPath }); const watchSrc = watch(`${srcPath}/**`, { base: srcPath, readDelay: 200 }); diff --git a/build/next/index.ts b/build/next/index.ts index bd499363c62d2..2b926a1e5e6f6 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -919,7 +919,7 @@ ${tslib}`, // ============================================================================ async function watch(): Promise { - console.log('[watch] Starting...'); + console.log('Starting transpilation...'); const outDir = OUT_DIR; @@ -932,9 +932,10 @@ async function watch(): Promise { try { await transpile(outDir, false); await copyResources(outDir, 'desktop', false, false); - console.log(`[transpile] Done in ${Date.now() - t1}ms`); + console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`); } catch (err) { console.error('[watch] Initial build failed:', err); + console.log(`Finished transpilation with 1 errors after ${Date.now() - t1} ms`); // Continue watching anyway } @@ -942,6 +943,7 @@ async function watch(): Promise { let pendingCopyFiles: Set = new Set(); const processChanges = async () => { + console.log('Starting transpilation...'); const t1 = Date.now(); const tsFiles = [...pendingTsFiles]; const filesToCopy = [...pendingCopyFiles]; @@ -971,10 +973,11 @@ async function watch(): Promise { } if (tsFiles.length > 0 || filesToCopy.length > 0) { - console.log(`[watch] Done in ${Date.now() - t1}ms`); + console.log(`Finished transpilation with 0 errors after ${Date.now() - t1} ms`); } } catch (err) { console.error('[watch] Rebuild failed:', err); + console.log(`Finished transpilation with 1 errors after ${Date.now() - t1} ms`); // Continue watching } }; diff --git a/build/next/working.md b/build/next/working.md index dcd2ee4d68e50..f9ebdd40d040b 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -105,3 +105,23 @@ npm run gulp vscode-reh-web-darwin-arm64-min 2. **Mangling** - The new build doesn't do TypeScript-based mangling yet. Old `core-ci` with mangling is now `core-ci-OLD`. 3. **Entry point duplication** - Entry points are duplicated between [buildfile.ts](../buildfile.ts) and [index.ts](index.ts). Consider consolidating. + +--- + +## Self-hosting Setup + +The default `VS Code - Build` task now runs three parallel watchers: + +| Task | What it does | Script | +|------|-------------|--------| +| **Core - Transpile** | esbuild single-file TS→JS (fast, no type checking) | `watch-client-transpiled` → `npx tsx build/next/index.ts transpile --watch` | +| **Core - Typecheck** | gulp-tsb `noEmit` watch (type errors only, no output) | `watch-clientd` → `gulp watch-client` (with `noEmit: true`) | +| **Ext - Build** | Extension compilation (unchanged) | `watch-extensionsd` | + +### Key Changes + +- **`compilation.ts`**: `ICompileTaskOptions` gained `noEmit?: boolean`. When set, `overrideOptions.noEmit = true` is passed to tsb. `watchTask()` accepts an optional 4th parameter `{ noEmit?: boolean }`. +- **`gulpfile.ts`**: `watchClientTask` no longer runs `rimraf('out')` (the transpiler owns that). Passes `{ noEmit: true }` to `watchTask`. +- **`index.ts`**: Watch mode emits `Starting transpilation...` / `Finished transpilation with N errors after X ms` for VS Code problem matcher. +- **`tasks.json`**: Old "Core - Build" split into "Core - Transpile" + "Core - Typecheck" with separate problem matchers (owners: `esbuild` vs `typescript`). +- **`package.json`**: Added `watch-client-transpile`, `watch-client-transpiled`, `kill-watch-client-transpiled` scripts. diff --git a/package.json b/package.json index ccd707811dafe..548147e4fba6f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "watch-client": "npm run gulp watch-client", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", + "watch-client-transpile": "npx tsx build/next/index.ts transpile --watch", + "watch-client-transpiled": "deemon npm run watch-client-transpile", + "kill-watch-client-transpiled": "deemon --kill npm run watch-client-transpile", "watch-extensions": "npm run gulp watch-extensions watch-extension-media", "watch-extensionsd": "deemon npm run watch-extensions", "kill-watch-extensionsd": "deemon --kill npm run watch-extensions", From 696c09b0615a20a5df2131354b6009c56d241f1d Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 9 Feb 2026 10:53:56 +0100 Subject: [PATCH 30/53] add --- build/buildConfig.ts | 12 ++++++++++++ build/gulpfile.ts | 5 ++++- build/next/index.ts | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 build/buildConfig.ts diff --git a/build/buildConfig.ts b/build/buildConfig.ts new file mode 100644 index 0000000000000..a4299d2647fe2 --- /dev/null +++ b/build/buildConfig.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * When `true`, self-hosting uses esbuild for fast transpilation (build/next) + * and gulp-tsb only for type-checking (`noEmit`). + * + * When `false`, gulp-tsb does both transpilation and type-checking (old behavior). + */ +export const useEsbuildTranspile = true; diff --git a/build/gulpfile.ts b/build/gulpfile.ts index 9ee544f006998..1c21618ca608f 100644 --- a/build/gulpfile.ts +++ b/build/gulpfile.ts @@ -13,6 +13,7 @@ import { compileExtensionMediaTask, compileExtensionsTask, watchExtensionsTask } import * as compilation from './lib/compilation.ts'; import * as task from './lib/task.ts'; import * as util from './lib/util.ts'; +import { useEsbuildTranspile } from './buildConfig.ts'; const require = createRequire(import.meta.url); @@ -32,7 +33,9 @@ gulp.task(transpileClientTask); const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compilation.compileApiProposalNamesTask, compilation.compileTask('src', 'out', false))); gulp.task(compileClientTask); -const watchClientTask = task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)); +const watchClientTask = useEsbuildTranspile + ? task.define('watch-client', task.parallel(compilation.watchTask('out', false, 'src', { noEmit: true }), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask)) + : task.define('watch-client', task.series(util.rimraf('out'), task.parallel(compilation.watchTask('out', false), compilation.watchApiProposalNamesTask, compilation.watchCodiconsTask))); gulp.task(watchClientTask); // All diff --git a/build/next/index.ts b/build/next/index.ts index 2b926a1e5e6f6..e7978d726cc6c 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -13,6 +13,7 @@ import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nl import { getVersion } from '../lib/getVersion.ts'; import product from '../../product.json' with { type: 'json' }; import packageJson from '../../package.json' with { type: 'json' }; +import { useEsbuildTranspile } from '../buildConfig.ts'; const globAsync = promisify(glob); @@ -919,6 +920,14 @@ ${tslib}`, // ============================================================================ async function watch(): Promise { + if (!useEsbuildTranspile) { + console.log('Starting transpilation...'); + console.log('Finished transpilation with 0 errors after 0 ms'); + console.log('[watch] esbuild transpile disabled (useEsbuildTranspile=false). Keeping process alive as no-op.'); + await new Promise(() => { }); // keep alive + return; + } + console.log('Starting transpilation...'); const outDir = OUT_DIR; From d27f42a111227b4654454222758f21afd7fc2762 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 9 Feb 2026 12:29:01 +0100 Subject: [PATCH 31/53] use --- build/gulpfile.vscode.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 41eee043f8da9..f13c68ed3ba16 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -31,6 +31,7 @@ import minimist from 'minimist'; import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts'; import { copyCodiconsTask } from './lib/compilation.ts'; +import { useEsbuildTranspile } from './buildConfig.ts'; import { promisify } from 'util'; import globCallback from 'glob'; import rceditCallback from 'rcedit'; @@ -215,7 +216,7 @@ const minifyVSCodeTask = task.define('minify-vscode', task.series( )); gulp.task(minifyVSCodeTask); -const coreCI = task.define('core-ci-OLD', task.series( +const coreCIOld = task.define('core-ci-old', task.series( gulp.task('compile-build-with-mangling') as task.Task, task.parallel( gulp.task('minify-vscode') as task.Task, @@ -223,9 +224,9 @@ const coreCI = task.define('core-ci-OLD', task.series( gulp.task('minify-vscode-reh-web') as task.Task, ) )); -gulp.task(coreCI); +gulp.task(coreCIOld); -const coreCIEsbuild = task.define('core-ci', task.series( +const coreCIEsbuild = task.define('core-ci-esbuild', task.series( copyCodiconsTask, cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, @@ -241,6 +242,8 @@ const coreCIEsbuild = task.define('core-ci', task.series( )); gulp.task(coreCIEsbuild); +gulp.task(task.define('core-ci', useEsbuildTranspile ? coreCIEsbuild : coreCIOld)); + const coreCIPR = task.define('core-ci-pr', task.series( gulp.task('compile-build-without-mangling') as task.Task, task.parallel( @@ -676,7 +679,7 @@ const innoSetupConfig: Record Date: Mon, 9 Feb 2026 15:56:52 +0100 Subject: [PATCH 32/53] Add Type-check task using tsgo to the build process --- build/gulpfile.vscode.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index f13c68ed3ba16..16a9ed4417370 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -208,6 +208,25 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: }); } +function runTsGoTypeCheck(): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn('tsgo', ['--project', 'src/tsconfig.json', '--noEmit', '--skipLibCheck'], { + cwd: root, + stdio: 'inherit', + shell: true + }); + + proc.on('error', reject); + proc.on('close', code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`tsgo typecheck failed with exit code ${code}`)); + } + }); + }); +} + const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const minifyVSCodeTask = task.define('minify-vscode', task.series( bundleVSCodeTask, @@ -231,6 +250,8 @@ const coreCIEsbuild = task.define('core-ci-esbuild', task.series( cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, + // Type-check with tsgo (no emit) + task.define('tsgo-typecheck', () => runTsGoTypeCheck()), // Transpile individual files to out-build first (for unit tests) task.define('esbuild-out-build', () => runEsbuildTranspile('out-build', false)), // Then bundle for shipping (bundles also write NLS files to out-build) From 6d6e9a4f3eb81bb181eb8d0078d3ed87f482ca04 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 9 Feb 2026 18:00:57 +0100 Subject: [PATCH 33/53] Refactor build process: remove unused entry points and update resource patterns for desktop build --- build/next/compare-builds.cjs | 207 ++++++++++++++++++++++++++++++++++ build/next/index.ts | 34 +++--- build/next/working.md | 60 ++++++++++ 3 files changed, 283 insertions(+), 18 deletions(-) create mode 100644 build/next/compare-builds.cjs diff --git a/build/next/compare-builds.cjs b/build/next/compare-builds.cjs new file mode 100644 index 0000000000000..520bcb40bcc2e --- /dev/null +++ b/build/next/compare-builds.cjs @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const fs = require('fs'); +const path = require('path'); + +function walk(dir, base) { + let results = []; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + const rel = path.join(base, e.name); + if (e.isDirectory()) { + results = results.concat(walk(path.join(dir, e.name), rel)); + } else { + results.push(rel); + } + } + } catch (err) { /* skip */ } + return results; +} + +const oldRoot = '/Users/jrieken/Code/vscode/Visual Studio Code - Insiders-OLD.app/Contents/Resources/app'; +const newRoot = '/Users/jrieken/Code/vscode/Visual Studio Code - Insiders-NEW.app/Contents/Resources/app'; + +const oldFiles = new Set(walk(oldRoot, '')); +const newFiles = new Set(walk(newRoot, '')); + +const onlyOld = [...oldFiles].filter(f => !newFiles.has(f)).sort(); +const onlyNew = [...newFiles].filter(f => !oldFiles.has(f)).sort(); + +// Group by top-level directory +function groupByTopDir(files) { + const groups = {}; + for (const f of files) { + const parts = f.split(path.sep); + const topDir = parts.length > 1 ? parts[0] : '(root)'; + if (!groups[topDir]) { groups[topDir] = []; } + groups[topDir].push(f); + } + return groups; +} + +console.log('OLD total files:', oldFiles.size); +console.log('NEW total files:', newFiles.size); +console.log(''); + +console.log('============================================'); +console.log('FILES ONLY IN OLD (missing from NEW):', onlyOld.length); +console.log('============================================'); +const oldGroups = groupByTopDir(onlyOld); +for (const [dir, files] of Object.entries(oldGroups).sort()) { + console.log(`\n [${dir}] (${files.length} files)`); + for (const f of files.slice(0, 50)) { + console.log(` - ${f}`); + } + if (files.length > 50) { console.log(` ... and ${files.length - 50} more`); } +} + +console.log(''); +console.log('============================================'); +console.log('FILES ONLY IN NEW (extra in NEW):', onlyNew.length); +console.log('============================================'); +const newGroups = groupByTopDir(onlyNew); +for (const [dir, files] of Object.entries(newGroups).sort()) { + console.log(`\n [${dir}] (${files.length} files)`); + for (const f of files.slice(0, 50)) { + console.log(` - ${f}`); + } + if (files.length > 50) { console.log(` ... and ${files.length - 50} more`); } +} + +// Check JS files with significant size differences +console.log(''); +console.log('============================================'); +console.log('.js files with >2x size difference'); +console.log('============================================'); +const common = [...oldFiles].filter(f => newFiles.has(f) && f.endsWith('.js')); +const sizeDiffs = []; +for (const f of common) { + try { + const oldSize = fs.statSync(path.join(oldRoot, f)).size; + const newSize = fs.statSync(path.join(newRoot, f)).size; + if (oldSize === 0 || newSize === 0) { continue; } + const ratio = newSize / oldSize; + if (ratio < 0.5 || ratio > 2.0) { + sizeDiffs.push({ file: f, oldSize, newSize, ratio }); + } + } catch (e) { /* skip */ } +} +sizeDiffs.sort((a, b) => a.ratio - b.ratio); +if (sizeDiffs.length === 0) { + console.log(' None found.'); +} else { + for (const d of sizeDiffs) { + const arrow = d.ratio < 1 ? 'SMALLER' : 'LARGER'; + console.log(` ${d.file}: OLD=${d.oldSize} NEW=${d.newSize} (${d.ratio.toFixed(2)}x, ${arrow})`); + } +} + +// Check for empty JS files in NEW that aren't empty in OLD +console.log(''); +console.log('============================================'); +console.log('.js files that became empty in NEW'); +console.log('============================================'); +let emptyCount = 0; +for (const f of common) { + if (!f.endsWith('.js')) { continue; } + try { + const oldSize = fs.statSync(path.join(oldRoot, f)).size; + const newSize = fs.statSync(path.join(newRoot, f)).size; + if (oldSize > 0 && newSize === 0) { + console.log(` ${f}: was ${oldSize} bytes, now 0`); + emptyCount++; + } + } catch (e) { /* skip */ } +} +if (emptyCount === 0) { console.log(' None found.'); } + +// Size comparison by top-level directory within out/ +console.log(''); +console.log('============================================'); +console.log('Size comparison by area (out/vs/*)'); +console.log('============================================'); + +function dirSize(dir) { + let total = 0; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) { + total += dirSize(full); + } else { + total += fs.statSync(full).size; + } + } + } catch (e) { /* skip */ } + return total; +} + +const oldOut = path.join(oldRoot, 'out'); +const newOut = path.join(newRoot, 'out'); + +// Top-level out/ size +const oldTotal = dirSize(oldOut); +const newTotal = dirSize(newOut); +console.log(` TOTAL out/: OLD=${(oldTotal / 1024 / 1024).toFixed(1)}MB NEW=${(newTotal / 1024 / 1024).toFixed(1)}MB diff=+${((newTotal - oldTotal) / 1024 / 1024).toFixed(1)}MB`); + +const areas = ['vs/base', 'vs/code', 'vs/editor', 'vs/platform', 'vs/workbench']; +for (const area of areas) { + const oldSize = dirSize(path.join(oldOut, area)); + const newSize = dirSize(path.join(newOut, area)); + const diff = newSize - oldSize; + const sign = diff >= 0 ? '+' : ''; + console.log(` ${area}: OLD=${(oldSize / 1024 / 1024).toFixed(1)}MB NEW=${(newSize / 1024 / 1024).toFixed(1)}MB diff=${sign}${(diff / 1024 / 1024).toFixed(1)}MB`); +} + +// Detailed breakdown of extra files in NEW +console.log(''); +console.log('============================================'); +console.log('EXTRA FILES IN NEW - DETAILED BREAKDOWN'); +console.log('============================================'); +const extra = [...newFiles].filter(f => !oldFiles.has(f)).sort(); + +const byExt = {}; +for (const f of extra) { + const ext = path.extname(f) || '(no ext)'; + if (!byExt[ext]) { byExt[ext] = []; } + byExt[ext].push(f); +} +console.log('\nBy extension:'); +for (const [ext, files] of Object.entries(byExt).sort((a, b) => b[1].length - a[1].length)) { + console.log(` ${ext}: ${files.length} files`); +} + +// List all JS files +console.log('\n--- Extra .js files ---'); +(byExt['.js'] || []).forEach(f => console.log(` ${f}`)); + +// List all HTML files +console.log('\n--- Extra .html files ---'); +(byExt['.html'] || []).forEach(f => console.log(` ${f}`)); + +// List other non-CSS files +console.log('\n--- Extra non-CSS/non-JS/non-HTML files ---'); +for (const f of extra) { + const ext = path.extname(f); + if (ext !== '.css' && ext !== '.js' && ext !== '.html') { + console.log(` ${f}`); + } +} + +// List CSS files grouped by area +console.log('\n--- Extra .css files by area ---'); +const cssFiles = byExt['.css'] || []; +const cssAreas = {}; +for (const f of cssFiles) { + const parts = f.split(path.sep); + const area = parts.length > 3 ? parts.slice(0, 3).join('/') : parts.slice(0, 2).join('/'); + if (!cssAreas[area]) { cssAreas[area] = []; } + cssAreas[area].push(f); +} +for (const [area, files] of Object.entries(cssAreas).sort()) { + console.log(` [${area}] (${files.length} files)`); +} diff --git a/build/next/index.ts b/build/next/index.ts index e7978d726cc6c..29528a79cd036 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -134,7 +134,6 @@ function getEntryPointsForTarget(target: BuildTarget): string[] { ...desktopWorkerEntryPoints, ...desktopEntryPoints, ...codeEntryPoints, - ...webEntryPoints, // Desktop also includes web for remote development ...keyboardMapEntryPoints, ]; case 'server': @@ -184,9 +183,7 @@ function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { case 'desktop': return new Set([ 'vs/workbench/workbench.desktop.main', - 'vs/workbench/workbench.web.main.internal', 'vs/code/electron-browser/workbench/workbench', - 'vs/code/browser/workbench/workbench', ]); case 'server': return new Set(); // Server has no UI @@ -230,9 +227,6 @@ const desktopResourcePatterns = [ // HTML 'vs/code/electron-browser/workbench/workbench.html', 'vs/code/electron-browser/workbench/workbench-dev.html', - 'vs/code/browser/workbench/workbench.html', - 'vs/code/browser/workbench/workbench-dev.html', - 'vs/code/browser/workbench/callback.html', 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', 'vs/workbench/contrib/webview/browser/pre/*.html', @@ -241,11 +235,11 @@ const desktopResourcePatterns = [ // Shell scripts 'vs/base/node/*.sh', - 'vs/workbench/contrib/terminal/common/scripts/**/*.sh', - 'vs/workbench/contrib/terminal/common/scripts/**/*.ps1', - 'vs/workbench/contrib/terminal/common/scripts/**/*.psm1', - 'vs/workbench/contrib/terminal/common/scripts/**/*.fish', - 'vs/workbench/contrib/terminal/common/scripts/**/*.zsh', + 'vs/workbench/contrib/terminal/common/scripts/*.sh', + 'vs/workbench/contrib/terminal/common/scripts/*.ps1', + 'vs/workbench/contrib/terminal/common/scripts/*.psm1', + 'vs/workbench/contrib/terminal/common/scripts/*.fish', + 'vs/workbench/contrib/terminal/common/scripts/*.zsh', 'vs/workbench/contrib/externalTerminal/**/*.scpt', // Media - audio @@ -254,8 +248,7 @@ const desktopResourcePatterns = [ // Media - images 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.svg', 'vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.png', - 'vs/workbench/contrib/extensions/browser/media/*.svg', - 'vs/workbench/contrib/extensions/browser/media/*.png', + 'vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', 'vs/workbench/services/extensionManagement/common/media/*.svg', 'vs/workbench/services/extensionManagement/common/media/*.png', 'vs/workbench/browser/parts/editor/media/*.png', @@ -460,6 +453,7 @@ async function copyFile(srcPath: string, destPath: string): Promise { const desktopStandaloneFiles = [ 'vs/base/parts/sandbox/electron-browser/preload.ts', 'vs/base/parts/sandbox/electron-browser/preload-aux.ts', + 'vs/platform/browserView/electron-browser/preload-browserView.ts', ]; async function compileStandaloneFiles(outDir: string, doMinify: boolean, target: BuildTarget): Promise { @@ -558,11 +552,15 @@ async function copyResources(outDir: string, target: BuildTarget, excludeDevFile } } - // Copy CSS files - const cssCount = await copyCssFiles(outDir, excludeTests); - copied += cssCount; - - console.log(`[resources] Copied ${copied} files (${cssCount} CSS)`); + // Copy CSS files (only for development/transpile builds, not production bundles + // where CSS is already bundled into combined files like workbench.desktop.main.css) + if (!excludeDevFiles) { + const cssCount = await copyCssFiles(outDir, excludeTests); + copied += cssCount; + console.log(`[resources] Copied ${copied} files (${cssCount} CSS)`); + } else { + console.log(`[resources] Copied ${copied} files (CSS skipped - bundled)`); + } } // ============================================================================ diff --git a/build/next/working.md b/build/next/working.md index f9ebdd40d040b..56a6aafbad0e8 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -108,6 +108,66 @@ npm run gulp vscode-reh-web-darwin-arm64-min --- +## Build Comparison: OLD (gulp-tsb) vs NEW (esbuild) — Desktop Build + +### Summary + +| Metric | OLD | NEW | Delta | +|--------|-----|-----|-------| +| Total files in `out/` | 3993 | 4301 | +309 extra, 1 missing | +| Total size of `out/` | 25.8 MB | 64.6 MB | +38.8 MB (2.5×) | +| `workbench.desktop.main.js` | 13.0 MB | 15.5 MB | +2.5 MB | + +### 1 Missing File (in OLD, not in NEW) + +| File | Why Missing | Fix | +|------|-------------|-----| +| `out/vs/platform/browserView/electron-browser/preload-browserView.js` | Not listed in `desktopStandaloneFiles` in index.ts. Only `preload.ts` and `preload-aux.ts` are compiled as standalone files. | **Add** `'vs/platform/browserView/electron-browser/preload-browserView.ts'` to the `desktopStandaloneFiles` array in `index.ts`. | + +### 309 Extra Files (in NEW, not in OLD) — Breakdown + +| Category | Count | Explanation | +|----------|-------|-------------| +| **CSS files** | 291 | `copyCssFiles()` copies ALL `.css` from `src/` to the output. The old bundler inlines CSS into the main `.css` bundle (e.g., `workbench.desktop.main.css`) and never ships individual CSS files. These individual files ARE needed at runtime because the new ESM system uses `import './foo.css'` resolved by an import map. | +| **Vendor JS files** | 3 | `dompurify.js`, `marked.js`, `semver.js` — listed in `commonResourcePatterns`. The old bundler inlines these into the main bundle. The new system keeps them as separate files because they're plain JS (not TS). They're needed. | +| **Web workbench bundle** | 1 | `vs/code/browser/workbench/workbench.js` (15.4 MB). This is the web workbench entry point bundle. It should NOT be in a desktop build — the old build explicitly excludes `out-build/vs/code/browser/**`. The `desktopResourcePatterns` in index.ts includes `vs/code/browser/workbench/*.html` and `callback.html` which is correct, but the actual bundle gets written by the esbuild desktop bundle step because the desktop entry points include web entry points. | +| **Web workbench internal** | 1 | `vs/workbench/workbench.web.main.internal.js` (15.4 MB). Similar: shouldn't ship in a desktop build. It's output by the esbuild bundler. | +| **Keyboard layout contributions** | 3 | `layout.contribution.{darwin,linux,win}.js` — the old bundler inlines these into the main bundle. These are new separate files from the esbuild bundler. | +| **NLS files** | 2 | `nls.messages.js` (new) and `nls.metadata.json` (new). The old build has `nls.messages.json` and `nls.keys.json` but not a `.js` version or metadata. The `.js` version is produced by the NLS plugin. | +| **HTML files** | 2 | `vs/code/browser/workbench/workbench.html` and `callback.html` — correctly listed in `desktopResourcePatterns` (these are needed for desktop's built-in web server). | +| **SVG loading spinners** | 3 | `loading-dark.svg`, `loading-hc.svg`, `loading.svg` in `vs/workbench/contrib/extensions/browser/media/`. The old build only copies `theme-icon.png` and `language-icon.svg` from that folder; the new build's `desktopResourcePatterns` uses `*.svg` which is broader. | +| **codicon.ttf (duplicate)** | 1 | At `vs/base/browser/ui/codicons/codicon/codicon.ttf`. The old build copies this to `out/media/codicon.ttf` only. The new build has BOTH: the copy in `out/media/` (from esbuild's `file` loader) AND the original path (from `commonResourcePatterns`). Duplicate. | +| **PSReadLine.psm1** | 1 | `vs/workbench/contrib/terminal/common/scripts/psreadline/PSReadLine.psm1` — the old build uses `*.psm1` in `terminal/common/scripts/` (non-recursive?). The new build uses `**/*.psm1` (recursive), picking up this subdirectory file. Check if it's needed. | +| **date file** | 1 | `out/date` — build timestamp, produced by the new build's `bundle()` function. The old build doesn't write this; it reads `package.json.date` instead. | + +### Size Increase Breakdown by Area + +| Area | OLD | NEW | Delta | Why | +|------|-----|-----|-------|-----| +| `vs/code` | 1.5 MB | 17.4 MB | +15.9 MB | Web workbench bundle (15.4 MB) shouldn't be in desktop build | +| `vs/workbench` | 18.9 MB | 38.7 MB | +19.8 MB | `workbench.web.main.internal.js` (15.4 MB) + unmangled desktop bundle (+2.5 MB) + individual CSS files (~1 MB) | +| `vs/base` | 0 MB | 0.4 MB | +0.4 MB | Individual CSS files + vendor JS | +| `vs/editor` | 0.3 MB | 0.5 MB | +0.1 MB | Individual CSS files | +| `vs/platform` | 1.7 MB | 1.9 MB | +0.2 MB | Individual CSS files | + +### JS Files with >2× Size Change + +| File | OLD | NEW | Ratio | Reason | +|------|-----|-----|-------|--------| +| `vs/workbench/contrib/webview/browser/pre/service-worker.js` | 7 KB | 15 KB | 2.2× | Not minified / includes more inlined code | +| `vs/code/electron-browser/workbench/workbench.js` | 10 KB | 28 KB | 2.75× | OLD is minified to 6 lines; NEW is 380 lines (not compressed, includes tslib banner) | + +### Action Items + +1. **[CRITICAL] Missing `preload-browserView.ts`** — Add to `desktopStandaloneFiles` in index.ts. Without it, BrowserView (used for Simple Browser) may fail. +2. **[SIZE] Web bundles in desktop build** — `workbench.web.main.internal.js` and `vs/code/browser/workbench/workbench.js` together add ~31 MB. These are written by the esbuild bundler and not filtered out. Consider: either don't bundle web entry points for the desktop target, or ensure the packaging step excludes them (currently `packageTask` takes `out-vscode-min/**` without filtering). +3. **[SIZE] No mangling** — The desktop main bundle is 2.5 MB larger due to no property mangling. Known open item. +4. **[MINOR] Duplicate codicon.ttf** — Exists at both `out/media/codicon.ttf` (from esbuild `file` loader) and `out/vs/base/browser/ui/codicons/codicon/codicon.ttf` (from `commonResourcePatterns`). Consider removing from `commonResourcePatterns` if it's already handled by the loader. +5. **[MINOR] Extra SVGs** — `desktopResourcePatterns` uses `*.svg` for extensions media but old build only ships `language-icon.svg`. The loading spinners may be unused in the desktop build. +6. **[MINOR] Extra PSReadLine.psm1** from recursive glob — verify if needed. + +--- + ## Self-hosting Setup The default `VS Code - Build` task now runs three parallel watchers: From 9a9079bf2654ca2ecb385f4a1c9735a333d168be Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 9 Feb 2026 21:11:03 +0100 Subject: [PATCH 34/53] esbuild: --- build/next/index.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index 29528a79cd036..62a3c2c177bb3 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -207,17 +207,21 @@ function getCssBundleEntryPointsForTarget(target: BuildTarget): Set { // Common resources needed by all targets const commonResourcePatterns = [ - // Fonts + // Tree-sitter queries + 'vs/editor/common/languages/highlights/*.scm', + 'vs/editor/common/languages/injections/*.scm', +]; + +// Resources only needed for dev/transpile builds (these get bundled into the main +// JS/CSS bundles for production, so separate copies are redundant) +const devOnlyResourcePatterns = [ + // Fonts (esbuild file loader copies to media/codicon.ttf for production) 'vs/base/browser/ui/codicons/codicon/codicon.ttf', - // Vendor JavaScript libraries (not transpiled) + // Vendor JavaScript libraries (bundled into workbench main JS for production) 'vs/base/common/marked/marked.js', 'vs/base/common/semver/semver.js', 'vs/base/browser/dompurify/dompurify.js', - - // Tree-sitter queries - 'vs/editor/common/languages/highlights/*.scm', - 'vs/editor/common/languages/injections/*.scm', ]; // Resources for desktop target @@ -552,6 +556,21 @@ async function copyResources(outDir: string, target: BuildTarget, excludeDevFile } } + // Copy dev-only resources (vendor JS, codicon font) - only for development/transpile + // builds. In production bundles these are inlined by esbuild. + if (!excludeDevFiles) { + for (const pattern of devOnlyResourcePatterns) { + const files = await globAsync(pattern, { + cwd: path.join(REPO_ROOT, SRC_DIR), + ignore: ignorePatterns, + }); + for (const file of files) { + await copyFile(path.join(REPO_ROOT, SRC_DIR, file), path.join(REPO_ROOT, outDir, file)); + copied++; + } + } + } + // Copy CSS files (only for development/transpile builds, not production bundles // where CSS is already bundled into combined files like workbench.desktop.main.css) if (!excludeDevFiles) { From 18456cc05f7c4dca4fd5e9b2316180bcb681870b Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 11:10:12 +0100 Subject: [PATCH 35/53] source maps: sourcesContent, CDN URL rewriting, replace @parcel/watcher --- build/gulpfile.vscode.ts | 13 ++++--- build/next/index.ts | 79 ++++++++++++++++++++++------------------ build/next/working.md | 30 +++++++++++++++ 3 files changed, 82 insertions(+), 40 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 16a9ed4417370..c3489e0931efe 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -180,7 +180,7 @@ function runEsbuildTranspile(outDir: string, excludeTests: boolean): Promise { +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: 'desktop' | 'server' | 'server-web' = 'desktop', sourceMapBaseUrl?: string): Promise { return new Promise((resolve, reject) => { // const tsxPath = path.join(root, 'build/node_modules/tsx/dist/cli.mjs'); const scriptPath = path.join(root, 'build/next/index.ts'); @@ -191,6 +191,9 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, target: if (nls) { args.push('--nls'); } + if (sourceMapBaseUrl) { + args.push('--source-map-base-url', sourceMapBaseUrl); + } const proc = cp.spawn(process.execPath, args, { cwd: root, @@ -256,9 +259,9 @@ const coreCIEsbuild = task.define('core-ci-esbuild', task.series( task.define('esbuild-out-build', () => runEsbuildTranspile('out-build', false)), // Then bundle for shipping (bundles also write NLS files to out-build) task.parallel( - task.define('esbuild-vscode-min', () => runEsbuildBundle('out-vscode-min', true, true, 'desktop')), - task.define('esbuild-vscode-reh-min', () => runEsbuildBundle('out-vscode-reh-min', true, true, 'server')), - task.define('esbuild-vscode-reh-web-min', () => runEsbuildBundle('out-vscode-reh-web-min', true, true, 'server-web')), + task.define('esbuild-vscode-min', () => runEsbuildBundle('out-vscode-min', true, true, 'desktop', `${sourceMappingURLBase}/core`)), + task.define('esbuild-vscode-reh-min', () => runEsbuildBundle('out-vscode-reh-min', true, true, 'server', `${sourceMappingURLBase}/core`)), + task.define('esbuild-vscode-reh-web-min', () => runEsbuildBundle('out-vscode-reh-web-min', true, true, 'server-web', `${sourceMappingURLBase}/core`)), ) )); gulp.task(coreCIEsbuild); @@ -646,7 +649,7 @@ BUILD_TARGETS.forEach(buildTarget => { const esbuildBundleTask = task.define( `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, - () => runEsbuildBundle(sourceFolderName, !!minified, true) + () => runEsbuildBundle(sourceFolderName, !!minified, true, 'desktop', minified ? `${sourceMappingURLBase}/core` : undefined) ); const tasks = [ diff --git a/build/next/index.ts b/build/next/index.ts index 62a3c2c177bb3..9ddf11f4be3f3 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -8,7 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import glob from 'glob'; -import * as watcher from '@parcel/watcher'; +import gulpWatch from '../lib/watch/index.ts'; import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts'; import { getVersion } from '../lib/getVersion.ts'; import product from '../../product.json' with { type: 'json' }; @@ -44,6 +44,7 @@ const options = { excludeTests: process.argv.includes('--exclude-tests'), out: getArgValue('--out'), target: getArgValue('--target') ?? 'desktop', // 'desktop' | 'server' | 'server-web' | 'web' + sourceMapBaseUrl: getArgValue('--source-map-base-url'), }; // Build targets @@ -736,7 +737,7 @@ async function transpile(outDir: string, excludeTests: boolean): Promise { // Bundle (Goal 2: JS → bundled JS) // ============================================================================ -async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: BuildTarget): Promise { +async function bundle(outDir: string, doMinify: boolean, doNls: boolean, target: BuildTarget, sourceMapBaseUrl?: string): Promise { await cleanDir(outDir); // Write build date file (used by packaging to embed in product.json) @@ -816,7 +817,7 @@ ${tslib}`, target: ['es2024'], packages: 'external', sourcemap: 'external', - sourcesContent: false, + sourcesContent: true, minify: doMinify, treeShaking: true, banner, @@ -868,7 +869,7 @@ ${tslib}`, target: ['es2024'], packages: 'external', sourcemap: 'external', - sourcesContent: false, + sourcesContent: true, minify: doMinify, treeShaking: true, banner, @@ -906,17 +907,30 @@ ${tslib}`, for (const file of result.outputFiles) { await fs.promises.mkdir(path.dirname(file.path), { recursive: true }); - if (file.path.endsWith('.js')) { + if (file.path.endsWith('.js') || file.path.endsWith('.css')) { let content = file.text; - // Apply NLS post-processing if enabled - if (doNls && indexMap.size > 0) { + // Apply NLS post-processing if enabled (JS only) + if (file.path.endsWith('.js') && doNls && indexMap.size > 0) { content = postProcessNLS(content, indexMap, preserveEnglish); } + // Rewrite sourceMappingURL to CDN URL if configured + if (sourceMapBaseUrl) { + const relativePath = path.relative(path.join(REPO_ROOT, outDir), file.path); + content = content.replace( + /\/\/# sourceMappingURL=.+$/m, + `//# sourceMappingURL=${sourceMapBaseUrl}/${relativePath}.map` + ); + content = content.replace( + /\/\*# sourceMappingURL=.+\*\/$/m, + `/*# sourceMappingURL=${sourceMapBaseUrl}/${relativePath}.map*/` + ); + } + await fs.promises.writeFile(file.path, content); } else { - // Write other files (source maps, etc.) as-is + // Write other files (source maps, assets) as-is await fs.promises.writeFile(file.path, file.contents); } } @@ -1011,40 +1025,34 @@ async function watch(): Promise { // Extensions to watch and copy (non-TypeScript resources) const copyExtensions = ['.css', '.html', '.js', '.json', '.ttf', '.svg', '.png', '.mp3', '.scm', '.sh', '.ps1', '.psm1', '.fish', '.zsh', '.scpt']; - // Watch src directory - const subscription = await watcher.subscribe( - path.join(REPO_ROOT, SRC_DIR), - (err, events) => { - if (err) { - console.error('[watch] Watcher error:', err); - return; - } + // Watch src directory using existing gulp-watch based watcher + let debounceTimer: ReturnType | undefined; + const srcDir = path.join(REPO_ROOT, SRC_DIR); + const watchStream = gulpWatch('src/**', { base: srcDir, readDelay: 200 }); - for (const event of events) { - if (event.path.includes('/test/')) { - continue; - } + watchStream.on('data', (file: { path: string }) => { + if (file.path.includes('/test/')) { + return; + } - if (event.path.endsWith('.ts') && !event.path.endsWith('.d.ts')) { - pendingTsFiles.add(event.path); - } else if (copyExtensions.some(ext => event.path.endsWith(ext))) { - pendingCopyFiles.add(event.path); - } - } + if (file.path.endsWith('.ts') && !file.path.endsWith('.d.ts')) { + pendingTsFiles.add(file.path); + } else if (copyExtensions.some(ext => file.path.endsWith(ext))) { + pendingCopyFiles.add(file.path); + } - if (pendingTsFiles.size > 0 || pendingCopyFiles.size > 0) { - processChanges(); - } - }, - { ignore: ['**/test/**', '**/node_modules/**'] } - ); + if (pendingTsFiles.size > 0 || pendingCopyFiles.size > 0) { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(processChanges, 200); + } + }); console.log('[watch] Watching src/**/*.{ts,css,...} (Ctrl+C to stop)'); // Keep process alive - process.on('SIGINT', async () => { + process.on('SIGINT', () => { console.log('\n[watch] Stopping...'); - await subscription.unsubscribe(); + watchStream.end(); process.exit(0); }); } @@ -1070,6 +1078,7 @@ Options for 'bundle': --nls Process NLS (localization) strings --out Output directory (default: out-vscode) --target Build target: desktop (default), server, server-web, web + --source-map-base-url Rewrite sourceMappingURL to CDN URL Examples: npx tsx build/next/index.ts transpile @@ -1110,7 +1119,7 @@ async function main(): Promise { break; case 'bundle': - await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.target as BuildTarget); + await bundle(options.out ?? OUT_VSCODE_DIR, options.minify, options.nls, options.target as BuildTarget, options.sourceMapBaseUrl); break; default: diff --git a/build/next/working.md b/build/next/working.md index 56a6aafbad0e8..ac466fce548c9 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -168,6 +168,36 @@ npm run gulp vscode-reh-web-darwin-arm64-min --- +## Source Maps + +### Fixes Applied + +1. **`sourcesContent: true`** — Production bundles now embed original TypeScript source content in `.map` files, matching the old build's `includeContent: true` behavior. Without this, crash reports from CDN-hosted source maps can't show original source. + +2. **`--source-map-base-url` option** — The `bundle` command accepts an optional `--source-map-base-url ` flag. When set, post-processing rewrites `sourceMappingURL` comments in `.js` and `.css` output files to point to the CDN (e.g., `https://main.vscode-cdn.net/sourcemaps//core/vs/...`). This matches the old build's `sourceMappingURL` function in `minifyTask()`. Wired up in `gulpfile.vscode.ts` for `core-ci-esbuild` and `vscode-esbuild-min` tasks. + +### NLS Source Map Accuracy (Decision: Accept Imprecision) + +**Problem:** `postProcessNLS()` replaces `"%%NLS:moduleId#key%%"` placeholders (~40 chars) with short index values like `null` (4 chars) in the final JS output. This shifts column positions without updating the `.map` files. + +**Options considered:** + +| Option | Description | Effort | Accuracy | +|--------|-------------|--------|----------| +| A. Fixed-width placeholders | Pad placeholders to match replacement length | Hard — indices unknown until all modules are collected across parallel bundles | Perfect | +| B. Post-process source map | Parse `.map`, track replacement offsets per line, adjust VLQ mappings | Medium | Perfect | +| C. Two-pass build | Assign NLS indices during plugin phase | Not feasible with parallel bundling | N/A | +| **D. Accept imprecision** | NLS replacements only affect column positions; line-level debugging works | Zero | Line-level | + +**Decision: Option D — accept imprecision.** Rationale: + +- NLS replacements only shift **columns**, never lines — line-level stack traces and breakpoints remain correct. +- Production crash reporting (the primary consumer of CDN source maps) uses line numbers; column-level accuracy is rarely needed. +- The old gulp build had the same fundamental issue in its `nls.nls()` step and used `SourceMapConsumer`/`SourceMapGenerator` to fix it — but that approach was fragile and slow. +- If column-level precision becomes important later (e.g., for minified+NLS bundles), Option B can be implemented: after NLS replacement, re-parse the source map, walk replacement sites, and adjust column offsets. This is a localized change in the post-processing loop. + +--- + ## Self-hosting Setup The default `VS Code - Build` task now runs three parallel watchers: From 5ea70f9e5b1b16b9f07f8bf8d1bee27e7db5cbba Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 11:36:17 +0100 Subject: [PATCH 36/53] refactor: streamline vscode build tasks and remove esbuild variants --- build/gulpfile.vscode.ts | 75 +++++++++++++++------------------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index c3489e0931efe..7f5138166feb8 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -610,76 +610,55 @@ BUILD_TARGETS.forEach(buildTarget => { const arch = buildTarget.arch; const opts = buildTarget.opts; - // Traditional gulp-based builds: vscode-{platform}-{arch} and vscode-{platform}-{arch}-min const [vscode, vscodeMin] = ['', 'min'].map(minified => { const sourceFolderName = `out-vscode${dashed(minified)}`; const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; - const tasks = [ + const packageTasks: task.Task[] = [ compileNativeExtensionsBuildTask, util.rimraf(path.join(buildRoot, destinationFolderName)), packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) ]; if (platform === 'win32') { - tasks.push(patchWin32DependenciesTask(destinationFolderName)); + packageTasks.push(patchWin32DependenciesTask(destinationFolderName)); } - const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...tasks)); + const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...packageTasks)); gulp.task(vscodeTaskCI); - const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( - minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask, - cleanExtensionsBuildTask, - compileNonNativeExtensionsBuildTask, - compileExtensionMediaBuildTask, - minified ? minifyVSCodeTask : bundleVSCodeTask, - vscodeTaskCI - )); + let vscodeTask: task.Task; + if (useEsbuildTranspile) { + const esbuildBundleTask = task.define( + `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, + () => runEsbuildBundle(sourceFolderName, !!minified, true, 'desktop', minified ? `${sourceMappingURLBase}/core` : undefined) + ); + vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + copyCodiconsTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + esbuildBundleTask, + vscodeTaskCI + )); + } else { + vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + minified ? minifyVSCodeTask : bundleVSCodeTask, + vscodeTaskCI + )); + } gulp.task(vscodeTask); return vscodeTask; }); - // esbuild-based builds: vscode-{platform}-{arch}-esbuild and vscode-{platform}-{arch}-esbuild-min - // These skip TypeScript compilation and bundle directly from source - const [vscodeEsbuild, vscodeEsbuildMin] = ['', 'min'].map(minified => { - const sourceFolderName = `out-vscode${dashed(minified)}`; - const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; - - const esbuildBundleTask = task.define( - `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, - () => runEsbuildBundle(sourceFolderName, !!minified, true, 'desktop', minified ? `${sourceMappingURLBase}/core` : undefined) - ); - - const tasks = [ - compileNativeExtensionsBuildTask, - util.rimraf(path.join(buildRoot, destinationFolderName)), - packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) - ]; - - if (platform === 'win32') { - tasks.push(patchWin32DependenciesTask(destinationFolderName)); - } - - const vscodeEsbuildTask = task.define(`vscode${dashed(platform)}${dashed(arch)}-esbuild${dashed(minified)}`, task.series( - copyCodiconsTask, - cleanExtensionsBuildTask, - compileNonNativeExtensionsBuildTask, - compileExtensionMediaBuildTask, - esbuildBundleTask, - ...tasks - )); - gulp.task(vscodeEsbuildTask); - - return vscodeEsbuildTask; - }); - if (process.platform === platform && process.arch === arch) { gulp.task(task.define('vscode', task.series(vscode))); gulp.task(task.define('vscode-min', task.series(vscodeMin))); - gulp.task(task.define('vscode-esbuild', task.series(vscodeEsbuild))); - gulp.task(task.define('vscode-esbuild-min', task.series(vscodeEsbuildMin))); } }); From 1974429562c0f230b794816dce80907a3428da08 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 11:37:28 +0100 Subject: [PATCH 37/53] fix: restore check for out-build --- build/azure-pipelines/product-compile.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 13a0df9326272..bc13d980df2dd 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -103,9 +103,9 @@ jobs: - script: | set -e - # [ -d "out-build" ] || { echo "ERROR: out-build folder is missing" >&2; exit 1; } - # [ -n "$(find out-build -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: out-build folder is empty" >&2; exit 1; } - # echo "out-build exists and is not empty" + [ -d "out-build" ] || { echo "ERROR: out-build folder is missing" >&2; exit 1; } + [ -n "$(find out-build -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: out-build folder is empty" >&2; exit 1; } + echo "out-build exists and is not empty" ls -d out-vscode-* >/dev/null 2>&1 || { echo "ERROR: No out-vscode-* folders found" >&2; exit 1; } for folder in out-vscode-*; do From edc58f9da13930a2596e269d4c6ae935e224c275 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 12:45:14 +0100 Subject: [PATCH 38/53] delete: remove obsolete compare-builds script --- build/next/compare-builds.cjs | 207 ---------------------------------- 1 file changed, 207 deletions(-) delete mode 100644 build/next/compare-builds.cjs diff --git a/build/next/compare-builds.cjs b/build/next/compare-builds.cjs deleted file mode 100644 index 520bcb40bcc2e..0000000000000 --- a/build/next/compare-builds.cjs +++ /dev/null @@ -1,207 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const fs = require('fs'); -const path = require('path'); - -function walk(dir, base) { - let results = []; - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const e of entries) { - const rel = path.join(base, e.name); - if (e.isDirectory()) { - results = results.concat(walk(path.join(dir, e.name), rel)); - } else { - results.push(rel); - } - } - } catch (err) { /* skip */ } - return results; -} - -const oldRoot = '/Users/jrieken/Code/vscode/Visual Studio Code - Insiders-OLD.app/Contents/Resources/app'; -const newRoot = '/Users/jrieken/Code/vscode/Visual Studio Code - Insiders-NEW.app/Contents/Resources/app'; - -const oldFiles = new Set(walk(oldRoot, '')); -const newFiles = new Set(walk(newRoot, '')); - -const onlyOld = [...oldFiles].filter(f => !newFiles.has(f)).sort(); -const onlyNew = [...newFiles].filter(f => !oldFiles.has(f)).sort(); - -// Group by top-level directory -function groupByTopDir(files) { - const groups = {}; - for (const f of files) { - const parts = f.split(path.sep); - const topDir = parts.length > 1 ? parts[0] : '(root)'; - if (!groups[topDir]) { groups[topDir] = []; } - groups[topDir].push(f); - } - return groups; -} - -console.log('OLD total files:', oldFiles.size); -console.log('NEW total files:', newFiles.size); -console.log(''); - -console.log('============================================'); -console.log('FILES ONLY IN OLD (missing from NEW):', onlyOld.length); -console.log('============================================'); -const oldGroups = groupByTopDir(onlyOld); -for (const [dir, files] of Object.entries(oldGroups).sort()) { - console.log(`\n [${dir}] (${files.length} files)`); - for (const f of files.slice(0, 50)) { - console.log(` - ${f}`); - } - if (files.length > 50) { console.log(` ... and ${files.length - 50} more`); } -} - -console.log(''); -console.log('============================================'); -console.log('FILES ONLY IN NEW (extra in NEW):', onlyNew.length); -console.log('============================================'); -const newGroups = groupByTopDir(onlyNew); -for (const [dir, files] of Object.entries(newGroups).sort()) { - console.log(`\n [${dir}] (${files.length} files)`); - for (const f of files.slice(0, 50)) { - console.log(` - ${f}`); - } - if (files.length > 50) { console.log(` ... and ${files.length - 50} more`); } -} - -// Check JS files with significant size differences -console.log(''); -console.log('============================================'); -console.log('.js files with >2x size difference'); -console.log('============================================'); -const common = [...oldFiles].filter(f => newFiles.has(f) && f.endsWith('.js')); -const sizeDiffs = []; -for (const f of common) { - try { - const oldSize = fs.statSync(path.join(oldRoot, f)).size; - const newSize = fs.statSync(path.join(newRoot, f)).size; - if (oldSize === 0 || newSize === 0) { continue; } - const ratio = newSize / oldSize; - if (ratio < 0.5 || ratio > 2.0) { - sizeDiffs.push({ file: f, oldSize, newSize, ratio }); - } - } catch (e) { /* skip */ } -} -sizeDiffs.sort((a, b) => a.ratio - b.ratio); -if (sizeDiffs.length === 0) { - console.log(' None found.'); -} else { - for (const d of sizeDiffs) { - const arrow = d.ratio < 1 ? 'SMALLER' : 'LARGER'; - console.log(` ${d.file}: OLD=${d.oldSize} NEW=${d.newSize} (${d.ratio.toFixed(2)}x, ${arrow})`); - } -} - -// Check for empty JS files in NEW that aren't empty in OLD -console.log(''); -console.log('============================================'); -console.log('.js files that became empty in NEW'); -console.log('============================================'); -let emptyCount = 0; -for (const f of common) { - if (!f.endsWith('.js')) { continue; } - try { - const oldSize = fs.statSync(path.join(oldRoot, f)).size; - const newSize = fs.statSync(path.join(newRoot, f)).size; - if (oldSize > 0 && newSize === 0) { - console.log(` ${f}: was ${oldSize} bytes, now 0`); - emptyCount++; - } - } catch (e) { /* skip */ } -} -if (emptyCount === 0) { console.log(' None found.'); } - -// Size comparison by top-level directory within out/ -console.log(''); -console.log('============================================'); -console.log('Size comparison by area (out/vs/*)'); -console.log('============================================'); - -function dirSize(dir) { - let total = 0; - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const e of entries) { - const full = path.join(dir, e.name); - if (e.isDirectory()) { - total += dirSize(full); - } else { - total += fs.statSync(full).size; - } - } - } catch (e) { /* skip */ } - return total; -} - -const oldOut = path.join(oldRoot, 'out'); -const newOut = path.join(newRoot, 'out'); - -// Top-level out/ size -const oldTotal = dirSize(oldOut); -const newTotal = dirSize(newOut); -console.log(` TOTAL out/: OLD=${(oldTotal / 1024 / 1024).toFixed(1)}MB NEW=${(newTotal / 1024 / 1024).toFixed(1)}MB diff=+${((newTotal - oldTotal) / 1024 / 1024).toFixed(1)}MB`); - -const areas = ['vs/base', 'vs/code', 'vs/editor', 'vs/platform', 'vs/workbench']; -for (const area of areas) { - const oldSize = dirSize(path.join(oldOut, area)); - const newSize = dirSize(path.join(newOut, area)); - const diff = newSize - oldSize; - const sign = diff >= 0 ? '+' : ''; - console.log(` ${area}: OLD=${(oldSize / 1024 / 1024).toFixed(1)}MB NEW=${(newSize / 1024 / 1024).toFixed(1)}MB diff=${sign}${(diff / 1024 / 1024).toFixed(1)}MB`); -} - -// Detailed breakdown of extra files in NEW -console.log(''); -console.log('============================================'); -console.log('EXTRA FILES IN NEW - DETAILED BREAKDOWN'); -console.log('============================================'); -const extra = [...newFiles].filter(f => !oldFiles.has(f)).sort(); - -const byExt = {}; -for (const f of extra) { - const ext = path.extname(f) || '(no ext)'; - if (!byExt[ext]) { byExt[ext] = []; } - byExt[ext].push(f); -} -console.log('\nBy extension:'); -for (const [ext, files] of Object.entries(byExt).sort((a, b) => b[1].length - a[1].length)) { - console.log(` ${ext}: ${files.length} files`); -} - -// List all JS files -console.log('\n--- Extra .js files ---'); -(byExt['.js'] || []).forEach(f => console.log(` ${f}`)); - -// List all HTML files -console.log('\n--- Extra .html files ---'); -(byExt['.html'] || []).forEach(f => console.log(` ${f}`)); - -// List other non-CSS files -console.log('\n--- Extra non-CSS/non-JS/non-HTML files ---'); -for (const f of extra) { - const ext = path.extname(f); - if (ext !== '.css' && ext !== '.js' && ext !== '.html') { - console.log(` ${f}`); - } -} - -// List CSS files grouped by area -console.log('\n--- Extra .css files by area ---'); -const cssFiles = byExt['.css'] || []; -const cssAreas = {}; -for (const f of cssFiles) { - const parts = f.split(path.sep); - const area = parts.length > 3 ? parts.slice(0, 3).join('/') : parts.slice(0, 2).join('/'); - if (!cssAreas[area]) { cssAreas[area] = []; } - cssAreas[area].push(f); -} -for (const [area, files] of Object.entries(cssAreas).sort()) { - console.log(` [${area}] (${files.length} files)`); -} From dbf3cdf0ac1f3a7a21f4812c871a864c014bb815 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 16:36:35 +0100 Subject: [PATCH 39/53] fix: update date handling in jsonEditor for product.json --- build/gulpfile.reh.ts | 2 +- build/gulpfile.vscode.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index b935764033e55..27149338d9f7e 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -321,7 +321,7 @@ function packageTask(type: string, platform: string, arch: string, sourceFolderN let productJsonContents = ''; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor({ commit, date: readISODate(sourceFolderName), version })) + .pipe(jsonEditor({ commit, date: readISODate('out-build'), version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 7f5138166feb8..92ae01fd01cf0 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -370,7 +370,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d let productJsonContents: string; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(jsonEditor({ commit, date: readISODate(out), checksums, version })) + .pipe(jsonEditor({ commit, date: readISODate('out-build'), checksums, version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); From 3180e9482c6606f4248a15ef274f006d240713f4 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 17:06:49 +0100 Subject: [PATCH 40/53] fix: remove test file exclusion from watch stream --- build/next/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index 9ddf11f4be3f3..47524b3829a61 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -1031,10 +1031,6 @@ async function watch(): Promise { const watchStream = gulpWatch('src/**', { base: srcDir, readDelay: 200 }); watchStream.on('data', (file: { path: string }) => { - if (file.path.includes('/test/')) { - return; - } - if (file.path.endsWith('.ts') && !file.path.endsWith('.d.ts')) { pendingTsFiles.add(file.path); } else if (copyExtensions.some(ext => file.path.endsWith(ext))) { From e562211c1fa0f4db555ebfaa2a053c2e36530c38 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 17:30:14 +0100 Subject: [PATCH 41/53] fix: remove keyboardMapEntryPoints from desktop entry points and nls.messages.js generation --- build/next/index.ts | 1 - build/next/nls-plugin.ts | 7 ------- build/next/working.md | 18 ++++++++++++++++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/build/next/index.ts b/build/next/index.ts index 47524b3829a61..8d8429012de12 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -135,7 +135,6 @@ function getEntryPointsForTarget(target: BuildTarget): string[] { ...desktopWorkerEntryPoints, ...desktopEntryPoints, ...codeEntryPoints, - ...keyboardMapEntryPoints, ]; case 'server': return [ diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 0601f9c2fdbf2..53f5359c3b839 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -134,13 +134,6 @@ export async function finalizeNLS( path.join(dir, 'nls.metadata.json'), JSON.stringify(nlsMetadataJson, null, '\t') ), - fs.promises.writeFile( - path.join(dir, 'nls.messages.js'), - `/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` - ) ])); console.log(`[nls] Extracted ${allMessages.length} messages from ${moduleToKeys.size} modules`); diff --git a/build/next/working.md b/build/next/working.md index ac466fce548c9..bbf23a99806b1 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -79,6 +79,24 @@ Two placeholders that need injection: - Removes `webEndpointUrlTemplate` from product config (see `tweakProductForServerWeb` in old build) - Uses `.build/extensions` for builtin extensions (not `.build/web/extensions`) +### 5. Entry Point Parity with Old Build + +**Problem:** The desktop target had `keyboardMapEntryPoints` as separate esbuild entry points, producing `layout.contribution.darwin.js`, `layout.contribution.linux.js`, and `layout.contribution.win.js` as standalone files in the output. + +**Root cause:** In the old build (`gulpfile.vscode.ts`), `vscodeEntryPoints` does NOT include `buildfile.keyboardMaps`. These files are only separate entry points for server-web (`gulpfile.reh.ts`) and web (`gulpfile.vscode.web.ts`). For desktop, they're imported as dependencies of `workbench.desktop.main` and get bundled into it. + +**Fix:** Removed `...keyboardMapEntryPoints` from the `desktop` case in `getEntryPointsForTarget()`. Keep for `server-web` and `web`. + +**Lesson:** Always verify new build entry points against the old build's per-target definitions in `buildfile.ts` and the respective gulpfiles. + +### 6. NLS Output File Parity + +**Problem:** `finalizeNLS()` was generating `nls.messages.js` (with `globalThis._VSCODE_NLS_MESSAGES=...`) in addition to the standard `.json` files. The old build only produces `nls.messages.json`, `nls.keys.json`, and `nls.metadata.json`. + +**Fix:** Removed `nls.messages.js` generation from `finalizeNLS()` in `nls-plugin.ts`. + +**Lesson:** Don't add new output file formats that create parity differences with the old build. The old build is the reference. + --- ## Testing the Fix From eae6d8b9c4ba1874f97fa38c35609c60ff01f306 Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 17:38:46 +0100 Subject: [PATCH 42/53] fix: update error pattern and file location in tasks.json for improved error handling --- .vscode/tasks.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8ea2442130836..14e637aaf2d1d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,13 +15,15 @@ "owner": "esbuild", "applyTo": "closedDocuments", "fileLocation": [ - "absolute" + "relative", + "${workspaceFolder}/src" ], "pattern": { - "regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$", + "regexp": "^(.+?):(\\d+):(\\d+): ERROR: (.+)$", "file": 1, - "location": 2, - "message": 3 + "line": 2, + "column": 3, + "message": 4 }, "background": { "beginsPattern": "Starting transpilation...", From ed009cc9ac12410a4d5e94326512cda8adcaa15a Mon Sep 17 00:00:00 2001 From: Johannes Date: Tue, 10 Feb 2026 17:45:28 +0100 Subject: [PATCH 43/53] include --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 606ebad07328c..5452598ee2e17 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "postinstall": "node build/npm/postinstall.ts", "compile": "npm run gulp compile", "compile-check-ts-native": "tsgo --project ./src/tsconfig.json --noEmit --skipLibCheck", - "watch": "npm-run-all2 -lp watch-client watch-extensions", + "watch": "npm-run-all2 -lp watch-client-transpile watch-client watch-extensions", "watchd": "deemon npm run watch", "watch-webd": "deemon npm run watch-web", "kill-watchd": "deemon --kill npm run watch", From 34ced5fa6ede3890c0e10b6adf93c246ebbb5a97 Mon Sep 17 00:00:00 2001 From: Johannes Date: Wed, 11 Feb 2026 10:21:02 +0100 Subject: [PATCH 44/53] write --- build/next/nls-plugin.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 53f5359c3b839..caae95cb434b1 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -136,6 +136,17 @@ export async function finalizeNLS( ), ])); + // Write nls.messages.js only to additional dirs (e.g., out-build) for CI NLS upload, + // not to the primary bundle output dir where it would be an extra file vs the old build + if (alsoWriteTo) { + await Promise.all(alsoWriteTo.map(dir => + fs.promises.writeFile( + path.join(dir, 'nls.messages.js'), + `/*---------------------------------------------------------\n * Copyright (C) Microsoft Corporation. All rights reserved.\n *--------------------------------------------------------*/\nglobalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` + ) + )); + } + console.log(`[nls] Extracted ${allMessages.length} messages from ${moduleToKeys.size} modules`); return { indexMap, messageCount: allMessages.length }; From bce538863f74c87c36f94a3b8dbbf109929c9ca9 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 11 Feb 2026 12:06:16 +0100 Subject: [PATCH 45/53] adds component explorer (#294075) * adds component explorer * Removes log --- .vscode/launch.json | 11 + build/vite/fixtures/aiStats.fixture.ts | 92 ++ build/vite/fixtures/baseUI.fixture.ts | 531 +++++++++ .../fixtures/editor/codeEditor.fixture.ts | 76 ++ .../editor/inlineCompletions.fixture.ts | 164 +++ build/vite/fixtures/fixtureUtils.ts | 512 +++++++++ build/vite/package-lock.json | 1009 ++++++++--------- build/vite/package.json | 11 +- build/vite/setup-dev.ts | 1 + build/vite/style.css | 6 + build/vite/vite.config.ts | 17 +- 11 files changed, 1886 insertions(+), 544 deletions(-) create mode 100644 build/vite/fixtures/aiStats.fixture.ts create mode 100644 build/vite/fixtures/baseUI.fixture.ts create mode 100644 build/vite/fixtures/editor/codeEditor.fixture.ts create mode 100644 build/vite/fixtures/editor/inlineCompletions.fixture.ts create mode 100644 build/vite/fixtures/fixtureUtils.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 9dbed82ee94c2..74dfd6a3da637 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -602,6 +602,17 @@ "order": 4 } }, + { + "name": "Component Explorer", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5199/___explorer", + "preLaunchTask": "Launch Monaco Editor Vite", + "presentation": { + "group": "monaco", + "order": 4 + } + }, { "name": "Monaco Editor - Self Contained Diff Editor", "type": "chrome", diff --git a/build/vite/fixtures/aiStats.fixture.ts b/build/vite/fixtures/aiStats.fixture.ts new file mode 100644 index 0000000000000..5ebe414b52b62 --- /dev/null +++ b/build/vite/fixtures/aiStats.fixture.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { observableValue } from '../../../src/vs/base/common/observable'; +import { createAiStatsHover, IAiStatsHoverData } from '../../../src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar'; +import { ISessionData } from '../../../src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart'; +import { Random } from '../../../src/vs/editor/test/common/core/random'; +import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils'; + +export default defineThemedFixtureGroup({ + AiStatsHover: defineComponentFixture({ + render: (context) => renderAiStatsHover({ ...context, data: createSampleDataWithSessions() }), + }), + + AiStatsHoverNoData: defineComponentFixture({ + render: (context) => renderAiStatsHover({ ...context, data: createEmptyData() }), + }), +}); + +function createSampleDataWithSessions(): IAiStatsHoverData { + const random = Random.create(42); + + // Use a fixed base time for determinism (Jan 1, 2025, 12:00:00 UTC) + const baseTime = 1735732800000; + const dayMs = 24 * 60 * 60 * 1000; + const sessionLengthMs = 5 * 60 * 1000; + + // Generate fake session data for the last 7 days + const fakeSessions: ISessionData[] = []; + for (let day = 6; day >= 0; day--) { + const dayStart = baseTime - day * dayMs; + const sessionsPerDay = random.nextIntRange(3, 9); + for (let s = 0; s < sessionsPerDay; s++) { + const sessionTime = dayStart + s * sessionLengthMs * 2; + fakeSessions.push({ + startTime: sessionTime, + typedCharacters: random.nextIntRange(100, 600), + aiCharacters: random.nextIntRange(200, 1000), + acceptedInlineSuggestions: random.nextIntRange(1, 16), + chatEditCount: random.nextIntRange(0, 5), + }); + } + } + + const totalAi = fakeSessions.reduce((sum, s) => sum + s.aiCharacters, 0); + const totalTyped = fakeSessions.reduce((sum, s) => sum + s.typedCharacters, 0); + const aiRate = totalAi / (totalAi + totalTyped); + + // "Today" for the fixture is the baseTime day + const startOfToday = baseTime - (baseTime % dayMs); + const todaySessions = fakeSessions.filter(s => s.startTime >= startOfToday); + const acceptedToday = todaySessions.reduce((sum, s) => sum + (s.acceptedInlineSuggestions ?? 0), 0); + + return { + aiRate: observableValue('aiRate', aiRate), + acceptedInlineSuggestionsToday: observableValue('acceptedToday', acceptedToday), + sessions: observableValue('sessions', fakeSessions), + }; +} + +function createEmptyData(): IAiStatsHoverData { + return { + aiRate: observableValue('aiRate', 0), + acceptedInlineSuggestionsToday: observableValue('acceptedToday', 0), + sessions: observableValue('sessions', []), + }; +} + +interface RenderAiStatsOptions extends ComponentFixtureContext { + data: IAiStatsHoverData; +} + +function renderAiStatsHover({ container, disposableStore, data }: RenderAiStatsOptions): HTMLElement { + container.style.width = '320px'; + container.style.padding = '8px'; + container.style.backgroundColor = 'var(--vscode-editorHoverWidget-background)'; + container.style.border = '1px solid var(--vscode-editorHoverWidget-border)'; + container.style.borderRadius = '4px'; + container.style.color = 'var(--vscode-editorHoverWidget-foreground)'; + + const hover = createAiStatsHover({ + data, + onOpenSettings: () => console.log('Open settings clicked'), + }); + + const elem = hover.keepUpdated(disposableStore).element; + container.appendChild(elem); + + return container; +} diff --git a/build/vite/fixtures/baseUI.fixture.ts b/build/vite/fixtures/baseUI.fixture.ts new file mode 100644 index 0000000000000..4bcf0be523519 --- /dev/null +++ b/build/vite/fixtures/baseUI.fixture.ts @@ -0,0 +1,531 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../src/vs/base/browser/dom'; +import { Codicon } from '../../../src/vs/base/common/codicons'; +import { ThemeIcon } from '../../../src/vs/base/common/themables'; +import { Action, Separator } from '../../../src/vs/base/common/actions'; + +// UI Components +import { Button, ButtonBar, ButtonWithDescription, unthemedButtonStyles } from '../../../src/vs/base/browser/ui/button/button'; +import { Toggle, Checkbox, unthemedToggleStyles } from '../../../src/vs/base/browser/ui/toggle/toggle'; +import { InputBox, MessageType, unthemedInboxStyles } from '../../../src/vs/base/browser/ui/inputbox/inputBox'; +import { CountBadge } from '../../../src/vs/base/browser/ui/countBadge/countBadge'; +import { ActionBar } from '../../../src/vs/base/browser/ui/actionbar/actionbar'; +import { ProgressBar } from '../../../src/vs/base/browser/ui/progressbar/progressbar'; +import { HighlightedLabel } from '../../../src/vs/base/browser/ui/highlightedlabel/highlightedLabel'; + +import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils'; + + +// ============================================================================ +// Styles (themed versions for fixture display) +// ============================================================================ + +const themedButtonStyles = { + ...unthemedButtonStyles, + buttonBackground: 'var(--vscode-button-background)', + buttonHoverBackground: 'var(--vscode-button-hoverBackground)', + buttonForeground: 'var(--vscode-button-foreground)', + buttonSecondaryBackground: 'var(--vscode-button-secondaryBackground)', + buttonSecondaryHoverBackground: 'var(--vscode-button-secondaryHoverBackground)', + buttonSecondaryForeground: 'var(--vscode-button-secondaryForeground)', + buttonBorder: 'var(--vscode-button-border)', +}; + +const themedToggleStyles = { + ...unthemedToggleStyles, + inputActiveOptionBorder: 'var(--vscode-inputOption-activeBorder)', + inputActiveOptionForeground: 'var(--vscode-inputOption-activeForeground)', + inputActiveOptionBackground: 'var(--vscode-inputOption-activeBackground)', +}; + +const themedCheckboxStyles = { + checkboxBackground: 'var(--vscode-checkbox-background)', + checkboxBorder: 'var(--vscode-checkbox-border)', + checkboxForeground: 'var(--vscode-checkbox-foreground)', + checkboxDisabledBackground: undefined, + checkboxDisabledForeground: undefined, +}; + +const themedInputBoxStyles = { + ...unthemedInboxStyles, + inputBackground: 'var(--vscode-input-background)', + inputForeground: 'var(--vscode-input-foreground)', + inputBorder: 'var(--vscode-input-border)', + inputValidationInfoBackground: 'var(--vscode-inputValidation-infoBackground)', + inputValidationInfoBorder: 'var(--vscode-inputValidation-infoBorder)', + inputValidationWarningBackground: 'var(--vscode-inputValidation-warningBackground)', + inputValidationWarningBorder: 'var(--vscode-inputValidation-warningBorder)', + inputValidationErrorBackground: 'var(--vscode-inputValidation-errorBackground)', + inputValidationErrorBorder: 'var(--vscode-inputValidation-errorBorder)', +}; + +const themedBadgeStyles = { + badgeBackground: 'var(--vscode-badge-background)', + badgeForeground: 'var(--vscode-badge-foreground)', + badgeBorder: undefined, +}; + +const themedProgressBarOptions = { + progressBarBackground: 'var(--vscode-progressBar-background)', +}; + + +// ============================================================================ +// Buttons +// ============================================================================ + +function renderButtons({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '12px'; + + // Section: Primary Buttons + const primarySection = $('div'); + primarySection.style.display = 'flex'; + primarySection.style.gap = '8px'; + primarySection.style.alignItems = 'center'; + container.appendChild(primarySection); + + const primaryButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Primary button' })); + primaryButton.label = 'Primary Button'; + + const primaryIconButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'With Icon', supportIcons: true })); + primaryIconButton.label = '$(add) Add Item'; + + const smallButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Small button', small: true })); + smallButton.label = 'Small'; + + // Section: Secondary Buttons + const secondarySection = $('div'); + secondarySection.style.display = 'flex'; + secondarySection.style.gap = '8px'; + secondarySection.style.alignItems = 'center'; + container.appendChild(secondarySection); + + const secondaryButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Secondary button' })); + secondaryButton.label = 'Secondary Button'; + + const secondaryIconButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Cancel', supportIcons: true })); + secondaryIconButton.label = '$(close) Cancel'; + + // Section: Disabled Buttons + const disabledSection = $('div'); + disabledSection.style.display = 'flex'; + disabledSection.style.gap = '8px'; + disabledSection.style.alignItems = 'center'; + container.appendChild(disabledSection); + + const disabledButton = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, title: 'Disabled', disabled: true })); + disabledButton.label = 'Disabled'; + disabledButton.enabled = false; + + const disabledSecondary = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, secondary: true, title: 'Disabled Secondary', disabled: true })); + disabledSecondary.label = 'Disabled Secondary'; + disabledSecondary.enabled = false; + + return container; +} + +function renderButtonBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '16px'; + + // Button Bar + const barContainer = $('div'); + container.appendChild(barContainer); + + const buttonBar = new ButtonBar(barContainer); + disposableStore.add(buttonBar); + + const okButton = buttonBar.addButton({ ...themedButtonStyles, title: 'OK' }); + okButton.label = 'OK'; + + const cancelButton = buttonBar.addButton({ ...themedButtonStyles, secondary: true, title: 'Cancel' }); + cancelButton.label = 'Cancel'; + + // Button with Description + const descContainer = $('div'); + descContainer.style.width = '300px'; + container.appendChild(descContainer); + + const buttonWithDesc = disposableStore.add(new ButtonWithDescription(descContainer, { ...themedButtonStyles, title: 'Install Extension', supportIcons: true })); + buttonWithDesc.label = '$(extensions) Install Extension'; + buttonWithDesc.description = 'This will install the extension and enable it globally'; + + return container; +} + + +// ============================================================================ +// Toggles and Checkboxes +// ============================================================================ + +function renderToggles({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '12px'; + + // Toggles + const toggleSection = $('div'); + toggleSection.style.display = 'flex'; + toggleSection.style.gap = '16px'; + toggleSection.style.alignItems = 'center'; + container.appendChild(toggleSection); + + const toggle1 = disposableStore.add(new Toggle({ + ...themedToggleStyles, + title: 'Case Sensitive', + isChecked: false, + icon: Codicon.caseSensitive, + })); + toggleSection.appendChild(toggle1.domNode); + + const toggle2 = disposableStore.add(new Toggle({ + ...themedToggleStyles, + title: 'Whole Word', + isChecked: true, + icon: Codicon.wholeWord, + })); + toggleSection.appendChild(toggle2.domNode); + + const toggle3 = disposableStore.add(new Toggle({ + ...themedToggleStyles, + title: 'Use Regular Expression', + isChecked: false, + icon: Codicon.regex, + })); + toggleSection.appendChild(toggle3.domNode); + + // Checkboxes + const checkboxSection = $('div'); + checkboxSection.style.display = 'flex'; + checkboxSection.style.flexDirection = 'column'; + checkboxSection.style.gap = '8px'; + container.appendChild(checkboxSection); + + const createCheckboxRow = (label: string, checked: boolean) => { + const row = $('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + + const checkbox = disposableStore.add(new Checkbox(label, checked, themedCheckboxStyles)); + row.appendChild(checkbox.domNode); + + const labelEl = $('span'); + labelEl.textContent = label; + labelEl.style.color = 'var(--vscode-foreground)'; + row.appendChild(labelEl); + + return row; + }; + + checkboxSection.appendChild(createCheckboxRow('Enable auto-save', true)); + checkboxSection.appendChild(createCheckboxRow('Show line numbers', true)); + checkboxSection.appendChild(createCheckboxRow('Word wrap', false)); + + return container; +} + + +// ============================================================================ +// Input Boxes +// ============================================================================ + +function renderInputBoxes({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '16px'; + container.style.width = '350px'; + + // Normal input + const normalInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Enter search query...', + inputBoxStyles: themedInputBoxStyles, + })); + + // Input with value + const filledInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'File path', + inputBoxStyles: themedInputBoxStyles, + })); + filledInput.value = '/src/vs/editor/browser'; + + // Input with info validation + const infoInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Username', + inputBoxStyles: themedInputBoxStyles, + validationOptions: { + validation: (value) => value.length < 3 ? { content: 'Username must be at least 3 characters', type: MessageType.INFO } : null + } + })); + infoInput.value = 'ab'; + infoInput.validate(); + + // Input with warning validation + const warningInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Password', + inputBoxStyles: themedInputBoxStyles, + validationOptions: { + validation: (value) => value.length < 8 ? { content: 'Password should be at least 8 characters for security', type: MessageType.WARNING } : null + } + })); + warningInput.value = 'pass'; + warningInput.validate(); + + // Input with error validation + const errorInput = disposableStore.add(new InputBox(container, undefined, { + placeholder: 'Email address', + inputBoxStyles: themedInputBoxStyles, + validationOptions: { + validation: (value) => !value.includes('@') ? { content: 'Please enter a valid email address', type: MessageType.ERROR } : null + } + })); + errorInput.value = 'invalid-email'; + errorInput.validate(); + + return container; +} + + +// ============================================================================ +// Count Badges +// ============================================================================ + +function renderCountBadges({ container }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.gap = '12px'; + container.style.alignItems = 'center'; + + // Various badge counts + const counts = [1, 5, 12, 99, 999]; + + for (const count of counts) { + const badgeContainer = $('div'); + badgeContainer.style.display = 'flex'; + badgeContainer.style.alignItems = 'center'; + badgeContainer.style.gap = '8px'; + + const label = $('span'); + label.textContent = 'Issues'; + label.style.color = 'var(--vscode-foreground)'; + badgeContainer.appendChild(label); + + new CountBadge(badgeContainer, { count }, themedBadgeStyles); + container.appendChild(badgeContainer); + } + + return container; +} + + +// ============================================================================ +// Action Bar +// ============================================================================ + +function renderActionBar({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '16px'; + + // Horizontal action bar + const horizontalLabel = $('div'); + horizontalLabel.textContent = 'Horizontal Actions:'; + horizontalLabel.style.color = 'var(--vscode-foreground)'; + horizontalLabel.style.marginBottom = '4px'; + container.appendChild(horizontalLabel); + + const horizontalContainer = $('div'); + container.appendChild(horizontalContainer); + + const horizontalBar = disposableStore.add(new ActionBar(horizontalContainer, { + ariaLabel: 'Editor Actions', + })); + + horizontalBar.push([ + new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save')), + new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo')), + new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo')), + new Separator(), + new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find')), + new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace')), + ]); + + // Action bar with disabled items + const mixedLabel = $('div'); + mixedLabel.textContent = 'Mixed States:'; + mixedLabel.style.color = 'var(--vscode-foreground)'; + mixedLabel.style.marginBottom = '4px'; + container.appendChild(mixedLabel); + + const mixedContainer = $('div'); + container.appendChild(mixedContainer); + + const mixedBar = disposableStore.add(new ActionBar(mixedContainer, { + ariaLabel: 'Mixed Actions', + })); + + mixedBar.push([ + new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { }), + new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }), + new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }), + ]); + + return container; +} + + +// ============================================================================ +// Progress Bar +// ============================================================================ + +function renderProgressBars({ container, disposableStore }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '24px'; + container.style.width = '400px'; + + const createSection = (label: string) => { + const section = $('div'); + const labelEl = $('div'); + labelEl.textContent = label; + labelEl.style.color = 'var(--vscode-foreground)'; + labelEl.style.marginBottom = '8px'; + labelEl.style.fontSize = '12px'; + section.appendChild(labelEl); + + // Progress bar container with proper constraints + const barContainer = $('div'); + barContainer.style.position = 'relative'; + barContainer.style.width = '100%'; + barContainer.style.height = '4px'; + barContainer.style.overflow = 'hidden'; + section.appendChild(barContainer); + + container.appendChild(section); + return barContainer; + }; + + // Infinite progress + const infiniteSection = createSection('Infinite Progress (loading...)'); + const infiniteBar = disposableStore.add(new ProgressBar(infiniteSection, themedProgressBarOptions)); + infiniteBar.infinite(); + + // Discrete progress - 30% + const progress30Section = createSection('Discrete Progress - 30%'); + const progress30Bar = disposableStore.add(new ProgressBar(progress30Section, themedProgressBarOptions)); + progress30Bar.total(100); + progress30Bar.worked(30); + + // Discrete progress - 60% + const progress60Section = createSection('Discrete Progress - 60%'); + const progress60Bar = disposableStore.add(new ProgressBar(progress60Section, themedProgressBarOptions)); + progress60Bar.total(100); + progress60Bar.worked(60); + + // Discrete progress - 90% + const progress90Section = createSection('Discrete Progress - 90%'); + const progress90Bar = disposableStore.add(new ProgressBar(progress90Section, themedProgressBarOptions)); + progress90Bar.total(100); + progress90Bar.worked(90); + + // Completed progress + const doneSection = createSection('Completed (100%)'); + const doneBar = disposableStore.add(new ProgressBar(doneSection, themedProgressBarOptions)); + doneBar.total(100); + doneBar.worked(100); + + return container; +} + + +// ============================================================================ +// Highlighted Label +// ============================================================================ + +function renderHighlightedLabels({ container }: ComponentFixtureContext): HTMLElement { + container.style.padding = '16px'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '8px'; + container.style.color = 'var(--vscode-foreground)'; + + const createHighlightedLabel = (text: string, highlights: { start: number; end: number }[]) => { + const row = $('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + + const labelContainer = $('div'); + const label = new HighlightedLabel(labelContainer); + label.set(text, highlights); + row.appendChild(labelContainer); + + const queryLabel = $('span'); + queryLabel.style.color = 'var(--vscode-descriptionForeground)'; + queryLabel.style.fontSize = '12px'; + queryLabel.textContent = `(matches highlighted)`; + row.appendChild(queryLabel); + + return row; + }; + + // File search examples + container.appendChild(createHighlightedLabel('codeEditorWidget.ts', [{ start: 0, end: 4 }])); // "code" + container.appendChild(createHighlightedLabel('inlineCompletionsController.ts', [{ start: 6, end: 10 }])); // "Comp" + container.appendChild(createHighlightedLabel('diffEditorViewModel.ts', [{ start: 0, end: 4 }, { start: 10, end: 14 }])); // "diff" and "View" + container.appendChild(createHighlightedLabel('workbenchTestServices.ts', [{ start: 9, end: 13 }])); // "Test" + + return container; +} + + +// ============================================================================ +// Export Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ + Buttons: defineComponentFixture({ + render: renderButtons, + }), + + ButtonBar: defineComponentFixture({ + render: renderButtonBar, + }), + + Toggles: defineComponentFixture({ + render: renderToggles, + }), + + InputBoxes: defineComponentFixture({ + render: renderInputBoxes, + }), + + CountBadges: defineComponentFixture({ + render: renderCountBadges, + }), + + ActionBar: defineComponentFixture({ + render: renderActionBar, + }), + + ProgressBars: defineComponentFixture({ + render: renderProgressBars, + }), + + HighlightedLabels: defineComponentFixture({ + render: renderHighlightedLabels, + }), +}); diff --git a/build/vite/fixtures/editor/codeEditor.fixture.ts b/build/vite/fixtures/editor/codeEditor.fixture.ts new file mode 100644 index 0000000000000..bdaaf35bf0fcf --- /dev/null +++ b/build/vite/fixtures/editor/codeEditor.fixture.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../src/vs/base/common/uri'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../src/vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils'; + +const SAMPLE_CODE = `// Welcome to VS Code +function greet(name: string): string { + return \`Hello, \${name}!\`; +} + +class Counter { + private _count = 0; + + increment(): void { + this._count++; + } + + get count(): number { + return this._count; + } +} + +const counter = new Counter(); +counter.increment(); +console.log(greet('World')); +console.log(\`Count: \${counter.count}\`); +`; + +function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtureContext): HTMLElement { + container.style.width = '600px'; + container.style.height = '400px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const model = disposableStore.add(createTextModel( + instantiationService, + SAMPLE_CODE, + URI.parse('inmemory://sample.ts'), + 'typescript' + )); + + const editorOptions: ICodeEditorWidgetOptions = { + contributions: [] + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: true }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + fontFamily: 'Consolas, "Courier New", monospace', + renderWhitespace: 'selection', + bracketPairColorization: { enabled: true }, + }, + editorOptions + )); + + editor.setModel(model); + + return container; +} + +export default defineThemedFixtureGroup({ + CodeEditor: defineComponentFixture({ + render: (context) => renderCodeEditor(context), + }), +}); diff --git a/build/vite/fixtures/editor/inlineCompletions.fixture.ts b/build/vite/fixtures/editor/inlineCompletions.fixture.ts new file mode 100644 index 0000000000000..06abdece6b686 --- /dev/null +++ b/build/vite/fixtures/editor/inlineCompletions.fixture.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { constObservable } from '../../../../src/vs/base/common/observable'; +import { URI } from '../../../../src/vs/base/common/uri'; +import { Range } from '../../../../src/vs/editor/common/core/range'; +import { IEditorOptions } from '../../../../src/vs/editor/common/config/editorOptions'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../src/vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { EditorExtensionsRegistry } from '../../../../src/vs/editor/browser/editorExtensions'; +import { InlineCompletionsController } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController'; +import { InlineCompletionsSource, InlineCompletionsState } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource'; +import { InlineEditItem } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem'; +import { TextModelValueReference } from '../../../../src/vs/editor/contrib/inlineCompletions/browser/model/textModelValueReference'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils'; + +// Import to register the inline completions contribution +import '../../../../src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution'; + + +// ============================================================================ +// Inline Edit Fixture +// ============================================================================ + +interface InlineEditOptions extends ComponentFixtureContext { + code: string; + cursorLine: number; + range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; + newText: string; + width?: string; + height?: string; + editorOptions?: IEditorOptions; +} + +function renderInlineEdit(options: InlineEditOptions): HTMLElement { + const { container, disposableStore, theme } = options; + container.style.width = options.width ?? '500px'; + container.style.height = options.height ?? '170px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + options.code, + URI.parse('inmemory://inline-edit.ts'), + 'typescript' + )); + + // Mock the InlineCompletionsSource to provide our test completion + instantiationService.stubInstance(InlineCompletionsSource, { + cancelUpdate: () => { }, + clear: () => { }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + clearOperationOnTextModelChange: constObservable(undefined) as any, + clearSuggestWidgetInlineCompletions: () => { }, + dispose: () => { }, + fetch: async () => true, + inlineCompletions: constObservable(new InlineCompletionsState([ + InlineEditItem.createForTest( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TextModelValueReference.snapshot(textModel as any), + new Range( + options.range.startLineNumber, + options.range.startColumn, + options.range.endLineNumber, + options.range.endColumn + ), + options.newText + ) + ], undefined)), + loading: constObservable(false), + seedInlineCompletionsWithSuggestWidget: () => { }, + seedWithCompletion: () => { }, + suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + }); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: EditorExtensionsRegistry.getEditorContributions() + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + ...options.editorOptions, + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + editor.setPosition({ lineNumber: options.cursorLine, column: 1 }); + editor.focus(); + + // Trigger inline completions + const controller = InlineCompletionsController.get(editor); + controller?.model?.get(); + + return container; +} + + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ + // Side-by-side view: Multi-line replacement + SideBySideView: defineComponentFixture({ + render: (context) => renderInlineEdit({ + ...context, + code: `function greet(name) { + console.log("Hello, " + name); +}`, + cursorLine: 2, + range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 100 }, + newText: ' console.log(`Hello, ${name}!`);', + }), + }), + + // Word replacement view: Single word change + WordReplacementView: defineComponentFixture({ + render: (context) => renderInlineEdit({ + ...context, + code: `class BufferData { + append(data: number[]) { + this.data.push(data); + } +}`, + cursorLine: 2, + range: { startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 8 }, + newText: 'push', + height: '200px', + }), + }), + + // Insertion view: Insert new content + InsertionView: defineComponentFixture({ + render: (context) => renderInlineEdit({ + ...context, + code: `class BufferData { + append(data: number[]) {} // appends data +}`, + cursorLine: 2, + range: { startLineNumber: 2, startColumn: 26, endLineNumber: 2, endColumn: 26 }, + newText: ` + console.log(data); + `, + height: '200px', + editorOptions: { + inlineSuggest: { + edits: { allowCodeShifting: 'always' } + } + } + }), + }), +}); diff --git a/build/vite/fixtures/fixtureUtils.ts b/build/vite/fixtures/fixtureUtils.ts new file mode 100644 index 0000000000000..fd90169dae792 --- /dev/null +++ b/build/vite/fixtures/fixtureUtils.ts @@ -0,0 +1,512 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer'; +import { DisposableStore, toDisposable } from '../../../src/vs/base/common/lifecycle'; +import { URI } from '../../../src/vs/base/common/uri'; +import '../style.css'; + +// Theme +import { COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS } from '../../../src/vs/workbench/services/themes/common/workbenchThemeService'; +import { ColorThemeData } from '../../../src/vs/workbench/services/themes/common/colorThemeData'; +import { ColorScheme } from '../../../src/vs/platform/theme/common/theme'; +import { generateColorThemeCSS } from '../../../src/vs/workbench/services/themes/browser/colorThemeCss'; +import { Registry } from '../../../src/vs/platform/registry/common/platform'; +import { Extensions as ThemingExtensions, IThemingRegistry } from '../../../src/vs/platform/theme/common/themeService'; +import { IEnvironmentService } from '../../../src/vs/platform/environment/common/environment'; +import { getIconsStyleSheet } from '../../../src/vs/platform/theme/browser/iconsStyleSheet'; + +// Instantiation +import { ServiceCollection } from '../../../src/vs/platform/instantiation/common/serviceCollection'; +import { SyncDescriptor } from '../../../src/vs/platform/instantiation/common/descriptors'; +import { ServiceIdentifier } from '../../../src/vs/platform/instantiation/common/instantiation'; +import { TestInstantiationService } from '../../../src/vs/platform/instantiation/test/common/instantiationServiceMock'; + +// Test service implementations +import { TestAccessibilityService } from '../../../src/vs/platform/accessibility/test/common/testAccessibilityService'; +import { MockKeybindingService, MockContextKeyService } from '../../../src/vs/platform/keybinding/test/common/mockKeybindingService'; +import { TestClipboardService } from '../../../src/vs/platform/clipboard/test/common/testClipboardService'; +import { TestEditorWorkerService } from '../../../src/vs/editor/test/common/services/testEditorWorkerService'; +import { NullOpenerService } from '../../../src/vs/platform/opener/test/common/nullOpenerService'; +import { TestNotificationService } from '../../../src/vs/platform/notification/test/common/testNotificationService'; +import { TestDialogService } from '../../../src/vs/platform/dialogs/test/common/testDialogService'; +import { TestConfigurationService } from '../../../src/vs/platform/configuration/test/common/testConfigurationService'; +import { TestTextResourcePropertiesService } from '../../../src/vs/editor/test/common/services/testTextResourcePropertiesService'; +import { TestThemeService } from '../../../src/vs/platform/theme/test/common/testThemeService'; +import { TestLanguageConfigurationService } from '../../../src/vs/editor/test/common/modes/testLanguageConfigurationService'; +import { TestCodeEditorService, TestCommandService } from '../../../src/vs/editor/test/browser/editorTestServices'; +import { TestTreeSitterLibraryService } from '../../../src/vs/editor/test/common/services/testTreeSitterLibraryService'; +import { TestMenuService } from '../../../src/vs/workbench/test/browser/workbenchTestServices'; + +// Service interfaces +import { IAccessibilityService } from '../../../src/vs/platform/accessibility/common/accessibility'; +import { IKeybindingService } from '../../../src/vs/platform/keybinding/common/keybinding'; +import { IClipboardService } from '../../../src/vs/platform/clipboard/common/clipboardService'; +import { IEditorWorkerService } from '../../../src/vs/editor/common/services/editorWorker'; +import { IOpenerService } from '../../../src/vs/platform/opener/common/opener'; +import { INotificationService } from '../../../src/vs/platform/notification/common/notification'; +import { IDialogService } from '../../../src/vs/platform/dialogs/common/dialogs'; +import { IUndoRedoService } from '../../../src/vs/platform/undoRedo/common/undoRedo'; +import { UndoRedoService } from '../../../src/vs/platform/undoRedo/common/undoRedoService'; +import { ILanguageService } from '../../../src/vs/editor/common/languages/language'; +import { LanguageService } from '../../../src/vs/editor/common/services/languageService'; +import { ILanguageConfigurationService } from '../../../src/vs/editor/common/languages/languageConfigurationRegistry'; +import { IConfigurationService } from '../../../src/vs/platform/configuration/common/configuration'; +import { ITextResourcePropertiesService } from '../../../src/vs/editor/common/services/textResourceConfiguration'; +import { IColorTheme, IThemeService } from '../../../src/vs/platform/theme/common/themeService'; +import { ILogService, NullLogService, ILoggerService, NullLoggerService } from '../../../src/vs/platform/log/common/log'; +import { IModelService } from '../../../src/vs/editor/common/services/model'; +import { ModelService } from '../../../src/vs/editor/common/services/modelService'; +import { ICodeEditorService } from '../../../src/vs/editor/browser/services/codeEditorService'; +import { IContextKeyService } from '../../../src/vs/platform/contextkey/common/contextkey'; +import { ICommandService } from '../../../src/vs/platform/commands/common/commands'; +import { ITelemetryService } from '../../../src/vs/platform/telemetry/common/telemetry'; +import { NullTelemetryServiceShape } from '../../../src/vs/platform/telemetry/common/telemetryUtils'; +import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../src/vs/editor/common/services/languageFeatureDebounce'; +import { ILanguageFeaturesService } from '../../../src/vs/editor/common/services/languageFeatures'; +import { LanguageFeaturesService } from '../../../src/vs/editor/common/services/languageFeaturesService'; +import { ITreeSitterLibraryService } from '../../../src/vs/editor/common/services/treeSitter/treeSitterLibraryService'; +import { IInlineCompletionsService, InlineCompletionsService } from '../../../src/vs/editor/browser/services/inlineCompletionsService'; +import { ICodeLensCache } from '../../../src/vs/editor/contrib/codelens/browser/codeLensCache'; +import { IHoverService } from '../../../src/vs/platform/hover/browser/hover'; +import { IDataChannelService, NullDataChannelService } from '../../../src/vs/platform/dataChannel/common/dataChannel'; +import { IContextMenuService, IContextViewService } from '../../../src/vs/platform/contextview/browser/contextView'; +import { ILabelService } from '../../../src/vs/platform/label/common/label'; +import { IMenuService } from '../../../src/vs/platform/actions/common/actions'; +import { IActionViewItemService, NullActionViewItemService } from '../../../src/vs/platform/actions/browser/actionViewItemService'; +import { IDefaultAccountService } from '../../../src/vs/platform/defaultAccount/common/defaultAccount'; +import { IStorageService, IStorageValueChangeEvent, IWillSaveStateEvent, StorageScope, StorageTarget, IStorageTargetChangeEvent, IStorageEntry, WillSaveStateReason, IWorkspaceStorageValueChangeEvent, IProfileStorageValueChangeEvent, IApplicationStorageValueChangeEvent } from '../../../src/vs/platform/storage/common/storage'; +import { Emitter, Event } from '../../../src/vs/base/common/event'; +import { mock } from '../../../src/vs/base/test/common/mock'; +import { IAnyWorkspaceIdentifier } from '../../../src/vs/platform/workspace/common/workspace'; +import { IUserDataProfile } from '../../../src/vs/platform/userDataProfile/common/userDataProfile'; +import { IUserInteractionService, MockUserInteractionService } from '../../../src/vs/platform/userInteraction/browser/userInteractionService'; + +// Editor +import { ITextModel } from '../../../src/vs/editor/common/model'; + + + +// Import color registrations to ensure colors are available +import '../../../src/vs/platform/theme/common/colors/baseColors'; +import '../../../src/vs/platform/theme/common/colors/editorColors'; +import '../../../src/vs/platform/theme/common/colors/listColors'; +import '../../../src/vs/platform/theme/common/colors/miscColors'; +import '../../../src/vs/workbench/common/theme'; + +/** + * A storage service that never stores anything and always returns the default/fallback value. + * This is useful for fixtures where we want consistent behavior without persisted state. + */ +class NullStorageService implements IStorageService { + + declare readonly _serviceBrand: undefined; + + private readonly _onDidChangeValue = new Emitter(); + onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event; + onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event; + onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event; + onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event { + return Event.filter(this._onDidChangeValue.event, e => e.scope === scope && (key === undefined || e.key === key), disposable); + } + + private readonly _onDidChangeTarget = new Emitter(); + readonly onDidChangeTarget: Event = this._onDidChangeTarget.event; + + private readonly _onWillSaveState = new Emitter(); + readonly onWillSaveState: Event = this._onWillSaveState.event; + + get(key: string, scope: StorageScope, fallbackValue: string): string; + get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined; + get(_key: string, _scope: StorageScope, fallbackValue?: string): string | undefined { + return fallbackValue; + } + + getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean; + getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined; + getBoolean(_key: string, _scope: StorageScope, fallbackValue?: boolean): boolean | undefined { + return fallbackValue; + } + + getNumber(key: string, scope: StorageScope, fallbackValue: number): number; + getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined; + getNumber(_key: string, _scope: StorageScope, fallbackValue?: number): number | undefined { + return fallbackValue; + } + + getObject(key: string, scope: StorageScope, fallbackValue: T): T; + getObject(key: string, scope: StorageScope, fallbackValue?: T): T | undefined; + getObject(_key: string, _scope: StorageScope, fallbackValue?: T): T | undefined { + return fallbackValue; + } + + store(_key: string, _value: string | boolean | number | undefined | null, _scope: StorageScope, _target: StorageTarget): void { + // no-op + } + + storeAll(_entries: IStorageEntry[], _external: boolean): void { + // no-op + } + + remove(_key: string, _scope: StorageScope): void { + // no-op + } + + isNew(_scope: StorageScope): boolean { + return true; + } + + flush(_reason?: WillSaveStateReason): Promise { + return Promise.resolve(); + } + + optimize(_scope: StorageScope): Promise { + return Promise.resolve(); + } + + log(): void { + // no-op + } + + keys(_scope: StorageScope, _target: StorageTarget): string[] { + return []; + } + + switch(): Promise { + return Promise.resolve(); + } + + hasScope(_scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean { + return false; + } +} + + +// ============================================================================ +// Themes +// ============================================================================ + +const themingRegistry = Registry.as(ThemingExtensions.ThemingContribution); +const mockEnvironmentService: IEnvironmentService = Object.create(null); + +export const darkTheme = ColorThemeData.createUnloadedThemeForThemeType( + ColorScheme.DARK, + COLOR_THEME_DARK_INITIAL_COLORS +); + +export const lightTheme = ColorThemeData.createUnloadedThemeForThemeType( + ColorScheme.LIGHT, + COLOR_THEME_LIGHT_INITIAL_COLORS +); + +let globalStyleSheet: CSSStyleSheet | undefined; +let iconsStyleSheetCache: CSSStyleSheet | undefined; +let darkThemeStyleSheet: CSSStyleSheet | undefined; +let lightThemeStyleSheet: CSSStyleSheet | undefined; + +function getGlobalStyleSheet(): CSSStyleSheet { + if (!globalStyleSheet) { + globalStyleSheet = new CSSStyleSheet(); + const globalRules: string[] = []; + for (const sheet of Array.from(document.styleSheets)) { + try { + for (const rule of Array.from(sheet.cssRules)) { + globalRules.push(rule.cssText); + } + } catch { + // Cross-origin stylesheets can't be read + } + } + globalStyleSheet.replaceSync(globalRules.join('\n')); + } + return globalStyleSheet; +} + +function getIconsStyleSheetCached(): CSSStyleSheet { + if (!iconsStyleSheetCache) { + iconsStyleSheetCache = new CSSStyleSheet(); + const iconsSheet = getIconsStyleSheet(undefined); + iconsStyleSheetCache.replaceSync(iconsSheet.getCSS() as string); + iconsSheet.dispose(); + } + return iconsStyleSheetCache; +} + +function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet { + const isDark = theme.type === ColorScheme.DARK; + if (isDark && darkThemeStyleSheet) { + return darkThemeStyleSheet; + } + if (!isDark && lightThemeStyleSheet) { + return lightThemeStyleSheet; + } + + const sheet = new CSSStyleSheet(); + const css = generateColorThemeCSS( + theme, + ':host', + themingRegistry.getThemingParticipants(), + mockEnvironmentService + ); + sheet.replaceSync(css.code); + + if (isDark) { + darkThemeStyleSheet = sheet; + } else { + lightThemeStyleSheet = sheet; + } + return sheet; +} + +/** + * Applies theme styling to a shadow DOM container. + * Adds theme class names and adopts shared stylesheets. + */ +export function setupTheme(container: HTMLElement, theme: ColorThemeData): void { + container.classList.add(...theme.classNames); + + const shadowRoot = container.getRootNode() as ShadowRoot; + if (shadowRoot.adoptedStyleSheets !== undefined) { + shadowRoot.adoptedStyleSheets = [ + getGlobalStyleSheet(), + getIconsStyleSheetCached(), + getThemeStyleSheet(theme), + ]; + } +} + + +// ============================================================================ +// Services +// ============================================================================ + +export interface ServiceRegistration { + define(id: ServiceIdentifier, ctor: new (...args: never[]) => T): void; + defineInstance(id: ServiceIdentifier, instance: T): void; +} + +export interface CreateServicesOptions { + /** + * The color theme to use for the theme service. + */ + colorTheme?: IColorTheme; + /** + * Additional services to register after the base editor services. + */ + additionalServices?: (registration: ServiceRegistration) => void; +} + +/** + * Creates a TestInstantiationService with all services needed for CodeEditorWidget. + * Additional services can be registered via the options callback. + */ +export function createEditorServices(disposables: DisposableStore, options?: CreateServicesOptions): TestInstantiationService { + const services = new ServiceCollection(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const serviceIdentifiers: ServiceIdentifier[] = []; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const define = (id: ServiceIdentifier, ctor: new (...args: any[]) => T) => { + if (!services.has(id)) { + services.set(id, new SyncDescriptor(ctor)); + } + serviceIdentifiers.push(id); + }; + + const defineInstance = (id: ServiceIdentifier, instance: T) => { + if (!services.has(id)) { + services.set(id, instance); + } + serviceIdentifiers.push(id); + }; + + // Base editor services + define(IAccessibilityService, TestAccessibilityService); + define(IKeybindingService, MockKeybindingService); + define(IClipboardService, TestClipboardService); + define(IEditorWorkerService, TestEditorWorkerService); + defineInstance(IOpenerService, NullOpenerService); + define(INotificationService, TestNotificationService); + define(IDialogService, TestDialogService); + define(IUndoRedoService, UndoRedoService); + define(ILanguageService, LanguageService); + define(ILanguageConfigurationService, TestLanguageConfigurationService); + define(IConfigurationService, TestConfigurationService); + define(ITextResourcePropertiesService, TestTextResourcePropertiesService); + defineInstance(IStorageService, new NullStorageService()); + if (options?.colorTheme) { + defineInstance(IThemeService, new TestThemeService(options.colorTheme)); + } else { + define(IThemeService, TestThemeService); + } + define(ILogService, NullLogService); + define(IModelService, ModelService); + define(ICodeEditorService, TestCodeEditorService); + define(IContextKeyService, MockContextKeyService); + define(ICommandService, TestCommandService); + define(ITelemetryService, NullTelemetryServiceShape); + define(ILoggerService, NullLoggerService); + define(IDataChannelService, NullDataChannelService); + define(IEnvironmentService, class extends mock() { + declare readonly _serviceBrand: undefined; + override isBuilt: boolean = true; + override isExtensionDevelopment: boolean = false; + }); + define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService); + define(ILanguageFeaturesService, LanguageFeaturesService); + define(ITreeSitterLibraryService, TestTreeSitterLibraryService); + define(IInlineCompletionsService, InlineCompletionsService); + defineInstance(ICodeLensCache, { + _serviceBrand: undefined, + put: () => { }, + get: () => undefined, + delete: () => { }, + } as ICodeLensCache); + defineInstance(IHoverService, { + _serviceBrand: undefined, + showDelayedHover: () => undefined, + setupDelayedHover: () => ({ dispose: () => { } }), + setupDelayedHoverAtMouse: () => ({ dispose: () => { } }), + showInstantHover: () => undefined, + hideHover: () => { }, + showAndFocusLastHover: () => { }, + setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }), + showManagedHover: () => { }, + } as IHoverService); + defineInstance(IDefaultAccountService, { + _serviceBrand: undefined, + onDidChangeDefaultAccount: new Emitter().event, + onDidChangePolicyData: new Emitter().event, + policyData: null, + getDefaultAccount: async () => null, + getDefaultAccountAuthenticationProvider: () => ({ id: 'test', name: 'Test', scopes: [], enterprise: false }), + setDefaultAccountProvider: () => { }, + refresh: async () => null, + signIn: async () => null, + } as IDefaultAccountService); + + // User interaction service with focus simulation enabled (all elements appear focused in fixtures) + defineInstance(IUserInteractionService, new MockUserInteractionService(true, false)); + + // Allow additional services to be registered + options?.additionalServices?.({ define, defineInstance }); + + const instantiationService = disposables.add(new TestInstantiationService(services, true)); + + disposables.add(toDisposable(() => { + for (const id of serviceIdentifiers) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const instanceOrDescriptor = services.get(id) as any; + if (typeof instanceOrDescriptor?.dispose === 'function') { + instanceOrDescriptor.dispose(); + } + } + })); + + return instantiationService; +} + +/** + * Registers additional services needed by workbench components (merge editor, etc.). + * Use with createEditorServices additionalServices option. + */ +export function registerWorkbenchServices(registration: ServiceRegistration): void { + registration.defineInstance(IContextMenuService, { + showContextMenu: () => { }, + onDidShowContextMenu: () => ({ dispose: () => { } }), + onDidHideContextMenu: () => ({ dispose: () => { } }), + } as unknown as IContextMenuService); + + registration.defineInstance(IContextViewService, { + showContextView: () => ({ dispose: () => { } }), + hideContextView: () => { }, + getContextViewElement: () => null, + layout: () => { }, + } as unknown as IContextViewService); + + registration.defineInstance(ILabelService, { + getUriLabel: (uri: URI) => uri.path, + getUriBasenameLabel: (uri: URI) => uri.path.split('/').pop() ?? '', + getWorkspaceLabel: () => '', + getHostLabel: () => '', + getSeparator: () => '/', + registerFormatter: () => ({ dispose: () => { } }), + onDidChangeFormatters: () => ({ dispose: () => { } }), + registerCachedFormatter: () => ({ dispose: () => { } }), + } as unknown as ILabelService); + + registration.define(IMenuService, TestMenuService); + registration.define(IActionViewItemService, NullActionViewItemService); +} + + +// ============================================================================ +// Text Models +// ============================================================================ + +/** + * Creates a text model using the ModelService. + */ +export function createTextModel( + instantiationService: TestInstantiationService, + text: string, + uri: URI, + languageId?: string +): ITextModel { + const modelService = instantiationService.get(IModelService); + const languageService = instantiationService.get(ILanguageService); + const languageSelection = languageId ? languageService.createById(languageId) : null; + return modelService.createModel(text, languageSelection, uri); +} + + +// ============================================================================ +// Fixture Adapters +// ============================================================================ + +export interface ComponentFixtureContext { + container: HTMLElement; + disposableStore: DisposableStore; + theme: ColorThemeData; +} + +export interface ComponentFixtureOptions { + render: (context: ComponentFixtureContext) => HTMLElement | Promise; +} + +type ThemedFixtures = ReturnType; + +/** + * Creates Dark and Light fixture variants from a single render function. + * The render function receives a context with container and disposableStore. + */ +export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures { + const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({ + isolation: 'shadow-dom', + displayMode: { type: 'component' }, + properties: [], + background: theme === darkTheme ? 'dark' : 'light', + render: async (container: HTMLElement) => { + const disposableStore = new DisposableStore(); + setupTheme(container, theme); + return options.render({ container, disposableStore, theme }); + }, + }); + + return defineFixtureVariants({ + Dark: createFixture(darkTheme), + Light: createFixture(lightTheme), + }); +} + +type ThemedFixtureGroupInput = Record; + +/** + * Creates a nested fixture group from themed fixtures. + * E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light + */ +export function defineThemedFixtureGroup(group: ThemedFixtureGroupInput): ReturnType { + return defineFixtureGroup(group); +} diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index 97fb9dc76aa64..7c67f659f0a54 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,96 +8,87 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/rollup-plugin-esm-url": "^1.0.1-0", - "vite": "^7.1.11" + "@vscode/component-explorer": "0.1.1-1", + "@vscode/component-explorer-vite-plugin": "0.1.1-1", + "@vscode/rollup-plugin-esm-url": "^1.0.1-1", + "vite": "npm:rolldown-vite@latest" } }, - "../lib": { - "name": "monaco-editor-core", - "version": "0.0.0", - "extraneous": true, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "postcss-copy": "^7.1.0", - "postcss-copy-assets": "^0.3.1", - "rollup": "^4.35.0", - "rollup-plugin-esbuild": "^6.2.1", - "rollup-plugin-lib-style": "^2.3.2", - "rollup-plugin-postcss": "^4.0.2" + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], + "node_modules/@oxc-project/runtime": { + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", + "integrity": "sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "node_modules/@oxc-project/types": { + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.101.0.tgz", + "integrity": "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==", "cpu": [ "arm64" ], @@ -105,18 +96,18 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -125,30 +116,30 @@ "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==", "cpu": [ "x64" ], @@ -159,13 +150,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.53.tgz", + "integrity": "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==", "cpu": [ "arm" ], @@ -176,13 +167,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==", "cpu": [ "arm64" ], @@ -193,15 +184,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", @@ -210,15 +201,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", @@ -227,15 +218,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==", "cpu": [ - "mips64el" + "x64" ], "dev": true, "license": "MIT", @@ -244,64 +235,64 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.53.tgz", + "integrity": "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==", "cpu": [ - "riscv64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.0" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==", "cpu": [ "x64" ], @@ -309,33 +300,21 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.49.0", "cpu": [ "x64" ], @@ -343,507 +322,386 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "win32" ], - "engines": { - "node": ">=18" - } + "peer": true }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], + "node_modules/@types/estree": { + "version": "1.0.8", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "peer": true + }, + "node_modules/@vscode/component-explorer": { + "version": "0.1.1-1", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-1.tgz", + "integrity": "sha512-yBSCsbkoSyd3dqgplM/QbhD5BhghkycLhNC0gWan24Z/uTHNik3QqkUCQW1B8DAa8NntwLQaNRiI2AN7glDb1w==", + "dev": true, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], + "node_modules/@vscode/component-explorer-vite-plugin": { + "version": "0.1.1-1", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-1.tgz", + "integrity": "sha512-VxNga/UgGxBBILlY984u/JqiIVNTjNAfj2YKKGGQ8xzpJZc+6GyPmTIIo+SzYsB0jXRomUyWCOqcFJw/YsKfqg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tinyglobby": "^0.2.0" + }, + "peerDependencies": { + "@vscode/component-explorer": "*", + "vite": "*" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], + "node_modules/@vscode/rollup-plugin-esm-url": { + "version": "1.0.1-1", + "resolved": "https://registry.npmjs.org/@vscode/rollup-plugin-esm-url/-/rollup-plugin-esm-url-1.0.1-1.tgz", + "integrity": "sha512-vNmIR3ZyiwACUi8qnXhKNukoXaFkOM9skiqVOVHNKJTBb7kJS+evtyadrBc/fMm1y303WQWBNA90E7fCCsE2Sw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "peerDependencies": { + "rollup": "^3.0.0 || ^4.0.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], + "node_modules/fdir": { + "version": "6.5.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">=18" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", - "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", - "cpu": [ - "arm" - ], + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "peer": true }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", - "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", - "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", - "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", - "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", - "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", - "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", - "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", - "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", - "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", - "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", - "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz", - "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz", - "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ - "riscv64" + "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz", - "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==", - "cpu": [ - "s390x" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz", - "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz", - "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz", - "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz", - "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz", - "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vscode/rollup-plugin-esm-url": { - "version": "1.0.1-0", - "resolved": "https://registry.npmjs.org/@vscode/rollup-plugin-esm-url/-/rollup-plugin-esm-url-1.0.1-0.tgz", - "integrity": "sha512-5k9c2ZK6xTTa2MGa0uD+f/a1cZV8SCQm9ZhorQDyBRvobIzOTg57LjOl3di9Z6zawPh7nDgbWp6g+GnSSpdCrg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "rollup": "^3.0.0 || ^4.0.0" - } - }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + ], "engines": { - "node": ">=18" + "node": ">= 12.0.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "bin": { + "loose-envify": "cli.js" } }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -861,15 +719,11 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -881,8 +735,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -908,12 +760,72 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", + "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-x64": "1.0.0-beta.53", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" + } + }, "node_modules/rollup": { "version": "4.49.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz", - "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -948,10 +860,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -960,8 +881,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -975,18 +894,28 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "name": "rolldown-vite", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.3.1.tgz", + "integrity": "sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", + "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.43.0", + "rolldown": "1.0.0-beta.53", "tinyglobby": "^0.2.15" }, "bin": { @@ -1003,9 +932,9 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -1018,13 +947,13 @@ "@types/node": { "optional": true }, - "jiti": { + "esbuild": { "optional": true }, - "less": { + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { diff --git a/build/vite/package.json b/build/vite/package.json index f65f359145ed1..5fb413ec97760 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,7 +9,14 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/rollup-plugin-esm-url": "^1.0.1-0", - "vite": "^7.1.11" + "@vscode/rollup-plugin-esm-url": "^1.0.1-1", + "vite": "npm:rolldown-vite@latest", + "@vscode/component-explorer": "0.1.1-1", + "@vscode/component-explorer-vite-plugin": "0.1.1-1" + }, + "overrides": { + "@vscode/component-explorer-vite-plugin": { + "vite": "$vite" + } } } diff --git a/build/vite/setup-dev.ts b/build/vite/setup-dev.ts index 8f1f997599c18..b0ef8682c9a4e 100644 --- a/build/vite/setup-dev.ts +++ b/build/vite/setup-dev.ts @@ -10,6 +10,7 @@ import { getSingletonServiceDescriptors, InstantiationType, registerSingleton } import { IWebWorkerService } from '../../src/vs/platform/webWorker/browser/webWorkerService.ts'; // eslint-disable-next-line local/code-no-standalone-editor import { StandaloneWebWorkerService } from '../../src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts'; +import './style.css'; enableHotReload(); diff --git a/build/vite/style.css b/build/vite/style.css index b9573061e51fa..f1ee3ca812f62 100644 --- a/build/vite/style.css +++ b/build/vite/style.css @@ -7,3 +7,9 @@ height: 400px; border: 1px solid black; } + +@font-face { + font-family: "codicon"; + font-display: block; + src: url("~@vscode/codicons/dist/codicon.ttf") format("truetype"); +} diff --git a/build/vite/vite.config.ts b/build/vite/vite.config.ts index 1ddb4f53adfd5..1d748f908f857 100644 --- a/build/vite/vite.config.ts +++ b/build/vite/vite.config.ts @@ -5,9 +5,11 @@ import { createLogger, defineConfig, Plugin } from 'vite'; import path, { join } from 'path'; -import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url'; +// import { componentExplorer } from 'D:/dev/hediet/js-component-explorer/packages/vite-plugin'; +import { componentExplorer } from '@vscode/component-explorer-vite-plugin'; import { statSync } from 'fs'; import { pathToFileURL } from 'url'; +import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url'; function injectBuiltinExtensionsPlugin(): Plugin { let builtinExtensionsCache: unknown[] | null = null; @@ -166,9 +168,20 @@ export default defineConfig({ plugins: [ rollupEsmUrlPlugin({}), injectBuiltinExtensionsPlugin(), - createHotClassSupport() + createHotClassSupport(), + componentExplorer({ + logLevel: 'verbose', + include: 'build/vite/**/*.fixture.ts', + }), ], customLogger: logger, + resolve: { + alias: { + // '@vscode/component-explorer': 'D:/dev/hediet/js-component-explorer/packages/explorer/src', + '@vscode/component-explorer': path.resolve(__dirname, 'node_modules/@vscode/component-explorer'), + '~@vscode/codicons': '/node_modules/@vscode/codicons', + } + }, esbuild: { tsconfigRaw: { compilerOptions: { From 46f41a817b8312e2c237277c8532ebceda21e152 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 11 Feb 2026 11:06:45 +0000 Subject: [PATCH 46/53] Adjust icon size in ActivitybarPart from 24 to 16 --- src/vs/workbench/browser/parts/activitybar/activitybarPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 6978ffacbf0bd..60ffabb3a631e 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -77,7 +77,7 @@ export class ActivitybarPart extends Part { viewContainersWorkspaceStateKey: ActivitybarPart.viewContainersWorkspaceStateKey, orientation: ActionsOrientation.VERTICAL, icon: true, - iconSize: 24, + iconSize: 16, activityHoverOptions: { position: () => this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT, }, From 6441536a79a3f2df1d78d28cddfb61b6a1b965f0 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:31:38 +0100 Subject: [PATCH 47/53] Chat context API feedback (#294537) --- .../api/common/extHostChatContext.ts | 24 +++++----- .../vscode.proposed.chatContextProvider.d.ts | 45 +++++++++++++++---- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 1e1a75fa152d1..f0c6509f8a5a8 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -50,7 +50,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext throw new Error('Workspace context provider not found'); } const provider = entry.provider as vscode.ChatWorkspaceContextProvider; - const result = (await provider.provideChatContext(token)) ?? []; + const result = (await provider.provideWorkspaceChatContext(token)) ?? (await provider.provideChatContext?.(token)) ?? []; return this._convertItems(handle, result); } @@ -63,7 +63,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext throw new Error('Explicit context provider not found'); } const provider = entry.provider as vscode.ChatExplicitContextProvider; - const result = (await provider.provideChatContext(token)) ?? []; + const result = (await provider.provideExplicitChatContext(token)) ?? (await provider.provideChatContext?.(token)) ?? []; return this._convertItems(handle, result); } @@ -77,7 +77,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!extItem) { throw new Error('Chat context item not found'); } - return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token); + return this._doResolve((provider.resolveExplicitChatContext ?? provider.resolveChatContext).bind(provider), context, extItem, token); } // Resource context provider methods @@ -89,7 +89,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext } const provider = entry.provider as vscode.ChatResourceContextProvider; - const result = await provider.provideChatContext({ resource: URI.revive(options.resource) }, token); + const result = (await provider.provideResourceChatContext({ resource: URI.revive(options.resource) }, token)) ?? (await provider.provideChatContext?.({ resource: URI.revive(options.resource) }, token)); if (!result) { return undefined; } @@ -109,7 +109,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext command: result.command ? { id: result.command.command } : undefined }; if (options.withValue && !item.value) { - const resolved = await provider.resolveChatContext(result, token); + const resolved = await (provider.resolveResourceChatContext ?? provider.resolveChatContext).bind(provider)(result, token); item.value = resolved?.value; item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip; } @@ -127,7 +127,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!extItem) { throw new Error('Chat context item not found'); } - return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token); + return this._doResolve((provider.resolveResourceChatContext ?? provider.resolveChatContext).bind(provider), context, extItem, token); } // Command execution @@ -209,7 +209,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (provider.provideWorkspaceChatContext) { const workspaceProvider: vscode.ChatWorkspaceContextProvider = { onDidChangeWorkspaceChatContext: provider.onDidChangeWorkspaceChatContext, - provideChatContext: (token) => provider.provideWorkspaceChatContext!(token) + provideWorkspaceChatContext: (token) => provider.provideWorkspaceChatContext!(token) }; disposables.push(this.registerChatWorkspaceContextProvider(id, workspaceProvider)); } @@ -217,8 +217,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext // Register explicit context provider if the provider supports it if (provider.provideChatContextExplicit) { const explicitProvider: vscode.ChatExplicitContextProvider = { - provideChatContext: (token) => provider.provideChatContextExplicit!(token), - resolveChatContext: provider.resolveChatContext + provideExplicitChatContext: (token) => provider.provideChatContextExplicit!(token), + resolveExplicitChatContext: provider.resolveChatContext ? (context, token) => provider.resolveChatContext!(context, token) : (context) => context }; @@ -228,8 +228,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext // Register resource context provider if the provider supports it and has a selector if (provider.provideChatContextForResource && selector) { const resourceProvider: vscode.ChatResourceContextProvider = { - provideChatContext: (options, token) => provider.provideChatContextForResource!(options, token), - resolveChatContext: provider.resolveChatContext + provideResourceChatContext: (options, token) => provider.provideChatContextForResource!(options, token), + resolveResourceChatContext: provider.resolveChatContext ? (context, token) => provider.resolveChatContext!(context, token) : (context) => context }; @@ -315,7 +315,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return; } const provideWorkspaceContext = async () => { - const workspaceContexts = await provider.provideChatContext(CancellationToken.None); + const workspaceContexts = await provider.provideWorkspaceChatContext(CancellationToken.None); const resolvedContexts = this._convertItems(handle, workspaceContexts ?? []); return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); }; diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index d9e6583c093eb..e7cd493ae50ad 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -24,7 +24,9 @@ declare module 'vscode' { export function registerChatWorkspaceContextProvider(id: string, provider: ChatWorkspaceContextProvider): Disposable; /** - * Register a chat explicit context provider. Explicit context items are shown as options when the user explicitly attaches context. + * Register a chat explicit context provider. Explicit context items are shown as options when the user explicitly attaches context use the "Attache Context" action in the chat input box. + * + * Explicit context providers should also be statically contributed in package.json using the `chatContext` contribution point. * * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. * @@ -109,7 +111,12 @@ declare module 'vscode' { * * @param token A cancellation token. */ - provideChatContext(token: CancellationToken): ProviderResult; + provideWorkspaceChatContext(token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + provideChatContext?(token: CancellationToken): ProviderResult; } export interface ChatExplicitContextProvider { @@ -121,7 +128,12 @@ declare module 'vscode' { * * @param token A cancellation token. */ - provideChatContext(token: CancellationToken): ProviderResult; + provideExplicitChatContext(token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + provideChatContext?(token: CancellationToken): ProviderResult; /** * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. @@ -129,7 +141,12 @@ declare module 'vscode' { * @param context The context item to resolve. * @param token A cancellation token. */ - resolveChatContext(context: T, token: CancellationToken): ProviderResult; + resolveExplicitChatContext(context: T, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + resolveChatContext?(context: T, token: CancellationToken): ProviderResult; } export interface ChatResourceContextProvider { @@ -144,7 +161,12 @@ declare module 'vscode' { * @param options Options include the resource for which to provide context. * @param token A cancellation token. */ - provideChatContext(options: { resource: Uri }, token: CancellationToken): ProviderResult; + provideResourceChatContext(options: { resource: Uri }, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + provideChatContext?(options: { resource: Uri }, token: CancellationToken): ProviderResult; /** * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. @@ -152,7 +174,12 @@ declare module 'vscode' { * @param context The context item to resolve. * @param token A cancellation token. */ - resolveChatContext(context: T, token: CancellationToken): ProviderResult; + resolveResourceChatContext(context: T, token: CancellationToken): ProviderResult; + + /** + * @deprecated + */ + resolveChatContext?(context: T, token: CancellationToken): ProviderResult; } /** @@ -168,19 +195,19 @@ declare module 'vscode' { /** * Provide a list of chat context items to be included as workspace context for all chat requests. - * @deprecated Use {@link ChatWorkspaceContextProvider.provideChatContext} instead. + * @deprecated Use {@link ChatWorkspaceContextProvider.provideWorkspaceChatContext} instead. */ provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; /** * Provide a list of chat context items that a user can choose from. - * @deprecated Use {@link ChatExplicitContextProvider.provideChatContext} instead. + * @deprecated Use {@link ChatExplicitContextProvider.provideExplicitChatContext} instead. */ provideChatContextExplicit?(token: CancellationToken): ProviderResult; /** * Given a particular resource, provide a chat context item for it. - * @deprecated Use {@link ChatResourceContextProvider.provideChatContext} instead. + * @deprecated Use {@link ChatResourceContextProvider.provideResourceChatContext} instead. */ provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult; From 341b0df09fc4a5f1fbd24b1b07054707a67ee652 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 11 Feb 2026 11:37:12 +0000 Subject: [PATCH 48/53] Update @vscode/codicons to version 0.0.45-6 in package.json and package-lock.json --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- .../browser/ui/codicons/codicon/codicon.ttf | Bin 125696 -> 125648 bytes 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c6806f2b4d98..47f013803d747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -2947,9 +2947,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-5", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-5.tgz", - "integrity": "sha512-tvRtMrVAe+CnlePF1Z3uhRfu0mLhAVgrSiY9zuEaGyXyBlN1/06bnpXno8XYb4O3ggOFx2phximqje4dhoLnkQ==", + "version": "0.0.45-6", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-6.tgz", + "integrity": "sha512-HjJmIxw6anUPk/yiQTyF60ERjARNfc/A11kKoiO7jg2bzNeaCexunu4oUo/W8lHGr/dvHxYcruM1V3ZoGxyFNQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/deviceid": { diff --git a/package.json b/package.json index e79cfc226569a..64d267e47af0f 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index e83d23193296b..48c97fba4c5fd 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-5", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-5.tgz", - "integrity": "sha512-tvRtMrVAe+CnlePF1Z3uhRfu0mLhAVgrSiY9zuEaGyXyBlN1/06bnpXno8XYb4O3ggOFx2phximqje4dhoLnkQ==", + "version": "0.0.45-6", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-6.tgz", + "integrity": "sha512-HjJmIxw6anUPk/yiQTyF60ERjARNfc/A11kKoiO7jg2bzNeaCexunu4oUo/W8lHGr/dvHxYcruM1V3ZoGxyFNQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index f738d5554fa26..4f591ed99068f 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-5", + "@vscode/codicons": "^0.0.45-6", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index a3098e1715c529ea8fa52ad57586c686ca2fd629..ff46f210b8ab96e49e12c17db2d6794ed6b5eecd 100644 GIT binary patch delta 493 zcmXAhKWGzS7{d~rO>SLkEA6Z8gZKN}cef8ebauR*w;i>!*!k8q zx);0Od->kA-rxRQKkOfVT-!yv8+*pyqk%YhGRzF`3_JH%i|eaJcyVhPqexH$4~a-e zNmN4%2vsfOxaAtK|3~06T6P^`F+ZqUDole4j$0r(X9gc0T|B delta 559 zcmXAlO=uHQ5Xawa-tK1K?!LF1x0|>LiIPZ)(L_n&D)>n$9*T#Gp2UL%58f<(Ts#<| z;vp4jK)i^hpm>m61fhYX+cviOXq(WBJ@)9ygGdg=le^6vW`_U#X8tgHKjoME^5S*v zcD}iQP<|P~$5!oT8Rw?G<=U%^c@H5toO|@->B5IBK`^*eYn;PhXXayI~1Zai<3_UOm^O}croHMQ02WINT) z-)^ou*FEfA?!D+o`w#m2+jP6Jb8KgCH@Ev`aAHs&y2E$FgZZWW%2FOaJ*$L}9}tH) za?yQMMTi%pu@u;rB&Q$=mc)R>Z>ilS483)nFP zjmt9E1jE4scc`u?aV|IpiAYj}Oc5qxSu>bMuq0uXSzMPwI7CTEHCZq=HGwbzha}yy znHoT(qA^30vB(HGwgCyQ`VMi1t)w8b2nmNtgo#4Z!YuAPt2_aQt;n*%DZyAYVx|-p z!8S1EqOPc>RwtB_x@M}1E^>+u8^o|=#tac-LJ=B&TSg Date: Wed, 11 Feb 2026 14:48:47 +0100 Subject: [PATCH 49/53] refactor: streamline NLS messages writing in finalizeNLS function --- build/next/nls-plugin.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index caae95cb434b1..56cb84fa33a06 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -134,19 +134,12 @@ export async function finalizeNLS( path.join(dir, 'nls.metadata.json'), JSON.stringify(nlsMetadataJson, null, '\t') ), + fs.promises.writeFile( + path.join(dir, 'nls.messages.js'), + `/*---------------------------------------------------------\n * Copyright (C) Microsoft Corporation. All rights reserved.\n *--------------------------------------------------------*/\nglobalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` + ), ])); - // Write nls.messages.js only to additional dirs (e.g., out-build) for CI NLS upload, - // not to the primary bundle output dir where it would be an extra file vs the old build - if (alsoWriteTo) { - await Promise.all(alsoWriteTo.map(dir => - fs.promises.writeFile( - path.join(dir, 'nls.messages.js'), - `/*---------------------------------------------------------\n * Copyright (C) Microsoft Corporation. All rights reserved.\n *--------------------------------------------------------*/\nglobalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(allMessages)};` - ) - )); - } - console.log(`[nls] Extracted ${allMessages.length} messages from ${moduleToKeys.size} modules`); return { indexMap, messageCount: allMessages.length }; From f4ca4716942d011fde0f3913cb14934c07aba9d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:48:54 -0800 Subject: [PATCH 50/53] Be explicit about tasks running shell commands Fixes #287073 --- src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index ef940211d69e7..edb27ecf95cb7 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -180,7 +180,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut private _showPrompt(notificationService: INotificationService, storageService: IStorageService, openerService: IOpenerService, configurationService: IConfigurationService, taskNames: string[], locations: Map): Promise { return new Promise(resolve => { notificationService.prompt(Severity.Info, nls.localize('tasks.run.allowAutomatic', - "This workspace has tasks ({0}) defined ({1}) that run automatically when you open this workspace. Do you want to allow automatic tasks to run in all trusted workspaces?", + "This workspace has tasks ({0}) defined ({1}) that run shell commands automatically when you open this workspace. Do you want to allow automatic tasks to run in all trusted workspaces?", taskNames.join(', '), Array.from(locations.keys()).join(', ') ), From e23d2464431a3f91901f5587c9e0e2613387ea50 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:02:51 -0800 Subject: [PATCH 51/53] Update src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts --- src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index edb27ecf95cb7..1e408be694c2a 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -180,7 +180,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut private _showPrompt(notificationService: INotificationService, storageService: IStorageService, openerService: IOpenerService, configurationService: IConfigurationService, taskNames: string[], locations: Map): Promise { return new Promise(resolve => { notificationService.prompt(Severity.Info, nls.localize('tasks.run.allowAutomatic', - "This workspace has tasks ({0}) defined ({1}) that run shell commands automatically when you open this workspace. Do you want to allow automatic tasks to run in all trusted workspaces?", + "This workspace has tasks ({0}) defined ({1}) that can launch processes automatically when you open this workspace. Do you want to allow automatic tasks to run in all trusted workspaces?", taskNames.join(', '), Array.from(locations.keys()).join(', ') ), From 0749dc9031db5b2d5af3e51667da8d69acd6fa75 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 11 Feb 2026 16:11:01 +0100 Subject: [PATCH 52/53] adds component explorer (#294548) updates component explorer --- build/vite/package-lock.json | 445 ++++++++++++++++++++++++++++++++--- build/vite/package.json | 4 +- build/vite/vite.config.ts | 3 - 3 files changed, 417 insertions(+), 35 deletions(-) diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index 7c67f659f0a54..4db5338149d1f 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,8 +8,8 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "0.1.1-1", - "@vscode/component-explorer-vite-plugin": "0.1.1-1", + "@vscode/component-explorer": "next", + "@vscode/component-explorer-vite-plugin": "next", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "vite": "npm:rolldown-vite@latest" } @@ -313,8 +313,370 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.49.0", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -339,14 +701,16 @@ }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-1", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-1.tgz", - "integrity": "sha512-yBSCsbkoSyd3dqgplM/QbhD5BhghkycLhNC0gWan24Z/uTHNik3QqkUCQW1B8DAa8NntwLQaNRiI2AN7glDb1w==", + "version": "0.1.1-2", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-2.tgz", + "integrity": "sha512-2VMoXLnDBk+hKrhw+iGUsEjnCd1YiiZqe+1LdQIKdk16zqYRtJ5iO6yDxZ4cKy3Wphd+qLDUWmZSULNtKioMrQ==", "dev": true, "peerDependencies": { "react": "^18.2.0", @@ -354,9 +718,9 @@ } }, "node_modules/@vscode/component-explorer-vite-plugin": { - "version": "0.1.1-1", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-1.tgz", - "integrity": "sha512-VxNga/UgGxBBILlY984u/JqiIVNTjNAfj2YKKGGQ8xzpJZc+6GyPmTIIo+SzYsB0jXRomUyWCOqcFJw/YsKfqg==", + "version": "0.1.1-2", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-2.tgz", + "integrity": "sha512-iYSp8shDZEJJrjMWGneWyjFbFyED5Og74c9h5XBmVPZBDN4INfOTmPlC+HYTv/CL5+NFxpl91CdtacCmqz2EXw==", "dev": true, "dependencies": { "tinyglobby": "^0.2.0" @@ -388,6 +752,8 @@ }, "node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -702,6 +1068,8 @@ }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -719,11 +1087,15 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -735,6 +1107,8 @@ }, "node_modules/postcss": { "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -822,7 +1196,9 @@ } }, "node_modules/rollup": { - "version": "4.49.0", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "peer": true, @@ -837,26 +1213,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.49.0", - "@rollup/rollup-android-arm64": "4.49.0", - "@rollup/rollup-darwin-arm64": "4.49.0", - "@rollup/rollup-darwin-x64": "4.49.0", - "@rollup/rollup-freebsd-arm64": "4.49.0", - "@rollup/rollup-freebsd-x64": "4.49.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", - "@rollup/rollup-linux-arm-musleabihf": "4.49.0", - "@rollup/rollup-linux-arm64-gnu": "4.49.0", - "@rollup/rollup-linux-arm64-musl": "4.49.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", - "@rollup/rollup-linux-ppc64-gnu": "4.49.0", - "@rollup/rollup-linux-riscv64-gnu": "4.49.0", - "@rollup/rollup-linux-riscv64-musl": "4.49.0", - "@rollup/rollup-linux-s390x-gnu": "4.49.0", - "@rollup/rollup-linux-x64-gnu": "4.49.0", - "@rollup/rollup-linux-x64-musl": "4.49.0", - "@rollup/rollup-win32-arm64-msvc": "4.49.0", - "@rollup/rollup-win32-ia32-msvc": "4.49.0", - "@rollup/rollup-win32-x64-msvc": "4.49.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -873,6 +1254,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -881,6 +1264,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/build/vite/package.json b/build/vite/package.json index 5fb413ec97760..9800e54af40f1 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -11,8 +11,8 @@ "devDependencies": { "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "vite": "npm:rolldown-vite@latest", - "@vscode/component-explorer": "0.1.1-1", - "@vscode/component-explorer-vite-plugin": "0.1.1-1" + "@vscode/component-explorer": "next", + "@vscode/component-explorer-vite-plugin": "next" }, "overrides": { "@vscode/component-explorer-vite-plugin": { diff --git a/build/vite/vite.config.ts b/build/vite/vite.config.ts index 1d748f908f857..6cdde88076a49 100644 --- a/build/vite/vite.config.ts +++ b/build/vite/vite.config.ts @@ -5,7 +5,6 @@ import { createLogger, defineConfig, Plugin } from 'vite'; import path, { join } from 'path'; -// import { componentExplorer } from 'D:/dev/hediet/js-component-explorer/packages/vite-plugin'; import { componentExplorer } from '@vscode/component-explorer-vite-plugin'; import { statSync } from 'fs'; import { pathToFileURL } from 'url'; @@ -177,8 +176,6 @@ export default defineConfig({ customLogger: logger, resolve: { alias: { - // '@vscode/component-explorer': 'D:/dev/hediet/js-component-explorer/packages/explorer/src', - '@vscode/component-explorer': path.resolve(__dirname, 'node_modules/@vscode/component-explorer'), '~@vscode/codicons': '/node_modules/@vscode/codicons', } }, From a7cc092d0a4adc41e931a29d58a0c0b0a0523752 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 11 Feb 2026 16:53:04 +0100 Subject: [PATCH 53/53] Adding lineHeightsAdded array to onLinesInserted (#289634) * adding lineHeightsAdded array * polish * fixing the test * renaming to lineHeightAdded --------- Co-authored-by: Alexandru Dima --- src/vs/editor/common/model.ts | 7 +++ src/vs/editor/common/model/textModel.ts | 18 +++++- src/vs/editor/common/textModelEvents.ts | 32 ++++++++-- .../editor/common/viewLayout/lineHeights.ts | 58 +++++++++++++++---- .../editor/common/viewLayout/linesLayout.ts | 11 ++-- src/vs/editor/common/viewLayout/viewLayout.ts | 10 ++-- .../editor/common/viewModel/viewModelImpl.ts | 30 +++++----- src/vs/editor/test/common/model/model.test.ts | 14 ++--- .../common/viewLayout/lineHeights.test.ts | 4 +- .../common/viewLayout/linesLayout.test.ts | 4 +- src/vs/monaco.d.ts | 6 ++ 11 files changed, 141 insertions(+), 53 deletions(-) diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index f39515c3348a3..5195e0b635374 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1182,6 +1182,13 @@ export interface ITextModel { */ getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param range The range to search in + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorationsInRange(range: Range, ownerId?: number): IModelDecoration[]; + /** * @internal */ diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ebd3eccec5109..bb3a86c98575b 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1563,6 +1563,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati rawContentChanges.push( new ModelRawLineChanged( editLineNumber, + currentEditLineNumber, this.getLineContent(currentEditLineNumber), decorationsInCurrentLine )); @@ -1593,7 +1594,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati rawContentChanges.push( new ModelRawLinesInserted( spliceLineNumber + 1, - startLineNumber + insertingLinesCnt, + fromLineNumber, + cnt, newLines, injectedTexts ) @@ -1653,7 +1655,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (affectedInjectedTextLines && affectedInjectedTextLines.size > 0) { const affectedLines = Array.from(affectedInjectedTextLines); - const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); + const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); this._onDidChangeContentOrInjectedText(new ModelInjectedTextChangedEvent(lineChangeEvents)); } this._fireOnDidChangeLineHeight(affectedLineHeights); @@ -1865,6 +1867,12 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return decs; } + public getCustomLineHeightsDecorationsInRange(range: Range, ownerId: number = 0): model.IModelDecoration[] { + const decs = this._decorationsTree.getCustomLineHeightsInInterval(this, this.getOffsetAt(range.getStartPosition()), this.getOffsetAt(range.getEndPosition()), ownerId); + pushMany(decs, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId)); + return decs; + } + private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { const startOffset = this._buffer.getOffsetAt(lineNumber, 1); const endOffset = startOffset + this._buffer.getLineLength(lineNumber); @@ -2249,6 +2257,12 @@ class DecorationsTrees { return this._ensureNodesHaveRanges(host, result).filter((i) => typeof i.options.lineHeight === 'number'); } + public getCustomLineHeightsInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number): model.IModelDecoration[] { + const versionId = host.getVersionId(); + const result = this._intervalSearch(start, end, filterOwnerId, false, false, versionId, false); + return this._ensureNodesHaveRanges(host, result).filter((i) => typeof i.options.lineHeight === 'number'); + } + public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, filterFontDecorations: boolean, overviewRulerOnly: boolean, onlyMarginDecorations: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._search(filterOwnerId, filterOutValidation, filterFontDecorations, overviewRulerOnly, versionId, onlyMarginDecorations); diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index b25c00aae8a70..4fa24afd2c1b0 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -308,9 +308,13 @@ export class LineInjectedText { export class ModelRawLineChanged { public readonly changeType = RawContentChangedType.LineChanged; /** - * The line that has changed. + * The line number that has changed (before the change was applied). */ public readonly lineNumber: number; + /** + * The new line number the old one is mapped to (after the change was applied). + */ + public readonly lineNumberPostEdit: number; /** * The new value of the line. */ @@ -320,8 +324,9 @@ export class ModelRawLineChanged { */ public readonly injectedText: LineInjectedText[] | null; - constructor(lineNumber: number, detail: string, injectedText: LineInjectedText[] | null) { + constructor(lineNumber: number, lineNumberPostEdit: number, detail: string, injectedText: LineInjectedText[] | null) { this.lineNumber = lineNumber; + this.lineNumberPostEdit = lineNumberPostEdit; this.detail = detail; this.injectedText = injectedText; } @@ -409,10 +414,26 @@ export class ModelRawLinesInserted { * Before what line did the insertion begin */ public readonly fromLineNumber: number; + /** + * The actual start line number in the updated buffer where the newly inserted content can be found. + */ + public readonly fromLineNumberPostEdit: number; + /** + * The count of inserted lines. + */ + public readonly count: number; /** * `toLineNumber` - `fromLineNumber` + 1 denotes the number of lines that were inserted */ - public readonly toLineNumber: number; + public get toLineNumber(): number { + return this.fromLineNumber + this.count - 1; + } + /** + * The actual end line number of the insertion in the updated buffer. + */ + public get toLineNumberPostEdit(): number { + return this.fromLineNumberPostEdit + this.count - 1; + } /** * The text that was inserted */ @@ -422,10 +443,11 @@ export class ModelRawLinesInserted { */ public readonly injectedTexts: (LineInjectedText[] | null)[]; - constructor(fromLineNumber: number, toLineNumber: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) { + constructor(fromLineNumber: number, fromLineNumberPostEdit: number, count: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) { this.injectedTexts = injectedTexts; this.fromLineNumber = fromLineNumber; - this.toLineNumber = toLineNumber; + this.fromLineNumberPostEdit = fromLineNumberPostEdit; + this.count = count; this.detail = detail; } } diff --git a/src/vs/editor/common/viewLayout/lineHeights.ts b/src/vs/editor/common/viewLayout/lineHeights.ts index 47e402e075b95..bceebf4aad990 100644 --- a/src/vs/editor/common/viewLayout/lineHeights.ts +++ b/src/vs/editor/common/viewLayout/lineHeights.ts @@ -5,6 +5,10 @@ import { binarySearch2 } from '../../../base/common/arrays.js'; import { intersection } from '../../../base/common/collections.js'; +import { IEditorConfiguration } from '../config/editorConfiguration.js'; +import { EditorOption } from '../config/editorOptions.js'; +import { ICoordinatesConverter } from '../coordinatesConverter.js'; +import { IModelDecoration } from '../model.js'; export class CustomLine { @@ -62,7 +66,7 @@ export class LineHeightsManager { private _defaultLineHeight: number; private _hasPending: boolean = false; - constructor(defaultLineHeight: number, customLineHeightData: ICustomLineHeightData[]) { + constructor(defaultLineHeight: number, customLineHeightData: CustomLineHeightData[]) { this._defaultLineHeight = defaultLineHeight; if (customLineHeightData.length > 0) { for (const data of customLineHeightData) { @@ -227,7 +231,7 @@ export class LineHeightsManager { } } - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { const insertCount = toLineNumber - fromLineNumber + 1; const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber); let startIndexOfInsertion: number; @@ -243,7 +247,22 @@ export class LineHeightsManager { } else { startIndexOfInsertion = -(candidateStartIndexOfInsertion + 1); } - const toReAdd: ICustomLineHeightData[] = []; + const maxLineHeightPerLine = new Map(); + for (const lineHeightAdded of lineHeightsAdded) { + for (let lineNumber = lineHeightAdded.startLineNumber; lineNumber <= lineHeightAdded.endLineNumber; lineNumber++) { + if (lineNumber >= fromLineNumber && lineNumber <= toLineNumber) { + const currentMax = maxLineHeightPerLine.get(lineNumber) ?? this._defaultLineHeight; + maxLineHeightPerLine.set(lineNumber, Math.max(currentMax, lineHeightAdded.lineHeight)); + } + } + this.insertOrChangeCustomLineHeight( + lineHeightAdded.decorationId, + lineHeightAdded.startLineNumber, + lineHeightAdded.endLineNumber, + lineHeightAdded.lineHeight + ); + } + const toReAdd: CustomLineHeightData[] = []; const decorationsImmediatelyAfter = new Set(); for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { if (this._orderedCustomLines[i].lineNumber === fromLineNumber) { @@ -257,9 +276,12 @@ export class LineHeightsManager { } } const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter); + const specialHeightToAdd = Array.from(maxLineHeightPerLine.values()).reduce((acc, height) => acc + height, 0); + const defaultHeightToAdd = (insertCount - maxLineHeightPerLine.size) * this._defaultLineHeight; + const prefixSumToAdd = specialHeightToAdd + defaultHeightToAdd; for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) { this._orderedCustomLines[i].lineNumber += insertCount; - this._orderedCustomLines[i].prefixSum += this._defaultLineHeight * insertCount; + this._orderedCustomLines[i].prefixSum += prefixSumToAdd; } if (decorationsWithGaps.size > 0) { @@ -281,8 +303,8 @@ export class LineHeightsManager { for (const dec of toReAdd) { this.insertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight); } - this.commit(); } + this.commit(); } public commit(): void { @@ -363,11 +385,27 @@ export class LineHeightsManager { } } -export interface ICustomLineHeightData { - readonly decorationId: string; - readonly startLineNumber: number; - readonly endLineNumber: number; - readonly lineHeight: number; +export class CustomLineHeightData { + + constructor( + readonly decorationId: string, + readonly startLineNumber: number, + readonly endLineNumber: number, + readonly lineHeight: number + ) { } + + public static fromDecorations(decorations: IModelDecoration[], coordinatesConverter: ICoordinatesConverter, configuration: IEditorConfiguration): CustomLineHeightData[] { + const defaultLineHeight = configuration.options.get(EditorOption.lineHeight); + return decorations.map((d) => { + const viewRange = coordinatesConverter.convertModelRangeToViewRange(d.range); + return new CustomLineHeightData( + d.id, + viewRange.startLineNumber, + viewRange.endLineNumber, + d.options.lineHeight ? d.options.lineHeight * defaultLineHeight : 0 + ); + }); + } } class ArrayMap { diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index b773e5dd4cb1f..376b0f2c7c9a8 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -5,7 +5,7 @@ import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js'; import * as strings from '../../../base/common/strings.js'; -import { ICustomLineHeightData, LineHeightsManager } from './lineHeights.js'; +import { CustomLineHeightData, LineHeightsManager } from './lineHeights.js'; interface IPendingChange { id: string; newAfterLineNumber: number; newHeight: number } interface IPendingRemove { id: string } @@ -95,7 +95,7 @@ export class LinesLayout { private _paddingBottom: number; private _lineHeightsManager: LineHeightsManager; - constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: ICustomLineHeightData[]) { + constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: CustomLineHeightData[]) { this._instanceId = strings.singleLetterHash(++LinesLayout.INSTANCE_COUNT); this._pendingChanges = new PendingChanges(); this._lastWhitespaceId = 0; @@ -155,7 +155,7 @@ export class LinesLayout { * * @param lineCount New number of lines. */ - public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { + public onFlushed(lineCount: number, customLineHeightData: CustomLineHeightData[]): void { this._lineCount = lineCount; this._lineHeightsManager = new LineHeightsManager(this._lineHeightsManager.defaultLineHeight, customLineHeightData); } @@ -353,8 +353,9 @@ export class LinesLayout { * * @param fromLineNumber The line number at which the insertion started, inclusive * @param toLineNumber The line number at which the insertion ended, inclusive. + * @param lineHeightsAdded The custom line height data for the inserted lines. */ - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { + public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { fromLineNumber = fromLineNumber | 0; toLineNumber = toLineNumber | 0; @@ -366,7 +367,7 @@ export class LinesLayout { this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1); } } - this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber); + this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber, lineHeightsAdded); } /** diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 10130ea8dcb08..048dc241eae7d 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -12,7 +12,7 @@ import { IEditorConfiguration } from '../config/editorConfiguration.js'; import { LinesLayout } from './linesLayout.js'; import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js'; import { ContentSizeChangedEvent } from '../viewModelEventDispatcher.js'; -import { ICustomLineHeightData } from './lineHeights.js'; +import { CustomLineHeightData } from './lineHeights.js'; const SMOOTH_SCROLLING_TIME = 125; @@ -164,7 +164,7 @@ export class ViewLayout extends Disposable implements IViewLayout { public readonly onDidScroll: Event; public readonly onDidContentSizeChange: Event; - constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: ICustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: CustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); this._configuration = configuration; @@ -237,14 +237,14 @@ export class ViewLayout extends Disposable implements IViewLayout { this._configureSmoothScrollDuration(); } } - public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void { + public onFlushed(lineCount: number, customLineHeightData: CustomLineHeightData[]): void { this._linesLayout.onFlushed(lineCount, customLineHeightData); } public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void { this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber); } - public onLinesInserted(fromLineNumber: number, toLineNumber: number): void { - this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber); + public onLinesInserted(fromLineNumber: number, toLineNumber: number, lineHeightsAdded: CustomLineHeightData[]): void { + this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber, lineHeightsAdded); } // ---- end view event handlers diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 954072212bce0..f3f33283bfc11 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -41,7 +41,7 @@ import { FocusChangedEvent, HiddenAreasChangedEvent, ModelContentChangedEvent, M import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from './viewModelLines.js'; import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GlyphMarginLanesModel } from './glyphLanesModel.js'; -import { ICustomLineHeightData } from '../viewLayout/lineHeights.js'; +import { CustomLineHeightData } from '../viewLayout/lineHeights.js'; import { TextModelEditSource } from '../textModelEditSource.js'; import { InlineDecoration } from './inlineDecorations.js'; import { ICoordinatesConverter } from '../coordinatesConverter.js'; @@ -196,23 +196,23 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.removeViewEventHandler(eventHandler); } - private _getCustomLineHeights(): ICustomLineHeightData[] { + private _getCustomLineHeights(): CustomLineHeightData[] { const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); if (!allowVariableLineHeights) { return []; } - const defaultLineHeight = this._configuration.options.get(EditorOption.lineHeight); const decorations = this.model.getCustomLineHeightsDecorations(this._editorId); - return decorations.map((d) => { - const lineNumber = d.range.startLineNumber; - const viewRange = this.coordinatesConverter.convertModelRangeToViewRange(new Range(lineNumber, 1, lineNumber, this.model.getLineMaxColumn(lineNumber))); - return { - decorationId: d.id, - startLineNumber: viewRange.startLineNumber, - endLineNumber: viewRange.endLineNumber, - lineHeight: d.options.lineHeight ? d.options.lineHeight * defaultLineHeight : 0 - }; - }); + return CustomLineHeightData.fromDecorations(decorations, this.coordinatesConverter, this._configuration); + } + + private _getCustomLineHeightsForLines(fromLineNumber: number, toLineNumber: number): CustomLineHeightData[] { + const allowVariableLineHeights = this._configuration.options.get(EditorOption.allowVariableLineHeights); + if (!allowVariableLineHeights) { + return []; + } + const modelRange = new Range(fromLineNumber, 1, toLineNumber, this.model.getLineMaxColumn(toLineNumber)); + const decorations = this.model.getCustomLineHeightsDecorationsInRange(modelRange, this._editorId); + return CustomLineHeightData.fromDecorations(decorations, this.coordinatesConverter, this._configuration); } private _updateConfigurationViewLineCountNow(): void { @@ -379,7 +379,7 @@ export class ViewModel extends Disposable implements IViewModel { const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.fromLineNumberPostEdit, change.toLineNumberPostEdit)); } hadOtherModelChange = true; break; @@ -394,7 +394,7 @@ export class ViewModel extends Disposable implements IViewModel { } if (linesInsertedEvent) { eventsCollector.emitViewEvent(linesInsertedEvent); - this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); + this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber, this._getCustomLineHeightsForLines(change.lineNumberPostEdit, change.lineNumberPostEdit)); } if (linesDeletedEvent) { eventsCollector.emitViewEvent(linesDeletedEvent); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 9113fbdff2d84..72140c2636000 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -130,7 +130,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'foo My First Line', null) + new ModelRawLineChanged(1, 1, 'foo My First Line', null) ], 2, false, @@ -144,8 +144,8 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My new line', null), - new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null]), + new ModelRawLineChanged(1, 1, 'My new line', null), + new ModelRawLinesInserted(2, 2, 1, ['No longer First Line'], [null]), ], 2, false, @@ -216,7 +216,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'y First Line', null), + new ModelRawLineChanged(1, 1, 'y First Line', null), ], 2, false, @@ -230,7 +230,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, '', null), + new ModelRawLineChanged(1, 1, '', null), ], 2, false, @@ -244,7 +244,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My Second Line', null), + new ModelRawLineChanged(1, 1, 'My Second Line', null), new ModelRawLinesDeleted(2, 2), ], 2, @@ -259,7 +259,7 @@ suite('Editor Model - Model', () => { }); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My Third Line', null), + new ModelRawLineChanged(1, 1, 'My Third Line', null), new ModelRawLinesDeleted(2, 3), ], 2, diff --git a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts index 191536462752c..c94726913baa3 100644 --- a/src/vs/editor/test/common/viewLayout/lineHeights.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineHeights.test.ts @@ -195,7 +195,7 @@ suite('Editor ViewLayout - LineHeightsManager', () => { manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); manager.commit(); - manager.onLinesInserted(3, 4); // Insert 2 lines at line 3 + manager.onLinesInserted(3, 4, []); // Insert 2 lines at line 3 assert.strictEqual(manager.heightForLineNumber(5), 10); assert.strictEqual(manager.heightForLineNumber(6), 10); @@ -209,7 +209,7 @@ suite('Editor ViewLayout - LineHeightsManager', () => { manager.insertOrChangeCustomLineHeight('dec1', 5, 7, 20); manager.commit(); - manager.onLinesInserted(6, 7); // Insert 2 lines at line 6 + manager.onLinesInserted(6, 7, []); // Insert 2 lines at line 6 assert.strictEqual(manager.heightForLineNumber(5), 20); assert.strictEqual(manager.heightForLineNumber(6), 20); diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index 7bf20a78d8457..ceb624ac2740e 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -208,7 +208,7 @@ suite('Editor ViewLayout - LinesLayout', () => { // Insert two lines at the beginning // 10 lines // whitespace: - a(6,10) - linesLayout.onLinesInserted(1, 2); + linesLayout.onLinesInserted(1, 2, []); assert.strictEqual(linesLayout.getLinesTotalHeight(), 20); assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(1), 0); assert.strictEqual(linesLayout.getVerticalOffsetForLineNumber(2), 1); @@ -909,7 +909,7 @@ suite('Editor ViewLayout - LinesLayout', () => { assert.strictEqual(linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(5), 50); // Insert a line before line 1 - linesLayout.onLinesInserted(1, 1); + linesLayout.onLinesInserted(1, 1, []); // whitespaces: d(3, 30), c(4, 20) assert.strictEqual(linesLayout.getWhitespacesCount(), 2); assert.strictEqual(linesLayout.getAfterLineNumberForWhitespaceIndex(0), 3); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index be805ae83dfbc..80497d1a193b3 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2322,6 +2322,12 @@ declare namespace monaco.editor { * @param ownerId If set, it will ignore decorations belonging to other owners. */ getCustomLineHeightsDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain custom line heights. + * @param range The range to search in + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getCustomLineHeightsDecorationsInRange(range: Range, ownerId?: number): IModelDecoration[]; /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */