From 1c26d9e5ac645a6a30097af52fe873fdcbd98fd6 Mon Sep 17 00:00:00 2001 From: Titian Cernicova-Dragomir Date: Thu, 9 Jul 2026 09:52:52 +0100 Subject: [PATCH 1/2] Add emit method to the API. --- Herebyfile.mjs | 1 + _packages/native-preview/src/api/async/api.ts | 19 ++- .../native-preview/src/api/async/types.ts | 12 ++ _packages/native-preview/src/api/sync/api.ts | 19 ++- .../native-preview/src/api/sync/types.ts | 12 ++ .../native-preview/src/enums/emitOnly.enum.ts | 8 ++ .../native-preview/src/enums/emitOnly.ts | 8 ++ .../native-preview/test/async/api.test.ts | 112 ++++++++++++++++++ .../native-preview/test/sync/api.test.ts | 112 ++++++++++++++++++ internal/api/proto.go | 21 ++++ internal/api/session.go | 66 +++++++++++ internal/compiler/emitter.go | 3 +- internal/compiler/program.go | 1 + 13 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 _packages/native-preview/src/enums/emitOnly.enum.ts create mode 100644 _packages/native-preview/src/enums/emitOnly.ts diff --git a/Herebyfile.mjs b/Herebyfile.mjs index 47ef8dd6a9b..7081f71ccb3 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -348,6 +348,7 @@ const enumDefs = [ { name: "TokenFlags", goPrefix: "TokenFlags", goFile: "internal/ast/tokenflags.go", outDir: "_packages/native-preview/src/enums" }, { name: "NodeBuilderFlags", goPrefix: "Flags", goFile: "internal/nodebuilder/types.go", outDir: "_packages/native-preview/src/enums" }, { name: "CompletionItemKind", goPrefix: "CompletionItemKind", goFile: "internal/lsp/lsproto/lsp_generated.go", outDir: "_packages/native-preview/src/enums" }, + { name: "EmitOnly", goPrefix: "Emit", goFile: "internal/compiler/emitter.go", outDir: "_packages/native-preview/src/enums" }, // String enum: Go stores internal names with a "\xFE" sentinel prefix, but the escaped // form sent over the wire uses "__" (see EscapeSymbolName), so map the sentinel accordingly. { name: "InternalSymbolName", goPrefix: "InternalSymbolName", goFile: "internal/ast/symbol.go", outDir: "_packages/native-preview/src/enums", stringEnum: true, valueReplacements: { InternalSymbolNamePrefix: "__" } }, diff --git a/_packages/native-preview/src/api/async/api.ts b/_packages/native-preview/src/api/async/api.ts index 1c8b5d7f0fe..27df32be56a 100644 --- a/_packages/native-preview/src/api/async/api.ts +++ b/_packages/native-preview/src/api/async/api.ts @@ -2,6 +2,7 @@ import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; +import { EmitOnly } from "#enums/emitOnly"; import { ModuleKind } from "#enums/moduleKind"; import { NodeBuilderFlags } from "#enums/nodeBuilderFlags"; import { ObjectFlags } from "#enums/objectFlags"; @@ -87,6 +88,8 @@ import type { CompletionOptions, ConditionalType, Diagnostic, + EmitOutputFile, + EmitResult, FreshableType, IdentifierTypePredicate, IndexedAccessType, @@ -115,8 +118,8 @@ import type { } from "./types.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; -export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; -export type { APIOptions, AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, DocumentIdentifier, DocumentPosition, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, RequestTiming, SourceFileMetadata, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TimingAccumulators, TimingInfo, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export { CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; +export type { APIOptions, AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, DocumentIdentifier, DocumentPosition, EmitOutputFile, EmitResult, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, RequestTiming, SourceFileMetadata, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TimingAccumulators, TimingInfo, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export class API { private client: Client; @@ -798,6 +801,18 @@ export class Program { }); return data ?? []; } + + async emit(file?: DocumentIdentifier | readonly DocumentIdentifier[], emitOnly?: EmitOnly): Promise { + const files = file === undefined ? undefined + : Array.isArray(file) ? file + : [file]; + return this.client.apiRequest("emit", { + snapshot: this.snapshotId, + project: this.project.id, + files, + emitOnly, + }); + } } export class Checker { diff --git a/_packages/native-preview/src/api/async/types.ts b/_packages/native-preview/src/api/async/types.ts index ccc619227f1..9be5e49c129 100644 --- a/_packages/native-preview/src/api/async/types.ts +++ b/_packages/native-preview/src/api/async/types.ts @@ -351,3 +351,15 @@ export interface Diagnostic { /** Related diagnostic information */ readonly relatedInformation?: readonly Diagnostic[] | undefined; } + +export interface EmitOutputFile { + readonly fileName: string; + readonly text: string; + readonly sourceFileName?: string | undefined; +} + +export interface EmitResult { + readonly emitSkipped: boolean; + readonly diagnostics: readonly Diagnostic[]; + readonly outputFiles: readonly EmitOutputFile[]; +} diff --git a/_packages/native-preview/src/api/sync/api.ts b/_packages/native-preview/src/api/sync/api.ts index 1a0296ac771..16d0db37a1e 100644 --- a/_packages/native-preview/src/api/sync/api.ts +++ b/_packages/native-preview/src/api/sync/api.ts @@ -10,6 +10,7 @@ import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; +import { EmitOnly } from "#enums/emitOnly"; import { ModuleKind } from "#enums/moduleKind"; import { NodeBuilderFlags } from "#enums/nodeBuilderFlags"; import { ObjectFlags } from "#enums/objectFlags"; @@ -95,6 +96,8 @@ import type { CompletionOptions, ConditionalType, Diagnostic, + EmitOutputFile, + EmitResult, FreshableType, IdentifierTypePredicate, IndexedAccessType, @@ -123,8 +126,8 @@ import type { } from "./types.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; -export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; -export type { APIOptions, AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, DocumentIdentifier, DocumentPosition, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, RequestTiming, SourceFileMetadata, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TimingAccumulators, TimingInfo, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export { CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; +export type { APIOptions, AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, DocumentIdentifier, DocumentPosition, EmitOutputFile, EmitResult, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, RequestTiming, SourceFileMetadata, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TimingAccumulators, TimingInfo, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export class API { private client: Client; @@ -806,6 +809,18 @@ export class Program { }); return data ?? []; } + + emit(file?: DocumentIdentifier | readonly DocumentIdentifier[], emitOnly?: EmitOnly): EmitResult { + const files = file === undefined ? undefined + : Array.isArray(file) ? file + : [file]; + return this.client.apiRequest("emit", { + snapshot: this.snapshotId, + project: this.project.id, + files, + emitOnly, + }); + } } export class Checker { diff --git a/_packages/native-preview/src/api/sync/types.ts b/_packages/native-preview/src/api/sync/types.ts index 658d0780655..8c816bbe0f8 100644 --- a/_packages/native-preview/src/api/sync/types.ts +++ b/_packages/native-preview/src/api/sync/types.ts @@ -359,3 +359,15 @@ export interface Diagnostic { /** Related diagnostic information */ readonly relatedInformation?: readonly Diagnostic[] | undefined; } + +export interface EmitOutputFile { + readonly fileName: string; + readonly text: string; + readonly sourceFileName?: string | undefined; +} + +export interface EmitResult { + readonly emitSkipped: boolean; + readonly diagnostics: readonly Diagnostic[]; + readonly outputFiles: readonly EmitOutputFile[]; +} diff --git a/_packages/native-preview/src/enums/emitOnly.enum.ts b/_packages/native-preview/src/enums/emitOnly.enum.ts new file mode 100644 index 00000000000..b2c2e2e5b6b --- /dev/null +++ b/_packages/native-preview/src/enums/emitOnly.enum.ts @@ -0,0 +1,8 @@ +// Code generated by Herebyfile.mjs generate:enums from internal/compiler/emitter.go. DO NOT EDIT. + +export enum EmitOnly { + All = 0, + OnlyJs = 1, + OnlyDts = 2, + OnlyForcedDts = 3, +} diff --git a/_packages/native-preview/src/enums/emitOnly.ts b/_packages/native-preview/src/enums/emitOnly.ts new file mode 100644 index 00000000000..b04dfd37568 --- /dev/null +++ b/_packages/native-preview/src/enums/emitOnly.ts @@ -0,0 +1,8 @@ +// Code generated by Herebyfile.mjs generate:enums from internal/compiler/emitter.go. DO NOT EDIT. +export var EmitOnly: any; +(function (EmitOnly) { + EmitOnly[EmitOnly["All"] = 0] = "All"; + EmitOnly[EmitOnly["OnlyJs"] = 1] = "OnlyJs"; + EmitOnly[EmitOnly["OnlyDts"] = 2] = "OnlyDts"; + EmitOnly[EmitOnly["OnlyForcedDts"] = 3] = "OnlyForcedDts"; +})(EmitOnly || (EmitOnly = {})); diff --git a/_packages/native-preview/test/async/api.test.ts b/_packages/native-preview/test/async/api.test.ts index 16b3b00ecbc..f6317959f9a 100644 --- a/_packages/native-preview/test/async/api.test.ts +++ b/_packages/native-preview/test/async/api.test.ts @@ -45,6 +45,7 @@ import { type BigIntLiteralType, type ConditionalType, DiagnosticCategory, + EmitOnly, type FreshableType, type IndexedAccessType, type IndexType, @@ -4307,6 +4308,117 @@ export const obj = { m: 1, s: "hi", b: true }; }); }); +describe("Program - emit", () => { + test("emit single file produces JS output", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a: number = 1;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = await project.program.emit("/src/a.ts"); + assert.equal(result.emitSkipped, false); + assert.ok(result.outputFiles.length > 0); + const jsFile = result.outputFiles.find(f => f.fileName.endsWith(".js")); + assert.ok(jsFile, "should produce a .js file"); + assert.ok(jsFile.text.includes("export const a = 1"), "JS output should contain the declaration"); + assert.equal(jsFile.sourceFileName, "/src/a.ts"); + assert.deepEqual(result.diagnostics, []); + } + finally { + await api.close(); + } + }); + + test("emit multiple files produces output for each", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a: number = 1;`, + "/src/b.ts": `export const b: string = "hello";`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = await project.program.emit(["/src/a.ts", "/src/b.ts"]); + assert.equal(result.emitSkipped, false); + const jsFiles = result.outputFiles.filter(f => f.fileName.endsWith(".js")); + assert.equal(jsFiles.length, 2); + assert.ok(jsFiles.some(f => f.sourceFileName === "/src/a.ts")); + assert.ok(jsFiles.some(f => f.sourceFileName === "/src/b.ts")); + } + finally { + await api.close(); + } + }); + + test("emit with no file emits all files", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = await project.program.emit(); + assert.equal(result.emitSkipped, false); + const jsFiles = result.outputFiles.filter(f => f.fileName.endsWith(".js")); + assert.ok(jsFiles.length >= 2, "should emit at least 2 JS files"); + } + finally { + await api.close(); + } + }); + + test("emit with empty array returns empty", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a = 1;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = await project.program.emit([]); + assert.equal(result.emitSkipped, false); + assert.deepEqual(result.outputFiles, []); + } + finally { + await api.close(); + } + }); + + test("emit single file respects emitOnly", async () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020", declaration: true } }), + "/src/a.ts": `export const a: number = 1;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + + const all = await project.program.emit("/src/a.ts"); + assert.equal(all.emitSkipped, false); + assert.ok(all.outputFiles.some(f => f.fileName.endsWith(".js"))); + assert.ok(all.outputFiles.some(f => f.fileName.endsWith(".d.ts"))); + assert.ok(all.outputFiles.every(f => f.sourceFileName === "/src/a.ts")); + + const jsOnly = await project.program.emit("/src/a.ts", EmitOnly.OnlyJs); + assert.equal(jsOnly.emitSkipped, false); + assert.ok(jsOnly.outputFiles.some(f => f.fileName.endsWith(".js"))); + assert.ok(!jsOnly.outputFiles.some(f => f.fileName.endsWith(".d.ts"))); + + const dtsOnly = await project.program.emit("/src/a.ts", EmitOnly.OnlyDts); + assert.equal(dtsOnly.emitSkipped, false); + assert.ok(dtsOnly.outputFiles.some(f => f.fileName.endsWith(".d.ts"))); + assert.ok(!dtsOnly.outputFiles.some(f => f.fileName.endsWith(".js"))); + } + finally { + await api.close(); + } + }); +}); + describe("modifierFlags", () => { test("export async function has Export | Async flags", async () => { const api = spawnAPI({ diff --git a/_packages/native-preview/test/sync/api.test.ts b/_packages/native-preview/test/sync/api.test.ts index 6a90030eb2b..498dd0b5d7b 100644 --- a/_packages/native-preview/test/sync/api.test.ts +++ b/_packages/native-preview/test/sync/api.test.ts @@ -55,6 +55,7 @@ import { type BigIntLiteralType, type ConditionalType, DiagnosticCategory, + EmitOnly, type FreshableType, type IndexedAccessType, type IndexType, @@ -4315,6 +4316,117 @@ export const obj = { m: 1, s: "hi", b: true }; }); }); +describe("Program - emit", () => { + test("emit single file produces JS output", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a: number = 1;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = project.program.emit("/src/a.ts"); + assert.equal(result.emitSkipped, false); + assert.ok(result.outputFiles.length > 0); + const jsFile = result.outputFiles.find(f => f.fileName.endsWith(".js")); + assert.ok(jsFile, "should produce a .js file"); + assert.ok(jsFile.text.includes("export const a = 1"), "JS output should contain the declaration"); + assert.equal(jsFile.sourceFileName, "/src/a.ts"); + assert.deepEqual(result.diagnostics, []); + } + finally { + api.close(); + } + }); + + test("emit multiple files produces output for each", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a: number = 1;`, + "/src/b.ts": `export const b: string = "hello";`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = project.program.emit(["/src/a.ts", "/src/b.ts"]); + assert.equal(result.emitSkipped, false); + const jsFiles = result.outputFiles.filter(f => f.fileName.endsWith(".js")); + assert.equal(jsFiles.length, 2); + assert.ok(jsFiles.some(f => f.sourceFileName === "/src/a.ts")); + assert.ok(jsFiles.some(f => f.sourceFileName === "/src/b.ts")); + } + finally { + api.close(); + } + }); + + test("emit with no file emits all files", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = project.program.emit(); + assert.equal(result.emitSkipped, false); + const jsFiles = result.outputFiles.filter(f => f.fileName.endsWith(".js")); + assert.ok(jsFiles.length >= 2, "should emit at least 2 JS files"); + } + finally { + api.close(); + } + }); + + test("emit with empty array returns empty", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020" } }), + "/src/a.ts": `export const a = 1;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const result = project.program.emit([]); + assert.equal(result.emitSkipped, false); + assert.deepEqual(result.outputFiles, []); + } + finally { + api.close(); + } + }); + + test("emit single file respects emitOnly", () => { + const api = spawnAPI({ + "/tsconfig.json": JSON.stringify({ compilerOptions: { target: "es2020", declaration: true } }), + "/src/a.ts": `export const a: number = 1;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + + const all = project.program.emit("/src/a.ts"); + assert.equal(all.emitSkipped, false); + assert.ok(all.outputFiles.some(f => f.fileName.endsWith(".js"))); + assert.ok(all.outputFiles.some(f => f.fileName.endsWith(".d.ts"))); + assert.ok(all.outputFiles.every(f => f.sourceFileName === "/src/a.ts")); + + const jsOnly = project.program.emit("/src/a.ts", EmitOnly.OnlyJs); + assert.equal(jsOnly.emitSkipped, false); + assert.ok(jsOnly.outputFiles.some(f => f.fileName.endsWith(".js"))); + assert.ok(!jsOnly.outputFiles.some(f => f.fileName.endsWith(".d.ts"))); + + const dtsOnly = project.program.emit("/src/a.ts", EmitOnly.OnlyDts); + assert.equal(dtsOnly.emitSkipped, false); + assert.ok(dtsOnly.outputFiles.some(f => f.fileName.endsWith(".d.ts"))); + assert.ok(!dtsOnly.outputFiles.some(f => f.fileName.endsWith(".js"))); + } + finally { + api.close(); + } + }); +}); + describe("modifierFlags", () => { test("export async function has Export | Async flags", () => { const api = spawnAPI({ diff --git a/internal/api/proto.go b/internal/api/proto.go index 0917c385d64..a111b35095b 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -179,6 +179,7 @@ const ( // Emitter methods MethodPrintNode Method = "printNode" + MethodEmit Method = "emit" // Intrinsic type getters MethodGetAnyType Method = "getAnyType" @@ -451,6 +452,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetSignatureUsages: unmarshallerFor[GetSignatureUsagesParams], MethodGetCompletionsAtPosition: unmarshallerFor[GetCompletionsAtPositionParams], MethodPrintNode: unmarshallerFor[PrintNodeParams], + MethodEmit: unmarshallerFor[EmitParams], MethodGetAnyType: unmarshallerFor[GetIntrinsicTypeParams], MethodGetStringType: unmarshallerFor[GetIntrinsicTypeParams], MethodGetNumberType: unmarshallerFor[GetIntrinsicTypeParams], @@ -1030,6 +1032,25 @@ type PrintNodeParams struct { TerminateUnterminatedLiterals bool `json:"terminateUnterminatedLiterals,omitempty"` } +type EmitParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Files []DocumentIdentifier `json:"files,omitempty"` + EmitOnly *byte `json:"emitOnly,omitempty"` +} + +type EmitOutputFile struct { + FileName string `json:"fileName"` + Text string `json:"text"` + SourceFileName *string `json:"sourceFileName,omitempty"` +} + +type EmitResponse struct { + EmitSkipped bool `json:"emitSkipped"` + Diagnostics []*DiagnosticResponse `json:"diagnostics"` + OutputFiles []*EmitOutputFile `json:"outputFiles"` +} + // CheckerTypeParams are parameters for checker methods that operate on a type. type CheckerTypeParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/internal/api/session.go b/internal/api/session.go index 7e29c44b6ff..4081a389171 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -672,6 +672,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleTypeToString(ctx, parsed.(*TypeToTypeNodeParams)) case string(MethodPrintNode): return s.handlePrintNode(ctx, parsed.(*PrintNodeParams)) + case string(MethodEmit): + return s.handleEmit(ctx, parsed.(*EmitParams)) case string(MethodIsContextSensitive): return s.handleIsContextSensitive(ctx, parsed.(*GetContextualTypeParams)) case string(MethodGetReturnTypeOfSignature): @@ -2155,6 +2157,70 @@ func (s *Session) handlePrintNode(_ context.Context, params *PrintNodeParams) (s return p.Emit(node, nil), nil } +func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + + emitOnly := compiler.EmitAll + if params.EmitOnly != nil { + emitOnly = compiler.EmitOnly(*params.EmitOnly) + } + + var outputFiles collections.SyncSet[*EmitOutputFile] + writeFile := func(fileName string, text string, data *compiler.WriteFileData) error { + var sourceFileName *string + if data.SourceFile != nil { + name := data.SourceFile.FileName() + sourceFileName = &name + } + outputFiles.Add(&EmitOutputFile{FileName: fileName, Text: text, SourceFileName: sourceFileName}) + return nil + } + + if params.Files != nil { + var allDiags []*ast.Diagnostic + emitSkipped := false + for _, file := range params.Files { + sourceFile, err := s.resolveOptionalSourceFile(program, &file) + if err != nil { + return nil, err + } + result := program.Emit(ctx, compiler.EmitOptions{ + TargetSourceFile: sourceFile, + EmitOnly: emitOnly, + WriteFile: writeFile, + }) + allDiags = append(allDiags, result.Diagnostics...) + if result.EmitSkipped { + emitSkipped = true + } + } + return &EmitResponse{ + EmitSkipped: emitSkipped, + Diagnostics: NewDiagnosticResponses(allDiags), + OutputFiles: outputFiles.ToSlice(), + }, nil + } + + result := program.Emit(ctx, compiler.EmitOptions{ + EmitOnly: emitOnly, + WriteFile: writeFile, + }) + + return &EmitResponse{ + EmitSkipped: result.EmitSkipped, + Diagnostics: NewDiagnosticResponses(result.Diagnostics), + OutputFiles: outputFiles.ToSlice(), + }, nil +} + // handleGetIntrinsicType returns an intrinsic type (any, string, number, etc.). func (s *Session) handleGetIntrinsicType(ctx context.Context, params *GetIntrinsicTypeParams, getter func(*checker.Checker) *checker.Type) (*TypeResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) diff --git a/internal/compiler/emitter.go b/internal/compiler/emitter.go index 81fa16ad4d6..beb1ce2f5a1 100644 --- a/internal/compiler/emitter.go +++ b/internal/compiler/emitter.go @@ -317,7 +317,7 @@ func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, s // Write the source map if len(sourceMapFilePath) > 0 { sourceMap := sourceMapGenerator.String() - err := e.writeText(sourceMapFilePath, sourceMap, nil) + err := e.writeText(sourceMapFilePath, sourceMap, &WriteFileData{SourceFile: e.sourceFile}) if err != nil { e.emitterDiagnostics.Add(ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, jsFilePath, err.Error())) } else { @@ -336,6 +336,7 @@ func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, s data := &WriteFileData{ SourceMapUrlPos: sourceMapUrlPos, Diagnostics: e.emitterDiagnostics.GetDiagnostics(), + SourceFile: e.sourceFile, } err := e.writeText(jsFilePath, text, data) skippedDtsWrite := data.SkippedDtsWrite diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 74b2504d9ec..c59d7131d70 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1607,6 +1607,7 @@ type WriteFileData struct { BuildInfo any Diagnostics []*ast.Diagnostic SkippedDtsWrite bool + SourceFile *ast.SourceFile } type WriteFile func(fileName string, text string, data *WriteFileData) error From e51ab43efbf2e19c409fc2e5ebbf2ef680e52d65 Mon Sep 17 00:00:00 2001 From: tcernicovad1 Date: Thu, 9 Jul 2026 11:55:11 +0100 Subject: [PATCH 2/2] Change emit to allow multiple files to avoid duplication of global diagnostics --- .claude/worktrees/migrate-emit | 1 + internal/api/session.go | 25 +++--------- internal/compiler/emitter.go | 13 +++--- internal/compiler/program.go | 40 ++++++++++++------- .../incremental/affectedfileshandler.go | 4 +- .../execute/incremental/emitfileshandler.go | 38 +++++++++++------- internal/execute/incremental/program.go | 6 +-- 7 files changed, 66 insertions(+), 61 deletions(-) create mode 160000 .claude/worktrees/migrate-emit diff --git a/.claude/worktrees/migrate-emit b/.claude/worktrees/migrate-emit new file mode 160000 index 00000000000..e7efb4b210d --- /dev/null +++ b/.claude/worktrees/migrate-emit @@ -0,0 +1 @@ +Subproject commit e7efb4b210d3e5c5e10bb7deb17969c00515ac40 diff --git a/internal/api/session.go b/internal/api/session.go index 4081a389171..4601e98730a 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -2183,35 +2183,22 @@ func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResp outputFiles.Add(&EmitOutputFile{FileName: fileName, Text: text, SourceFileName: sourceFileName}) return nil } - + var targetSourceFiles []*ast.SourceFile if params.Files != nil { - var allDiags []*ast.Diagnostic - emitSkipped := false + targetSourceFiles = make([]*ast.SourceFile, 0, len(params.Files)) for _, file := range params.Files { sourceFile, err := s.resolveOptionalSourceFile(program, &file) if err != nil { return nil, err } - result := program.Emit(ctx, compiler.EmitOptions{ - TargetSourceFile: sourceFile, - EmitOnly: emitOnly, - WriteFile: writeFile, - }) - allDiags = append(allDiags, result.Diagnostics...) - if result.EmitSkipped { - emitSkipped = true - } + targetSourceFiles = append(targetSourceFiles, sourceFile) } - return &EmitResponse{ - EmitSkipped: emitSkipped, - Diagnostics: NewDiagnosticResponses(allDiags), - OutputFiles: outputFiles.ToSlice(), - }, nil } result := program.Emit(ctx, compiler.EmitOptions{ - EmitOnly: emitOnly, - WriteFile: writeFile, + EmitOnly: emitOnly, + WriteFile: writeFile, + TargetSourceFiles: targetSourceFiles, }) return &EmitResponse{ diff --git a/internal/compiler/emitter.go b/internal/compiler/emitter.go index beb1ce2f5a1..30348df415e 100644 --- a/internal/compiler/emitter.go +++ b/internal/compiler/emitter.go @@ -503,14 +503,11 @@ func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host SourceFileMayBeEmit return true } -func getSourceFilesToEmit(host SourceFileMayBeEmittedHost, targetSourceFile *ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile { - var sourceFiles []*ast.SourceFile - if targetSourceFile != nil { - sourceFiles = []*ast.SourceFile{targetSourceFile} - } else { - sourceFiles = host.SourceFiles() +func getSourceFilesToEmit(host SourceFileMayBeEmittedHost, targetSourceFiles []*ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile { + if targetSourceFiles == nil { + targetSourceFiles = host.SourceFiles() } - return core.Filter(sourceFiles, func(sourceFile *ast.SourceFile) bool { + return core.Filter(targetSourceFiles, func(sourceFile *ast.SourceFile) bool { return sourceFileMayBeEmitted(sourceFile, host, forceDtsEmit) }) } @@ -521,7 +518,7 @@ func isSourceFileNotJson(file *ast.SourceFile) bool { func getDeclarationDiagnostics(host EmitHost, file *ast.SourceFile) []*ast.Diagnostic { // TODO: use p.getSourceFilesToEmit cache - fullFiles := core.Filter(getSourceFilesToEmit(host, file, false), isSourceFileNotJson) + fullFiles := core.Filter(getSourceFilesToEmit(host, core.SingleElementSlice(file), false), isSourceFileNotJson) if !core.Some(fullFiles, func(f *ast.SourceFile) bool { return f == file }) { return []*ast.Diagnostic{} } diff --git a/internal/compiler/program.go b/internal/compiler/program.go index c59d7131d70..bb17b107cba 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -716,14 +716,14 @@ func (p *Program) canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile) return isPlainJS || isCheckJS || sourceFile.ScriptKind == core.ScriptKindDeferred } -func (p *Program) getSourceFilesToEmit(targetSourceFile *ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile { - if targetSourceFile == nil && !forceDtsEmit { +func (p *Program) getSourceFilesToEmit(targetSourceFiles []*ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile { + if targetSourceFiles == nil && !forceDtsEmit { p.sourceFilesToEmitOnce.Do(func() { p.sourceFilesToEmit = getSourceFilesToEmit(p, nil, false) }) return p.sourceFilesToEmit } - return getSourceFilesToEmit(p, targetSourceFile, forceDtsEmit) + return getSourceFilesToEmit(p, targetSourceFiles, forceDtsEmit) } func (p *Program) verifyCompilerOptions() { @@ -1613,9 +1613,9 @@ type WriteFileData struct { type WriteFile func(fileName string, text string, data *WriteFileData) error type EmitOptions struct { - TargetSourceFile *ast.SourceFile // Single file to emit. If `nil`, emits all files - EmitOnly EmitOnly - WriteFile WriteFile + TargetSourceFiles []*ast.SourceFile // Single file to emit. If `nil`, emits all files + EmitOnly EmitOnly + WriteFile WriteFile } type EmitResult struct { @@ -1640,7 +1640,7 @@ func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { result := HandleNoEmitOnError( ctx, p, - options.TargetSourceFile, + options.TargetSourceFiles, ) if result != nil || ctx.Err() != nil { return result @@ -1655,7 +1655,7 @@ func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { } wg := core.NewWorkGroup(p.SingleThreaded()) var emitters []*emitter - sourceFiles := p.getSourceFilesToEmit(options.TargetSourceFile, options.EmitOnly == EmitOnlyForcedDts) + sourceFiles := p.getSourceFilesToEmit(options.TargetSourceFiles, options.EmitOnly == EmitOnlyForcedDts) for _, sourceFile := range sourceFiles { emitter := &emitter{ @@ -1731,7 +1731,7 @@ type ProgramLike interface { Program() *Program } -func HandleNoEmitOnError(ctx context.Context, program ProgramLike, file *ast.SourceFile) *EmitResult { +func HandleNoEmitOnError(ctx context.Context, program ProgramLike, files []*ast.SourceFile) *EmitResult { if !program.Options().NoEmitOnError.IsTrue() { return nil // No emit on error is not set, so we can proceed with emitting } @@ -1739,7 +1739,7 @@ func HandleNoEmitOnError(ctx context.Context, program ProgramLike, file *ast.Sou diagnostics := GetDiagnosticsOfAnyProgram( ctx, program, - file, + files, true, program.GetBindDiagnostics, program.GetSemanticDiagnostics, @@ -1756,7 +1756,7 @@ func HandleNoEmitOnError(ctx context.Context, program ProgramLike, file *ast.Sou func GetDiagnosticsOfAnyProgram( ctx context.Context, program ProgramLike, - file *ast.SourceFile, + files []*ast.SourceFile, skipNoEmitCheckForDtsDiagnostics bool, getBindDiagnostics func(context.Context, *ast.SourceFile) []*ast.Diagnostic, getSemanticDiagnostics func(context.Context, *ast.SourceFile) []*ast.Diagnostic, @@ -1764,7 +1764,17 @@ func GetDiagnosticsOfAnyProgram( allDiagnostics := slices.Clip(program.GetConfigFileParsingDiagnostics()) configFileParsingDiagnosticsLength := len(allDiagnostics) - allDiagnostics = append(allDiagnostics, program.GetSyntacticDiagnostics(ctx, file)...) + appendDiagnosticsForAllFiles := func(diagnostics []*ast.Diagnostic, getDiagnostics func(context.Context, *ast.SourceFile) []*ast.Diagnostic) []*ast.Diagnostic { + if files == nil { + return append(diagnostics, getDiagnostics(ctx, nil)...) + } + for _, file := range files { + diagnostics = append(allDiagnostics, getDiagnostics(ctx, file)...) + } + return diagnostics + } + + allDiagnostics = appendDiagnosticsForAllFiles(allDiagnostics, program.GetSyntacticDiagnostics) // If we didn't have any syntactic errors, then also try getting the program (options), // global and semantic errors. @@ -1772,19 +1782,19 @@ func GetDiagnosticsOfAnyProgram( allDiagnostics = append(allDiagnostics, program.GetProgramDiagnostics()...) // Do binding early so we can track the time. - getBindDiagnostics(ctx, file) + appendDiagnosticsForAllFiles(nil, getBindDiagnostics) if program.Options().ListFilesOnly.IsFalseOrUnknown() { allDiagnostics = append(allDiagnostics, program.GetGlobalDiagnostics(ctx)...) if len(allDiagnostics) == configFileParsingDiagnosticsLength { - allDiagnostics = append(allDiagnostics, getSemanticDiagnostics(ctx, file)...) + allDiagnostics = appendDiagnosticsForAllFiles(allDiagnostics, getSemanticDiagnostics) // Ask for the global diagnostics again (they were empty above); we may have found new during checking, e.g. missing globals. allDiagnostics = append(allDiagnostics, program.GetGlobalDiagnostics(ctx)...) } if (skipNoEmitCheckForDtsDiagnostics || program.Options().NoEmit.IsTrue()) && program.Options().GetEmitDeclarations() && len(allDiagnostics) == configFileParsingDiagnosticsLength { - allDiagnostics = append(allDiagnostics, program.GetDeclarationDiagnostics(ctx, file)...) + allDiagnostics = appendDiagnosticsForAllFiles(allDiagnostics, program.GetDeclarationDiagnostics) } } } diff --git a/internal/execute/incremental/affectedfileshandler.go b/internal/execute/incremental/affectedfileshandler.go index 8eda5e47773..8008268e5f7 100644 --- a/internal/execute/incremental/affectedfileshandler.go +++ b/internal/execute/incremental/affectedfileshandler.go @@ -69,8 +69,8 @@ func (h *affectedFilesHandler) removeDiagnosticsOfLibraryFiles() { func (h *affectedFilesHandler) computeDtsSignature(file *ast.SourceFile) string { var signature string h.program.program.Emit(h.ctx, compiler.EmitOptions{ - TargetSourceFile: file, - EmitOnly: compiler.EmitOnlyForcedDts, + TargetSourceFiles: core.SingleElementSlice(file), + EmitOnly: compiler.EmitOnlyForcedDts, WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error { if !tspath.IsDeclarationFileName(fileName) { panic("File extension for signature expected to be dts, got : " + fileName) diff --git a/internal/execute/incremental/emitfileshandler.go b/internal/execute/incremental/emitfileshandler.go index 6bbd93cc9f8..2ab2d46167a 100644 --- a/internal/execute/incremental/emitfileshandler.go +++ b/internal/execute/incremental/emitfileshandler.go @@ -50,12 +50,14 @@ func (h *emitFilesHandler) emitAllAffectedFiles(options compiler.EmitOptions) *c if h.program.snapshot.canUseIncrementalState() { results := h.emitFilesIncremental(options) if h.isForDtsErrors { - if options.TargetSourceFile != nil { + if options.TargetSourceFiles != nil { // Result from cache - diagnostics, _ := h.program.snapshot.emitDiagnosticsPerFile.Load(options.TargetSourceFile.Path()) result := &compiler.EmitResult{ EmitSkipped: true, - Diagnostics: diagnostics.getDiagnostics(h.program.program, options.TargetSourceFile), + Diagnostics: core.FlatMap(options.TargetSourceFiles, func(targetFile *ast.SourceFile) []*ast.Diagnostic { + diagnostics, _ := h.program.snapshot.emitDiagnosticsPerFile.Load(targetFile.Path()) + return diagnostics.getDiagnostics(h.program.program, targetFile) + }), } h.updateHasEmitDiagnostics(result) return result @@ -78,9 +80,17 @@ func (h *emitFilesHandler) emitAllAffectedFiles(options compiler.EmitOptions) *c h.emitBuildInfo(options, result) return result } else { + var diagnostics []*ast.Diagnostic + if options.TargetSourceFiles == nil { + diagnostics = h.program.program.GetDeclarationDiagnostics(h.ctx, nil) + } else { + diagnostics = core.FlatMap(options.TargetSourceFiles, func(targetSourceFile *ast.SourceFile) []*ast.Diagnostic { + return h.program.program.GetDeclarationDiagnostics(h.ctx, targetSourceFile) + }) + } result := &compiler.EmitResult{ EmitSkipped: true, - Diagnostics: h.program.program.GetDeclarationDiagnostics(h.ctx, options.TargetSourceFile), + Diagnostics: diagnostics, } if len(result.Diagnostics) != 0 { h.updateHasEmitDiagnostics(result) @@ -136,9 +146,9 @@ func (h *emitFilesHandler) emitFilesIncremental(options compiler.EmitOptions) [] var result *compiler.EmitResult if !h.isForDtsErrors { result = h.program.program.Emit(h.ctx, h.getEmitOptions(compiler.EmitOptions{ - TargetSourceFile: affectedFile, - EmitOnly: emitOnly, - WriteFile: options.WriteFile, + TargetSourceFiles: core.SingleElementSlice(affectedFile), + EmitOnly: emitOnly, + WriteFile: options.WriteFile, })) } else { result = &compiler.EmitResult{ @@ -189,29 +199,29 @@ func (h *emitFilesHandler) getEmitOptions(options compiler.EmitOptions) compiler } canUseIncrementalState := h.program.snapshot.canUseIncrementalState() return compiler.EmitOptions{ - TargetSourceFile: options.TargetSourceFile, - EmitOnly: options.EmitOnly, + TargetSourceFiles: options.TargetSourceFiles, + EmitOnly: options.EmitOnly, WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error { var differsOnlyInMap bool if tspath.IsDeclarationFileName(fileName) { if canUseIncrementalState { var emitSignature string - info, _ := h.program.snapshot.fileInfos.Load(options.TargetSourceFile.Path()) + info, _ := h.program.snapshot.fileInfos.Load(data.SourceFile.Path()) if info.signature == info.version { - signature := h.program.snapshot.computeSignatureWithDiagnostics(options.TargetSourceFile, text, data) + signature := h.program.snapshot.computeSignatureWithDiagnostics(data.SourceFile, text, data) // With d.ts diagnostics they are also part of the signature so emitSignature will be different from it since its just hash of d.ts if len(data.Diagnostics) == 0 { emitSignature = signature } if signature != info.version { // Update it - h.signatures.Store(options.TargetSourceFile.Path(), signature) + h.signatures.Store(data.SourceFile.Path(), signature) } } // Store d.ts emit hash so later can be compared to check if d.ts has changed. // Currently we do this only for composite projects since these are the only projects that can be referenced by other projects // and would need their d.ts change time in --build mode - if h.skipDtsOutputOfComposite(options.TargetSourceFile, fileName, text, data, emitSignature, &differsOnlyInMap) { + if h.skipDtsOutputOfComposite(data.SourceFile, fileName, text, data, emitSignature, &differsOnlyInMap) { return nil } } @@ -328,7 +338,7 @@ func emitFiles(ctx context.Context, program *Program, options compiler.EmitOptio emitHandler := &emitFilesHandler{ctx: ctx, program: program, isForDtsErrors: isForDtsErrors} // Single file emit - do direct from program - if !isForDtsErrors && options.TargetSourceFile != nil { + if !isForDtsErrors && options.TargetSourceFiles != nil { result := program.program.Emit(ctx, emitHandler.getEmitOptions(options)) emitHandler.updateHasEmitDiagnostics(result) if ctx.Err() != nil { diff --git a/internal/execute/incremental/program.go b/internal/execute/incremental/program.go index d57db6fbd6f..81efb18f9ec 100644 --- a/internal/execute/incremental/program.go +++ b/internal/execute/incremental/program.go @@ -185,7 +185,7 @@ func (p *Program) getSemanticDiagnosticsOfFile(file *ast.SourceFile) []*ast.Diag func (p *Program) GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetDeclarationDiagnostics") result := emitFiles(ctx, p, compiler.EmitOptions{ - TargetSourceFile: file, + TargetSourceFiles: core.SingleElementSlice(file), }, true) if result != nil { return result.Diagnostics @@ -207,13 +207,13 @@ func (p *Program) Emit(ctx context.Context, options compiler.EmitOptions) *compi if p.snapshot.options.NoEmit.IsTrue() { result = &compiler.EmitResult{EmitSkipped: true} } else { - result = compiler.HandleNoEmitOnError(ctx, p, options.TargetSourceFile) + result = compiler.HandleNoEmitOnError(ctx, p, options.TargetSourceFiles) if ctx.Err() != nil { return nil } } if result != nil { - if options.TargetSourceFile != nil { + if options.TargetSourceFiles != nil { return result }