From db58c63ac42c24fc2df8387e5ca45da661d68b61 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 a9dd53fe3a3f1..78709a7353043 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -485,13 +485,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, () => { @@ -2178,6 +2177,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 93774869c4407..b8f2cded08cc2 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -338,9 +338,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 8950863c33460..a052d60673d6d 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, () => { @@ -3933,6 +3931,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 379f07b6b68f7..6e394c8b062fa 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1706,7 +1706,7 @@ describe("BuildOrchestrator", () => { overrideCompilerOptions: { incremental: true, assumeChangesOnlyAffectDirectDependencies: true, - declaration: false, + declaration: true, declarationMap: true, emitDeclarationOnly: false, sourceMap: false, @@ -1725,6 +1725,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 f048e8ede87e6..d5977ca114cce 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -1572,7 +1572,7 @@ describe("BuildOrchestrator", () => { overrideCompilerOptions: { incremental: true, assumeChangesOnlyAffectDirectDependencies: true, - declaration: false, + declaration: true, declarationMap: true, emitDeclarationOnly: false, sourceMap: false, @@ -1591,6 +1591,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 6a7cdb969ce43..2d7d1e9c1064c 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 c03e7ff85ed46..b905155e2f499 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -968,9 +968,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 1328b3839eaeab7b2bf8cdc09bb81ae61dc90931 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 6e394c8b062fa..84b4774239196 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -1751,6 +1751,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 d5977ca114cce..8580875a57bb8 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -1617,6 +1617,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({