From 6f5539e13c922142852438a1f0433881e8965362 Mon Sep 17 00:00:00 2001 From: Elecmonkey Date: Tue, 13 Oct 2026 10:43:42 +0800 Subject: [PATCH 1/2] Add noEmit to BuildOrchestrator overrideCompilerOptions Also fix CreateBuildOrchestratorParams so that build options and compiler option overrides reach the server. The embedded BuildOptions and CompilerOptions had json name tags, so the server expected them nested, but gen-proto ignores tags on embedded fields and generated a flat type, so the client sent them flat and they were silently dropped. Use named fields so the generated type is nested, and have the client build both objects explicitly. --- packages/typescript/src/api/async/api.ts | 10 ++-- .../typescript/src/api/proto.generated.ts | 5 +- packages/typescript/src/api/sync/api.ts | 19 ++++--- packages/typescript/test/async/api.test.ts | 51 ++++++++++++++++++- packages/typescript/test/sync/api.test.ts | 51 ++++++++++++++++++- tools/gen-proto/main_test.go | 3 +- tsc/internal/api/proto.go | 6 +-- 7 files changed, 123 insertions(+), 22 deletions(-) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index bf3fe53bbce5d..b6b9798550f16 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -486,13 +486,12 @@ export class API implements FormatDiagnosticsHo /** @internal */ async createBuildOrchestrator(rootNames: readonly string[], buildOrchestratorOptions: BuildOrchestratorOptions): Promise { await this.ensureInitialized(); + const { cwd, dry, force, verbose, stopBuildOnErrors, overrideCompilerOptions } = buildOrchestratorOptions; const orchestratorResponse = await this.client.apiRequest("createBuildOrchestrator", { - ...buildOrchestratorOptions, - ...buildOrchestratorOptions.overrideCompilerOptions, - cwd: buildOrchestratorOptions.cwd === undefined - ? undefined - : toRootedDirectoryPath(buildOrchestratorOptions.cwd, this.getCurrentDirectory()), rootNames, + cwd: cwd === undefined ? undefined : toRootedDirectoryPath(cwd, this.getCurrentDirectory()), + buildOptions: { dry, force, verbose, stopBuildOnErrors }, + compilerOptions: overrideCompilerOptions, }); const orchestrator = new BuildOrchestrator(this.client, orchestratorResponse, () => { @@ -2201,6 +2200,7 @@ export interface OverrideCompilerOptions { sourceMap?: boolean; inlineSourceMap?: boolean; traceResolution?: boolean; + noEmit?: boolean; } export class BuildOrchestrator { diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35502aade2ac5..90dea2f6933f6 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -351,9 +351,12 @@ export interface GetCurrentLanguageServerSnapshotParams { changes?: LanguageServerSnapshotChanges | undefined; } -export interface CreateBuildOrchestratorParams extends BuildOptions, CompilerOptions { +export interface CreateBuildOrchestratorParams { rootNames: readonly string[] | null; cwd?: RootedDirectoryPath | undefined; + /** Only a subset of these options are exposed in the API */ + buildOptions?: BuildOptions | undefined; + compilerOptions?: CompilerOptions | undefined; } export interface CreateBuildOrchestratorResponse { diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index c8db7d186170c..7b1783a4d92ce 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -592,13 +592,12 @@ export class API implements FormatDiagnosticsHo "createBuildOrchestrator", function (rootNames: readonly string[], buildOrchestratorOptions: BuildOrchestratorOptions): BuildOrchestrator { owner.ensureInitialized(); + const { cwd, dry, force, verbose, stopBuildOnErrors, overrideCompilerOptions } = buildOrchestratorOptions; const orchestratorResponse = owner.client.apiRequest("createBuildOrchestrator", { - ...buildOrchestratorOptions, - ...buildOrchestratorOptions.overrideCompilerOptions, - cwd: buildOrchestratorOptions.cwd === undefined - ? undefined - : toRootedDirectoryPath(buildOrchestratorOptions.cwd, owner.getCurrentDirectory()), rootNames, + cwd: cwd === undefined ? undefined : toRootedDirectoryPath(cwd, owner.getCurrentDirectory()), + buildOptions: { dry, force, verbose, stopBuildOnErrors }, + compilerOptions: overrideCompilerOptions, }); const orchestrator = new BuildOrchestrator(owner.client, orchestratorResponse, () => { @@ -609,13 +608,12 @@ export class API implements FormatDiagnosticsHo }, function* (rootNames: readonly string[], buildOrchestratorOptions: BuildOrchestratorOptions): Generator { yield* owner.ensureInitialized.gen(); + const { cwd, dry, force, verbose, stopBuildOnErrors, overrideCompilerOptions } = buildOrchestratorOptions; const orchestratorResponse = yield* apiRequest("createBuildOrchestrator", { - ...buildOrchestratorOptions, - ...buildOrchestratorOptions.overrideCompilerOptions, - cwd: buildOrchestratorOptions.cwd === undefined - ? undefined - : toRootedDirectoryPath(buildOrchestratorOptions.cwd, owner.getCurrentDirectory()), rootNames, + cwd: cwd === undefined ? undefined : toRootedDirectoryPath(cwd, owner.getCurrentDirectory()), + buildOptions: { dry, force, verbose, stopBuildOnErrors }, + compilerOptions: overrideCompilerOptions, }); const orchestrator = new BuildOrchestrator(owner.client, orchestratorResponse, () => { @@ -3946,6 +3944,7 @@ export interface OverrideCompilerOptions { sourceMap?: boolean; inlineSourceMap?: boolean; traceResolution?: boolean; + noEmit?: boolean; } export class BuildOrchestrator { diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 3fc2126b968bd..c9a5b4c545ab3 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1712,7 +1712,7 @@ describe("BuildOrchestrator", () => { overrideCompilerOptions: { incremental: true, assumeChangesOnlyAffectDirectDependencies: true, - declaration: false, + declaration: true, declarationMap: true, emitDeclarationOnly: false, sourceMap: false, @@ -1731,6 +1731,55 @@ describe("BuildOrchestrator", () => { assert.match(fs.readFile!(toRootedFilePath("/b/dist/index.js", undefined))!, /export const b = 2/); }); + test("applies overrideCompilerOptions to the build", async () => { + const { api: disposableApi, fs } = spawnAPIWithFS({ ...files }); + await using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = await api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", overrideCompilerOptions: { declarationMap: true } }, + ); + + assert.equal((await orchestrator.build()).status, 0); + assert.match(fs.readFile!(toRootedFilePath("/a/dist/index.d.ts.map", undefined))!, /"sources":\["\.\.\/src\/index\.ts"\]/); + }); + + test("applies build options to the build", async () => { + const { api: disposableApi, fs } = spawnAPIWithFS({ ...files }); + await using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = await api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", dry: true }, + ); + + assert.equal((await orchestrator.build()).status, 0); + assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); + }); + + test("does not emit when noEmit is overridden", async () => { + const source = `export const value: string = 1;`; + const { api: disposableApi, fs } = spawnAPIWithFS({ + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/a/src/index.ts": source, + }); + await using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = await api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", overrideCompilerOptions: { noEmit: true } }, + ); + + const response = await orchestrator.build(); + assert.equal(response.status, 2); + assert.equal(response.diagnostics?.length, 1); + assert.equal(response.diagnostics[0].code, 2322); + assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); + }); + test("returns diagnostics in build response information", async () => { const source = `export const value: string = 1;`; const { api: disposableApi } = spawnAPIWithFS({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index bb4caee65856e..ed45b9a764415 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -1578,7 +1578,7 @@ describe("BuildOrchestrator", () => { overrideCompilerOptions: { incremental: true, assumeChangesOnlyAffectDirectDependencies: true, - declaration: false, + declaration: true, declarationMap: true, emitDeclarationOnly: false, sourceMap: false, @@ -1597,6 +1597,55 @@ describe("BuildOrchestrator", () => { assert.match(fs.readFile!(toRootedFilePath("/b/dist/index.js", undefined))!, /export const b = 2/); }); + test("applies overrideCompilerOptions to the build", () => { + const { api: disposableApi, fs } = spawnAPIWithFS({ ...files }); + using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", overrideCompilerOptions: { declarationMap: true } }, + ); + + assert.equal((orchestrator.build()).status, 0); + assert.match(fs.readFile!(toRootedFilePath("/a/dist/index.d.ts.map", undefined))!, /"sources":\["\.\.\/src\/index\.ts"\]/); + }); + + test("applies build options to the build", () => { + const { api: disposableApi, fs } = spawnAPIWithFS({ ...files }); + using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", dry: true }, + ); + + assert.equal((orchestrator.build()).status, 0); + assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); + }); + + test("does not emit when noEmit is overridden", () => { + const source = `export const value: string = 1;`; + const { api: disposableApi, fs } = spawnAPIWithFS({ + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/a/src/index.ts": source, + }); + using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", overrideCompilerOptions: { noEmit: true } }, + ); + + const response = orchestrator.build(); + assert.equal(response.status, 2); + assert.equal(response.diagnostics?.length, 1); + assert.equal(response.diagnostics[0].code, 2322); + assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); + }); + test("returns diagnostics in build response information", () => { const source = `export const value: string = 1;`; const { api: disposableApi } = spawnAPIWithFS({ diff --git a/tools/gen-proto/main_test.go b/tools/gen-proto/main_test.go index c7cb555c4cd36..446b2f3d41486 100644 --- a/tools/gen-proto/main_test.go +++ b/tools/gen-proto/main_test.go @@ -49,7 +49,8 @@ func TestGenerate(t *testing.T) { `import type { CompilerOptions, PluginImport } from "./compilerOptions.generated.ts";`, `export type { CompilerOptions, PluginImport } from "./compilerOptions.generated.ts";`, `export * from "./compilerOptions.generated.ts";`, - `export interface CreateBuildOrchestratorParams extends BuildOptions, CompilerOptions`, + `buildOptions?: BuildOptions | undefined;`, + `compilerOptions?: CompilerOptions | undefined;`, `jsx?: JsxEmit | undefined;`, `module?: ModuleKind | undefined;`, `moduleResolution?: ModuleResolutionKind | undefined;`, diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index c3e55e188956d..fc617acfed133 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -992,9 +992,9 @@ type ProfileResult struct { type CreateBuildOrchestratorParams struct { RootNames []string `json:"rootNames"` Cwd tspath.RootedDirectoryPath `json:"cwd,omitempty"` - // Only a subset of these options are exposed the API - *core.BuildOptions `json:"buildOptions,omitempty"` - *core.CompilerOptions `json:"compilerOptions,omitempty"` + // Only a subset of these options are exposed in the API + BuildOptions *core.BuildOptions `json:"buildOptions,omitempty"` + CompilerOptions *core.CompilerOptions `json:"compilerOptions,omitempty"` } type CreateBuildOrchestratorResponse struct { From d47d90405709d99c39d58ed356faf73fad0bace9 Mon Sep 17 00:00:00 2001 From: Elecmonkey Date: Tue, 13 Oct 2026 10:59:07 +0800 Subject: [PATCH 2/2] Add test for noEmit on error-free projects --- packages/typescript/test/async/api.test.ts | 21 +++++++++++++++++++++ packages/typescript/test/sync/api.test.ts | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index c9a5b4c545ab3..eca0e838982b2 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1757,6 +1757,27 @@ describe("BuildOrchestrator", () => { assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); }); + test("does not emit error-free projects when noEmit is overridden", async () => { + const { api: disposableApi, fs } = spawnAPIWithFS({ + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/a/src/index.ts": `export const a = 1;`, + }); + await using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = await api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", overrideCompilerOptions: { noEmit: true } }, + ); + + const response = await orchestrator.build(); + assert.equal(response.status, 0); + assert.equal(response.diagnostics, undefined); + assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); + }); + test("does not emit when noEmit is overridden", async () => { const source = `export const value: string = 1;`; const { api: disposableApi, fs } = spawnAPIWithFS({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index ed45b9a764415..256c021b5a535 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -1623,6 +1623,27 @@ describe("BuildOrchestrator", () => { assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); }); + test("does not emit error-free projects when noEmit is overridden", () => { + const { api: disposableApi, fs } = spawnAPIWithFS({ + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/a/src/index.ts": `export const a = 1;`, + }); + using api = disposableApi; + // @ts-ignore -- Internal API exercised by source tests. + const orchestrator = api.createBuildOrchestrator( + ["/a/tsconfig.json"], + { cwd: "/", overrideCompilerOptions: { noEmit: true } }, + ); + + const response = orchestrator.build(); + assert.equal(response.status, 0); + assert.equal(response.diagnostics, undefined); + assert.equal(fs.readFile!(toRootedFilePath("/a/dist/index.js", undefined)), serverFS.useOS); + }); + test("does not emit when noEmit is overridden", () => { const source = `export const value: string = 1;`; const { api: disposableApi, fs } = spawnAPIWithFS({