Fix standalone preload source-map publication (#336204)

build: fix standalone preload source-map publication

Honor the configured CDN source-map base for all standalone Electron preloads and embed their original TypeScript sources. Preserve unbundled CommonJS output, local map links, development behavior, and checksum ordering, with focused publication and mapping regressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dmitriy Vasyura
2026-09-15 19:33:22 +00:00
committed by GitHub
co-authored by Copilot
parent 28a499366a
commit 29d5c1ecf1
4 changed files with 325 additions and 46 deletions
@@ -38,6 +38,7 @@ grep -l "serverLicense" out-vscode-reh-web-test/vs/code/browser/workbench/workbe
- **[build-fast.ts](../../build/next/build-fast.ts)** - Git change discovery, persistent state, lane planning, and orchestration
- **[transpile.ts](../../build/next/transpile.ts)** - Shared full/watch/incremental transpile and copy operations
- **[resources.ts](../../build/next/resources.ts)** - Curated production resource selection and copied JavaScript minification
- **[standalone.ts](../../build/next/standalone.ts)** - Unbundled desktop Electron preloads and their source-map publication
- **[nls-plugin.ts](nls-plugin.ts)** - NLS (localization) esbuild plugin
- **[private-to-property.ts](../../build/next/private-to-property.ts)** - Native private to property transformation
- **[svg.ts](../../build/next/svg.ts)** - Production-only SVG finishing, independent of the legacy gulp infrastructure
@@ -190,6 +191,23 @@ Focused regression tests (no workbench compilation required):
node --test build/next/test/resources.test.ts build/next/test/transpile.test.ts build/next/test/source-map-url.test.ts build/next/test/svg.test.ts
```
### Standalone Electron Preload Source Maps
[standalone.ts](../../build/next/standalone.ts) compiles all three desktop preloads: [preload.ts](../../src/vs/base/parts/sandbox/electron-browser/preload.ts), [preload-aux.ts](../../src/vs/base/parts/sandbox/electron-browser/preload-aux.ts), and [preload-browserView.ts](../../src/vs/platform/browserView/electron-browser/preload-browserView.ts). These special-context scripts retain `bundle: false`, `format: 'cjs'`, the existing target and copyright banner. They must not use the normal ESM bundle options or receive NLS/private-field post-processing.
- Both minified and non-minified bundle outputs embed the original TypeScript in `sourcesContent`. Finalization uses the existing [rewriteSourceMappingURL()](../../build/next/source-map-url.ts), including platform-independent URL separators, before writing JavaScript. Only the trailing map comment changes; executable bytes and esbuild's mappings are preserved.
- With an omitted or empty `--source-map-base-url`, the sibling `*.js.map` reference remains local. Development transpile/watch/build-fast paths remain separate and retain their inline maps without embedded sources.
- `core-ci` supplies `https://main.vscode-cdn.net/sourcemaps/<commit>/core`. [upload-sourcemaps.ts](../../build/azure-pipelines/upload-sourcemaps.ts) uploads each map under `sourcemaps/<commit>/core/<output-relative-path>.map`. No extra `out/` or `src/` belongs in the JavaScript's CDN URL.
- The bundle command awaits all standalone writes before downstream packaging. CI packaging strips local JS/CSS maps and computes the existing `preload.js` integrity checksum from final JavaScript bytes; do not move URL rewriting after checksum calculation.
Focused tests compile the real three entrypoints into temporary outputs and check CDN/local modes, exact original source content and identity, representative line/column mappings, unchanged CommonJS output, non-desktop omission, development maps, and the upload/map-stripping/checksum assumptions. The packaging check exercises the real file streams and filter, not a complete Electron distribution or a manual debugger session.
```bash
node --test build/next/test/standalone.test.ts build/next/test/source-map-url.test.ts
```
This repairs production debugging/source publication, not startup performance.
---
## Testing the Fix
+2 -46
View File
@@ -25,6 +25,7 @@ import { copyFile, mapWithConcurrency, MAX_CONCURRENT_FILE_OPERATIONS, transpile
import { copyResources, type BuildTarget } from './resources.ts';
import { optimizeSvgFiles } from './svg.ts';
import { getBundleOptions } from './bundle.ts';
import { compileStandaloneFiles } from './standalone.ts';
const globAsync = promisify(glob);
@@ -295,51 +296,6 @@ function readISODate(outDir: string): string {
}
}
/**
* 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 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<void> {
// 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(desktopStandaloneFiles.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: 'linked',
sourcesContent: false,
minify: doMinify,
banner: { js: banner },
logLevel: 'warning',
});
}));
console.log(`[standalone] Done`);
}
/**
* Copy ALL non-TypeScript files from src/ to the output directory.
* This matches the old gulp build behavior where `gulp.src('src/**')` streams
@@ -755,7 +711,7 @@ async function bundle(outDir: string, doMinify: boolean, doNls: boolean, doMangl
await copyResources(path.join(REPO_ROOT, SRC_DIR), outDirPath, target, doMinify, sourceMapBaseUrl);
// Compile standalone TypeScript files (like Electron preload scripts) that cannot be bundled
await compileStandaloneFiles(outDir, doMinify, target);
await compileStandaloneFiles(path.join(REPO_ROOT, SRC_DIR), outDirPath, target, doMinify, sourceMapBaseUrl);
if (allEntryPoints.includes(sessionsWebEntryPoint)) {
await bundleDevTunnelsWeb({
+60
View File
@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* 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 type { BuildTarget } from './resources.ts';
import { rewriteSourceMappingURL } from './source-map-url.ts';
/**
* These run in special contexts (Electron preload) and must not be bundled.
*/
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',
];
export async function compileStandaloneFiles(srcDir: string, outDir: string, target: BuildTarget, minify: boolean, sourceMapBaseUrl?: string): Promise<void> {
if (target !== 'desktop') {
return;
}
console.log(`[standalone] Compiling ${desktopStandaloneFiles.length} standalone files...`);
const banner = `/*!--------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/`;
await Promise.all(desktopStandaloneFiles.map(async file => {
const entryPath = path.resolve(srcDir, file);
const outPath = path.resolve(outDir, file.replace(/\.ts$/, '.js'));
const result = await esbuild.build({
entryPoints: [entryPath],
outfile: outPath,
bundle: false,
format: 'cjs',
platform: 'node',
target: ['es2024'],
sourcemap: 'linked',
sourcesContent: true,
minify,
banner: { js: banner },
write: false,
logLevel: 'warning',
});
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
for (const output of result.outputFiles) {
await fs.promises.writeFile(output.path, output.path === outPath
? rewriteSourceMappingURL(output.text, path.relative(outDir, output.path), sourceMapBaseUrl?.replace(/\/$/, ''))
: output.contents);
}
}));
console.log(`[standalone] Done`);
}
+245
View File
@@ -0,0 +1,245 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import * as crypto from 'crypto';
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { suite, test, type TestContext } from 'node:test';
import { Writable } from 'stream';
import { pipeline } from 'stream/promises';
import * as vm from 'vm';
import { SourceMapConsumer, type RawSourceMap } from 'source-map';
import type Vinyl from 'vinyl';
import vfs from 'vinyl-fs';
import { filter, gulp } from '../../lib/gulp/facade.ts';
import { compileStandaloneFiles } from '../standalone.ts';
import { transpileFile } from '../transpile.ts';
const repoRoot = path.resolve(import.meta.dirname, '..', '..', '..');
const srcDir = path.join(repoRoot, 'src');
const sourceMapRoot = 'https://example.test/sourcemaps/commit';
const sourceMapBaseUrl = `${sourceMapRoot}/core`;
const preloads = [
{
file: 'vs/base/parts/sandbox/electron-browser/preload.ts',
markers: ['startsWith(', 'postMessage(', 'exposeInMainWorld('],
},
{
file: 'vs/base/parts/sandbox/electron-browser/preload-aux.ts',
markers: ['startsWith(', 'setZoomLevel(', 'exposeInMainWorld('],
},
{
file: 'vs/platform/browserView/electron-browser/preload-browserView.ts',
markers: ['isTrusted', 'exposeInMainWorld(', 'getBoundingClientRect(', '_onPicked('],
},
];
const javascriptFiles = preloads.map(({ file }) => file.replace(/\.ts$/, '.js')).sort();
suite('standalone preloads', () => {
for (const minify of [false, true]) {
for (const { mode, baseUrl } of [
{ mode: 'local', baseUrl: undefined },
{ mode: 'empty CDN base', baseUrl: '' },
{ mode: 'CDN', baseUrl: sourceMapBaseUrl },
{ mode: 'CDN with trailing slash', baseUrl: `${sourceMapBaseUrl}/` },
]) {
test(`links original sources for all preloads (minify: ${minify}, ${mode})`, async t => {
const outDir = path.join(await temporaryDirectory(t), 'out');
await compileStandaloneFiles(srcDir, outDir, 'desktop', minify, baseUrl);
assert.deepStrictEqual(await outputFiles(outDir), javascriptFiles.flatMap(file => [file, `${file}.map`]).sort());
const results = await Promise.all(preloads.map(async ({ file, markers }) => {
const entryPath = path.join(srcDir, file);
const outPath = path.join(outDir, file.replace(/\.ts$/, '.js'));
const [source, output, mapText, baseline] = await Promise.all([
fs.promises.readFile(entryPath, 'utf8'),
fs.promises.readFile(outPath, 'utf8'),
fs.promises.readFile(`${outPath}.map`, 'utf8'),
compileBaseline(entryPath, outPath, minify),
]);
const map: RawSourceMap = JSON.parse(mapText);
const consumer = new SourceMapConsumer(map);
const sourcePath = path.relative(path.dirname(outPath), entryPath).replaceAll('\\', '/');
const expectedURL = baseUrl
? `${sourceMapBaseUrl}/${file.replace(/\.ts$/, '.js.map')}`
: `${path.basename(outPath)}.map`;
const expectedComment = `//# sourceMappingURL=${expectedURL}`;
new vm.Script(output, { filename: outPath });
return {
actual: {
file,
comments: output.match(/^\/\/# sourceMappingURL=.+$/gm),
sources: map.sources,
embedsOriginalSource: map.sourcesContent?.length === 1 && map.sourcesContent[0] === source,
unchangedJavaScript: output === baseline.code.replace(/^\/\/# sourceMappingURL=.+$/m, expectedComment),
unchangedMappings: map.mappings === baseline.map.mappings,
header: consumer.originalPositionFor({ line: 1, column: 0 }),
positions: markers.map(marker => {
const { source, line, column } = consumer.originalPositionFor(positionOf(output, marker));
return { marker, source, line, column };
}),
},
expected: {
file,
comments: [expectedComment],
sources: [sourcePath],
embedsOriginalSource: true,
unchangedJavaScript: true,
unchangedMappings: true,
header: { source: null, line: null, column: null, name: null },
positions: markers.map(marker => ({ marker, source: sourcePath, ...positionOf(source, marker) })),
},
};
}));
assert.deepStrictEqual(results.map(result => result.actual), results.map(result => result.expected));
});
}
}
for (const target of ['server', 'server-web', 'web'] as const) {
test(`does not read or emit desktop preloads for ${target}`, async t => {
const root = await temporaryDirectory(t);
const outDir = path.join(root, 'out');
await compileStandaloneFiles(path.join(root, 'missing-src'), outDir, target, true, sourceMapBaseUrl);
assert.strictEqual(fs.existsSync(outDir), false);
});
}
test('development transpilation keeps inline maps without embedded sources', async t => {
const outDir = path.join(await temporaryDirectory(t), 'out');
const maps = await Promise.all(preloads.map(async ({ file }) => {
const entryPath = path.join(srcDir, file);
const outPath = path.join(outDir, file.replace(/\.ts$/, '.js'));
await transpileFile(entryPath, outPath);
const output = await fs.promises.readFile(outPath, 'utf8');
const encodedMap = output.match(/^\/\/# sourceMappingURL=data:application\/json;base64,(?<map>.+)$/m)?.groups?.map;
assert.ok(encodedMap);
const map: RawSourceMap = JSON.parse(Buffer.from(encodedMap, 'base64').toString('utf8'));
return { sources: map.sources.map(source => path.normalize(source)), sourcesContent: map.sourcesContent };
}));
assert.deepStrictEqual({
files: await outputFiles(outDir),
maps,
}, {
files: javascriptFiles,
maps: preloads.map(({ file }) => ({ sources: [path.join(srcDir, file)], sourcesContent: undefined })),
});
});
test('CI upload layout and map stripping retain final JavaScript and checksum bytes', async t => {
const root = await temporaryDirectory(t);
const outDir = path.join(root, 'out');
const packagedOut = path.join(root, 'package', 'out');
await compileStandaloneFiles(srcDir, outDir, 'desktop', true, sourceMapBaseUrl);
const finalContents = await Promise.all(javascriptFiles.map(file => fs.promises.readFile(path.join(outDir, file))));
const uploadedPaths: string[] = [];
// Match upload-sourcemaps.ts: select maps relative to the output root and insert core/.
await pipeline(
vfs.src('**/*.map', { cwd: outDir, base: outDir }),
new Writable({
objectMode: true,
write(file: Vinyl, _encoding, callback) {
file.path = path.join(file.base, 'core', file.relative);
uploadedPaths.push(file.relative.replaceAll('\\', '/'));
callback();
},
}),
);
// Match packageTask's CI filter, without assembling an Electron distribution.
await pipeline(
gulp.src('**', { cwd: outDir, base: outDir }),
filter(['**', '!**/*.{js,css}.map'], { dot: true }),
gulp.dest(packagedOut),
);
const packagedContents = await Promise.all(javascriptFiles.map(file => fs.promises.readFile(path.join(packagedOut, file))));
assert.deepStrictEqual({
uploadedPaths: uploadedPaths.sort(),
packagedFiles: await outputFiles(packagedOut),
comments: packagedContents.map(contents => contents.toString().trimEnd().split('\n').at(-1)),
checksums: packagedContents.map(checksum),
unchangedBytes: packagedContents.map((contents, index) => contents.equals(finalContents[index])),
}, {
uploadedPaths: javascriptFiles.map(file => `core/${file}.map`),
packagedFiles: javascriptFiles,
comments: javascriptFiles.map(file => `//# sourceMappingURL=${sourceMapRoot}/core/${file}.map`),
checksums: finalContents.map(checksum),
unchangedBytes: javascriptFiles.map(() => true),
});
const preloadIndex = javascriptFiles.indexOf('vs/base/parts/sandbox/electron-browser/preload.js');
const beforeURLRewrite = finalContents[preloadIndex].toString().replace(
/^\/\/# sourceMappingURL=.+$/m,
'//# sourceMappingURL=preload.js.map',
);
assert.notStrictEqual(checksum(finalContents[preloadIndex]), checksum(Buffer.from(beforeURLRewrite)));
});
test('fails rather than skipping missing standalone sources', async t => {
const root = await temporaryDirectory(t);
await assert.rejects(
compileStandaloneFiles(path.join(root, 'missing-src'), path.join(root, 'out'), 'desktop', true),
/Build failed/,
);
});
});
async function temporaryDirectory(t: TestContext): Promise<string> {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'vscode-standalone-test-'));
t.after(() => fs.promises.rm(root, { recursive: true, force: true }));
return root;
}
async function outputFiles(outDir: string): Promise<string[]> {
const entries = await fs.promises.readdir(outDir, { recursive: true, withFileTypes: true });
return entries.filter(entry => entry.isFile())
.map(entry => path.relative(outDir, path.join(entry.parentPath, entry.name)).replaceAll('\\', '/'))
.sort();
}
function positionOf(source: string, token: string): { line: number; column: number } {
const offset = source.indexOf(token);
assert.ok(offset >= 0, `Missing token: ${token}`);
const prefix = source.slice(0, offset);
return { line: prefix.split('\n').length, column: offset - prefix.lastIndexOf('\n') - 1 };
}
function checksum(contents: Buffer): string {
return crypto.createHash('sha256').update(contents).digest('base64').replace(/=+$/, '');
}
async function compileBaseline(entryPath: string, outPath: string, minify: boolean) {
// Original standalone options: only map publication is allowed to change JavaScript bytes.
const result = await esbuild.build({
entryPoints: [entryPath],
outfile: outPath,
bundle: false,
format: 'cjs',
platform: 'node',
target: ['es2024'],
sourcemap: 'linked',
sourcesContent: false,
minify,
banner: {
js: `/*!--------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/`,
},
write: false,
logLevel: 'warning',
});
const javascript = result.outputFiles.find(file => file.path === outPath);
const sourceMap = result.outputFiles.find(file => file.path === `${outPath}.map`);
assert.ok(javascript && sourceMap);
const map: RawSourceMap = JSON.parse(sourceMap.text);
return { code: javascript.text, map };
}