diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index d767720351e95..11633d0a016e0 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -3,6 +3,7 @@ import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; import { EmitOnly } from "#enums/emitOnly"; +import { FileEmitKind } from "#enums/fileEmitKind"; import { IndexKind } from "#enums/indexKind"; import { JsxEmit } from "#enums/jsxEmit"; import { ModuleKind } from "#enums/moduleKind"; @@ -75,8 +76,11 @@ import type { DocumentIdentifier, DocumentPosition, EmitOutputResponse as ProtocolEmitOutputResponse, + EmitResponse, FileNotifications, ImportAdderAction, + IncrementalOperationParams, + IncrementalStatusResponse, InferredProjectId, IntrinsicTypeMethod, LanguageServerSnapshotChanges as ProtocolLanguageServerSnapshotChanges, @@ -170,7 +174,7 @@ import type { export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; -export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; +export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, FileEmitKind, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { APIImportAdderAction as ImportAdderAction, APIOptions, @@ -520,7 +524,8 @@ export class API implements FormatDiagnosticsHo createSnapshot< const CreatePrograms extends CreateSnapshotParams["createPrograms"] = undefined, const OpenFiles extends CreateSnapshotParams["openFiles"] = undefined, - >(params: SnapshotOperationParams): Promise>; + const IncrementalOperations extends CreateSnapshotParams["incrementalOperations"] = undefined, + >(params: SnapshotOperationParams): Promise>; createSnapshot(): Promise; async createSnapshot(params?: CreateSnapshotParams): Promise { await this.ensureInitialized(); @@ -618,10 +623,11 @@ export class API implements FormatDiagnosticsHo getCurrentLanguageServerSnapshot< const CreatePrograms extends LanguageServerSnapshotChanges["createPrograms"] = undefined, const OpenFiles extends LanguageServerSnapshotChanges["openFiles"] = undefined, + const IncrementalOperations extends LanguageServerSnapshotChanges["incrementalOperations"] = undefined, >( - ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] + ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] - ): Promise>; + ): Promise>; getCurrentLanguageServerSnapshot( ...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] ): Promise; @@ -770,6 +776,33 @@ export class API implements FormatDiagnosticsHo program.setOwnedSnapshot(snapshot); return program; } + + /** + * Creates a program that restores persistent diagnostic and emit state from + * the build info file configured by `tsBuildInfoFile`. + */ + async createIncrementalProgram( + rootFiles: readonly DocumentIdentifier[], + compilerOptions: CompilerOptions, + createProgramOptions?: CreateProgramOptions, + ): Promise { + await this.ensureInitialized(); + + const snapshot = await this.createSnapshot({ + createPrograms: [{ rootFiles, compilerOptions, options: createProgramOptions, incremental: true }], + }); + const program = snapshot.operation.createdPrograms![0]; + if (!program) { + await snapshot.dispose(); + throw new Error("createIncrementalProgram did not return a project"); + } + if (!(program instanceof IncrementalProgram)) { + await snapshot.dispose(); + throw new Error("createIncrementalProgram did not return an incremental program"); + } + program.setOwnedSnapshot(snapshot); + return program; + } } type EnsureInitialized = () => Promise; // @sync: type EnsureInitialized = (() => void) & { gen(): Generator; }; @@ -843,6 +876,12 @@ type SnapshotUpdater = (params: CreateSnapshotParams) => Promise; // @ export interface SnapshotOperation { readonly createdPrograms?: readonly Program[] | undefined; readonly openedFiles?: readonly SnapshotOpenedFileOperation[] | undefined; + readonly incrementalOperations?: readonly IncrementalOperationResult[] | undefined; +} + +export interface IncrementalOperationResult { + readonly program: IncrementalProgram; + readonly result: EmitResult; } export interface SnapshotOpenedFileOperation { @@ -870,12 +909,18 @@ type ContextualizeTuple< /** Substitutes the operation arrays with contextually typed, tuple-preserving versions. */ type SnapshotOperationParams< - Params extends { createPrograms?: readonly unknown[] | undefined; openFiles?: readonly unknown[] | undefined; }, + Params extends { + createPrograms?: readonly unknown[] | undefined; + openFiles?: readonly unknown[] | undefined; + incrementalOperations?: readonly unknown[] | undefined; + }, CreatePrograms extends Params["createPrograms"], OpenFiles extends Params["openFiles"], -> = Omit & { + IncrementalOperations extends Params["incrementalOperations"], +> = Omit & { createPrograms?: ContextualizeTuple | undefined; openFiles?: ContextualizeTuple | undefined; + incrementalOperations?: ContextualizeTuple | undefined; }; /** @@ -885,17 +930,20 @@ type SnapshotOperationParams< type SnapshotForOperationResults< CreatePrograms extends readonly unknown[] | undefined, OpenFiles extends readonly unknown[] | undefined, + IncrementalOperations extends readonly unknown[] | undefined, > = Snapshot & { readonly operation: & SnapshotOperation & (CreatePrograms extends readonly unknown[] ? { readonly createdPrograms: MapTupleTo>; } : unknown) - & (OpenFiles extends readonly unknown[] ? { readonly openedFiles: MapTupleTo; } : unknown); + & (OpenFiles extends readonly unknown[] ? { readonly openedFiles: MapTupleTo; } : unknown) + & (IncrementalOperations extends readonly unknown[] ? { readonly incrementalOperations: MapTupleTo; } : unknown); }; /** Derives the refined snapshot result type from a complete operation parameter type. */ export type SnapshotForOperation = SnapshotForOperationResults< Params extends { createPrograms: infer CreatePrograms extends readonly unknown[]; } ? CreatePrograms : undefined, - Params extends { openFiles: infer OpenFiles extends readonly unknown[]; } ? OpenFiles : undefined + Params extends { openFiles: infer OpenFiles extends readonly unknown[]; } ? OpenFiles : undefined, + Params extends { incrementalOperations: infer IncrementalOperations extends readonly unknown[]; } ? IncrementalOperations : undefined >; export class Snapshot { @@ -939,13 +987,20 @@ export class Snapshot { this.snapshotRegistry = new SnapshotObjectRegistry(client, this.id, projectId => this.projectMap.get(projectId)); for (const projData of this.projectDataMap.values()) { - const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry); + const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry, this.updateSnapshot); this.projectMap.set(projData.id, project); } this.operation = { createdPrograms: data.operation.createdPrograms?.map(projectId => this.requireProject(projectId).program), openedFiles: data.operation.openedFiles?.map(result => ({ project: this.requireProject(result.project) })), + incrementalOperations: data.operation.incrementalOperations?.map(operation => { + const program = this.requireProject(operation.program).program; + if (!(program instanceof IncrementalProgram)) { + throw new Error(`Snapshot operation returned non-incremental program '${operation.program}'`); + } + return { program, result: toEmitResult(operation.result) }; + }), }; this.internal = new SnapshotInternalAPI(this.id, client); @@ -973,7 +1028,8 @@ export class Snapshot { update< const CreatePrograms extends CreateSnapshotParams["createPrograms"] = undefined, const OpenFiles extends CreateSnapshotParams["openFiles"] = undefined, - >(params: SnapshotOperationParams): Promise>; + const IncrementalOperations extends CreateSnapshotParams["incrementalOperations"] = undefined, + >(params: SnapshotOperationParams): Promise>; update(params: CreateSnapshotParams): Promise; update(params: CreateSnapshotParams): Promise { this.ensureNotDisposed(); @@ -1390,6 +1446,7 @@ export class Project { toPath: (fileName: string) => Path, formatDiagnosticsHost: FormatDiagnosticsHost, snapshotRegistry: SnapshotObjectRegistry, + updateSnapshot: SnapshotUpdater, ) { this.id = data.id as Id; this.configFileName = data.configFileName; @@ -1403,14 +1460,32 @@ export class Project { this.rootFiles = this.parsedCommandLine.fileNames; this.client = client; this.snapshotId = snapshotId; - this.program = new Program( - snapshotId, - this, - client, - sourceFileCache, - toPath, - formatDiagnosticsHost, - ); + if (data.incremental) { + if (!data.incrementalStatus) { + throw new Error(`Incremental project '${data.id}' has no incremental status`); + } + this.program = new IncrementalProgram( + snapshotId, + this, + client, + sourceFileCache, + toPath, + formatDiagnosticsHost, + updateSnapshot, + toIncrementalStatus(data.dirty, data.incrementalStatus), + ); + } + else { + this.program = new Program( + snapshotId, + this, + client, + sourceFileCache, + toPath, + formatDiagnosticsHost, + updateSnapshot, + ); + } const objectRegistry = new ProjectObjectRegistry(client, snapshotId, this, snapshotRegistry); this.checker = new Checker( snapshotId, @@ -1550,10 +1625,11 @@ export class Program implements FormatDiagnost readonly snapshotId: number; readonly id: Id; private readonly project: Project; - private readonly client: Client; + protected readonly client: Client; private readonly sourceFileCache: SourceFileCache; private readonly toPath: (fileName: string) => Path; private readonly formatDiagnosticsHost: FormatDiagnosticsHost; + protected readonly updateSnapshot: SnapshotUpdater; private readonly decoder = new Wtf8Decoder(); private readonly sourceFileMetadataCache = new Map>(); private ownedSnapshot: Snapshot | undefined; @@ -1566,6 +1642,7 @@ export class Program implements FormatDiagnost sourceFileCache: SourceFileCache, toPath: (fileName: string) => Path, formatDiagnosticsHost: FormatDiagnosticsHost, + updateSnapshot: SnapshotUpdater, ) { this.snapshotId = snapshotId; this.id = project.id; @@ -1574,6 +1651,7 @@ export class Program implements FormatDiagnost this.sourceFileCache = sourceFileCache; this.toPath = toPath; this.formatDiagnosticsHost = formatDiagnosticsHost; + this.updateSnapshot = updateSnapshot; } getCurrentDirectory(): string { @@ -1929,18 +2007,7 @@ export class Program implements FormatDiagnost project: this.project.id, emitOnly, }); - const fileSystem = response.emittedFilesContents.length - ? { - kind: "layer" as const, - files: Object.fromEntries(response.emittedFiles.map((fileName, index) => [fileName, response.emittedFilesContents[index]])), - } - : undefined; - return { - emitSkipped: response.emitSkipped, - diagnostics: response.diagnostics, - emittedFiles: response.emittedFiles, - fileSystem, - }; + return toEmitResult(response); } /** @@ -1984,6 +2051,120 @@ export class Program implements FormatDiagnost } } +export class IncrementalProgram extends Program { + readonly status: IncrementalStatus; + + constructor( + snapshotId: number, + project: Project, + client: Client, + sourceFileCache: SourceFileCache, + toPath: (fileName: string) => Path, + formatDiagnosticsHost: FormatDiagnosticsHost, + updateSnapshot: SnapshotUpdater, + status: IncrementalStatus, + ) { + super(snapshotId, project, client, sourceFileCache, toPath, formatDiagnosticsHost, updateSnapshot); + this.status = status; + } + + /** + * Emits pending files and returns the new snapshot containing the advanced incremental state. + */ + override async emit(emitOnly?: EmitOnly): Promise { + return this.runIncrementalOperation({ + program: this.id as SyntheticProjectId, + kind: "emit", + emitOnly, + }); + } + + /** + * Writes the current incremental build information and returns the new snapshot + * containing the advanced incremental state. + */ + async emitBuildInfo(): Promise { + return this.runIncrementalOperation({ + program: this.id as SyntheticProjectId, + kind: "emitBuildInfo", + }); + } + + /** + * Returns the current serialized incremental build information without writing it. + */ + async getBuildInfoEmit(): Promise { + return this.client.apiRequest("getBuildInfoEmit", { + snapshot: this.snapshotId, + project: this.id, + }); + } + + private async runIncrementalOperation(operation: IncrementalOperationParams): Promise { + const snapshot = await this.updateSnapshot({ + incrementalOperations: [operation], + }); + const result = snapshot.operation.incrementalOperations?.[0]; + if (!result) { + await snapshot.dispose(); + throw new Error("Snapshot update did not return an incremental operation result"); + } + return { + ...result.result, + snapshot, + program: result.program, + }; + } +} + +export interface IncrementalEmitResult extends EmitResult { + readonly snapshot: Snapshot; + readonly program: IncrementalProgram; +} + +export interface IncrementalPendingEmit { + readonly sourceFileName: string; + readonly kind: FileEmitKind; +} + +export interface IncrementalStatus { + readonly dirty: boolean; + readonly changedFiles: readonly string[]; + readonly pendingEmit: readonly IncrementalPendingEmit[]; + readonly pendingSemanticDiagnostics: readonly string[]; + readonly buildInfoEmitPending: boolean; + readonly latestChangedDtsFile?: string | undefined; +} + +function toIncrementalStatus(dirty: boolean, status: IncrementalStatusResponse): IncrementalStatus { + return { + dirty, + changedFiles: status.changedFiles, + pendingEmit: status.pendingEmit.map(emit => ({ + sourceFileName: emit.sourceFileName, + kind: emit.kind, + })), + pendingSemanticDiagnostics: status.pendingSemanticDiagnostics, + buildInfoEmitPending: status.buildInfoEmitPending, + latestChangedDtsFile: status.latestChangedDtsFile, + }; +} + +function toEmitResult(response: EmitResponse): EmitResult { + const fileSystem = response.emittedFilesContents.length + ? { + kind: "layer" as const, + files: Object.fromEntries(response.emittedFiles.map((fileName, index) => [fileName, response.emittedFilesContents[index]])), + } + : undefined; + return { + emitSkipped: response.emitSkipped, + diagnostics: response.diagnostics, + emittedFiles: response.emittedFiles, + fileSystem, + }; +} + export interface BuildOrchestratorOptions { cwd?: string | undefined; dry?: boolean; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 4dccbe902202c..9ca0b97f351a3 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -1,5 +1,6 @@ // Code generated by gen-proto; DO NOT EDIT. +import { FileEmitKind } from "#enums/fileEmitKind"; import { JsxEmit } from "#enums/jsxEmit"; import { ModuleDetectionKind } from "#enums/moduleDetectionKind"; import { ModuleKind } from "#enums/moduleKind"; @@ -9,6 +10,7 @@ import { ScriptKind } from "#enums/scriptKind"; import { ScriptTarget } from "#enums/scriptTarget"; import type { Path } from "../ast/index.ts"; +export { FileEmitKind } from "#enums/fileEmitKind"; export { JsxEmit } from "#enums/jsxEmit"; export { ModuleDetectionKind } from "#enums/moduleDetectionKind"; export { ModuleKind } from "#enums/moduleKind"; @@ -170,6 +172,7 @@ export interface APIMethodInfo { printNode: APIMethod; formatNodeForInsertion: APIMethod; emit: APIMethod; + getBuildInfoEmit: APIMethod; emitToString: APIMethod; getJavaScriptEmit: APIMethod; getDeclarationEmit: APIMethod; @@ -409,6 +412,8 @@ export interface ProjectResponse { configFileName: string; currentDirectory: string; dirty: boolean; + incremental: boolean; + incrementalStatus?: IncrementalStatusResponse | undefined; parsedCommandLine: ConfigFileResponse; /** @deprecated Use parsedCommandLine.fileNames. */ rootFiles: string[]; @@ -1112,6 +1117,7 @@ export interface BatchRequest { | "getBigIntType" | "getBindDiagnostics" | "getBooleanType" + | "getBuildInfoEmit" | "getCheckTypeOfType" | "getCompletionsAtPosition" | "getConfigFileNames" @@ -1287,6 +1293,7 @@ export interface BatchResponse { | "getBigIntType" | "getBindDiagnostics" | "getBooleanType" + | "getBuildInfoEmit" | "getCheckTypeOfType" | "getCompletionsAtPosition" | "getConfigFileNames" @@ -1470,6 +1477,8 @@ export interface SnapshotRequestChangesParams { * or all contained projects when true. */ ensurePrograms?: EnsurePrograms | undefined; + /** IncrementalOperations advances incremental program state while constructing the snapshot. */ + incrementalOperations?: readonly IncrementalOperationParams[] | undefined; } /** @@ -1528,6 +1537,7 @@ export interface SnapshotChanges { export interface SnapshotOperationResponse { createdPrograms?: SyntheticProjectId[] | undefined; openedFiles?: OpenedFileOperationResult[] | undefined; + incrementalOperations?: IncrementalOperationResultResponse[] | undefined; } /** @@ -1692,6 +1702,14 @@ export interface TranspileOptions { reportDiagnostics?: boolean | undefined; } +export interface IncrementalStatusResponse { + changedFiles: string[]; + pendingEmit: IncrementalPendingEmitResponse[]; + pendingSemanticDiagnostics: string[]; + buildInfoEmitPending: boolean; + latestChangedDtsFile?: string | undefined; +} + export interface PackageId { name: string; subModuleName: string; @@ -1737,6 +1755,8 @@ export interface CreateSnapshotProgramParams { rootFiles: readonly DocumentIdentifier[] | null; compilerOptions: CompilerOptions; options?: CreateProgramOptions | undefined; + /** Incremental restores persistent diagnostic and emit state from the configured build info file. */ + incremental?: boolean | undefined; } export interface ReconfigureSnapshotProgramParams { @@ -1746,6 +1766,12 @@ export interface ReconfigureSnapshotProgramParams { options?: CreateProgramOptions | undefined; } +export interface IncrementalOperationParams { + program: SyntheticProjectId; + kind: "emit" | "emitBuildInfo"; + emitOnly?: number | undefined; +} + /** * RequestDirectoryEntries is a cached directory listing. Entry names are * relative to the directory, matching vfs.GetAccessibleEntries. @@ -1781,6 +1807,11 @@ export interface OpenedFileOperationResult { project: ProjectId; } +export interface IncrementalOperationResultResponse { + program: ProjectId; + result: EmitResponse; +} + export interface PluginImport { name: string; } @@ -1792,6 +1823,11 @@ export interface ModuleResolutionEntry { result: StaticModuleResolution; } +export interface IncrementalPendingEmitResponse { + sourceFileName: string; + kind: FileEmitKind; +} + /** CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. */ export interface CompletionEntryLabelDetailsResponse { detail?: string | undefined; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index dcd00e40ee58d..ab77aeabdda05 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -20,6 +20,7 @@ import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; import { EmitOnly } from "#enums/emitOnly"; +import { FileEmitKind } from "#enums/fileEmitKind"; import { IndexKind } from "#enums/indexKind"; import { JsxEmit } from "#enums/jsxEmit"; import { ModuleKind } from "#enums/moduleKind"; @@ -92,8 +93,11 @@ import type { DocumentIdentifier, DocumentPosition, EmitOutputResponse as ProtocolEmitOutputResponse, + EmitResponse, FileNotifications, ImportAdderAction, + IncrementalOperationParams, + IncrementalStatusResponse, InferredProjectId, IntrinsicTypeMethod, LanguageServerSnapshotChanges as ProtocolLanguageServerSnapshotChanges, @@ -187,7 +191,7 @@ import type { export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diagnosticFormatter.ts"; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; -export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; +export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, FileEmitKind, IndexKind, JsxEmit, ModifierFlags, ModuleKind, ModuleResolutionKind, NodeBuilderFlags, ObjectFlags, ScriptKind, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind }; export type { APIImportAdderAction as ImportAdderAction, APIOptions, @@ -766,13 +770,13 @@ export class API implements FormatDiagnosticsHo } get createSnapshot(): { - (params: SnapshotOperationParams): SnapshotForOperationResults; + (params: SnapshotOperationParams): SnapshotForOperationResults; (): Snapshot; - gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; + gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; gen(): Generator; } { const owner = this; - function createSnapshot(params: SnapshotOperationParams): SnapshotForOperationResults; + function createSnapshot(params: SnapshotOperationParams): SnapshotForOperationResults; function createSnapshot(): Snapshot; function createSnapshot(params?: CreateSnapshotParams): Snapshot { owner.ensureInitialized(); @@ -797,7 +801,7 @@ export class API implements FormatDiagnosticsHo return snapshot; } - function gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; + function gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; function gen(): Generator; function* gen(params?: CreateSnapshotParams): Generator { yield* owner.ensureInitialized.gen(); @@ -936,22 +940,22 @@ export class API implements FormatDiagnosticsHo * adopting any supplied API-driven changes. Only available on LSP-connected APIs. */ get getCurrentLanguageServerSnapshot(): { - ( - ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] + ( + ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] - ): SnapshotForOperationResults; + ): SnapshotForOperationResults; (...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Snapshot; - gen( - ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] + gen( + ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] - ): Generator, ProtocolResponse["result"]>; + ): Generator, ProtocolResponse["result"]>; gen(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator; } { const owner = this; - function getCurrentLanguageServerSnapshot( - ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] + function getCurrentLanguageServerSnapshot( + ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] - ): SnapshotForOperationResults; + ): SnapshotForOperationResults; function getCurrentLanguageServerSnapshot(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Snapshot; function getCurrentLanguageServerSnapshot(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Snapshot { owner.ensureInitialized(); @@ -984,10 +988,10 @@ export class API implements FormatDiagnosticsHo owner.activeSnapshots.set(snapshot.id, snapshot); return snapshot; } - function gen( - ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] + function gen( + ...args: FromLSP extends true ? [changes: SnapshotOperationParams, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] - ): Generator, ProtocolResponse["result"]>; + ): Generator, ProtocolResponse["result"]>; function gen(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator; function* gen(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator { yield* owner.ensureInitialized.gen(); @@ -1269,6 +1273,57 @@ export class API implements FormatDiagnosticsHo }, ); } + + /** + * Creates a program that restores persistent diagnostic and emit state from + * the build info file configured by `tsBuildInfoFile`. + */ + get createIncrementalProgram(): { + (rootFiles: readonly DocumentIdentifier[], compilerOptions: CompilerOptions, createProgramOptions?: CreateProgramOptions): IncrementalProgram; + gen(rootFiles: readonly DocumentIdentifier[], compilerOptions: CompilerOptions, createProgramOptions?: CreateProgramOptions): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "createIncrementalProgram", + function (rootFiles: readonly DocumentIdentifier[], compilerOptions: CompilerOptions, createProgramOptions?: CreateProgramOptions): IncrementalProgram { + owner.ensureInitialized(); + + const snapshot = owner.createSnapshot({ + createPrograms: [{ rootFiles, compilerOptions, options: createProgramOptions, incremental: true }], + }); + const program = snapshot.operation.createdPrograms![0]; + if (!program) { + snapshot.dispose(); + throw new Error("createIncrementalProgram did not return a project"); + } + if (!(program instanceof IncrementalProgram)) { + snapshot.dispose(); + throw new Error("createIncrementalProgram did not return an incremental program"); + } + program.setOwnedSnapshot(snapshot); + return program; + }, + function* (rootFiles: readonly DocumentIdentifier[], compilerOptions: CompilerOptions, createProgramOptions?: CreateProgramOptions): Generator { + yield* owner.ensureInitialized.gen(); + + const snapshot = yield* owner.createSnapshot.gen({ + createPrograms: [{ rootFiles, compilerOptions, options: createProgramOptions, incremental: true }], + }); + const program = snapshot.operation.createdPrograms![0]; + if (!program) { + yield* snapshot.dispose.gen(); + throw new Error("createIncrementalProgram did not return a project"); + } + if (!(program instanceof IncrementalProgram)) { + yield* snapshot.dispose.gen(); + throw new Error("createIncrementalProgram did not return an incremental program"); + } + program.setOwnedSnapshot(snapshot); + return program; + }, + ); + } } type EnsureInitialized = (() => void) & { gen(): Generator; }; @@ -1419,6 +1474,12 @@ type SnapshotUpdater = ((params: CreateSnapshotParams) => Snapshot) & { gen(para export interface SnapshotOperation { readonly createdPrograms?: readonly Program[] | undefined; readonly openedFiles?: readonly SnapshotOpenedFileOperation[] | undefined; + readonly incrementalOperations?: readonly IncrementalOperationResult[] | undefined; +} + +export interface IncrementalOperationResult { + readonly program: IncrementalProgram; + readonly result: EmitResult; } export interface SnapshotOpenedFileOperation { @@ -1446,12 +1507,18 @@ type ContextualizeTuple< /** Substitutes the operation arrays with contextually typed, tuple-preserving versions. */ type SnapshotOperationParams< - Params extends { createPrograms?: readonly unknown[] | undefined; openFiles?: readonly unknown[] | undefined; }, + Params extends { + createPrograms?: readonly unknown[] | undefined; + openFiles?: readonly unknown[] | undefined; + incrementalOperations?: readonly unknown[] | undefined; + }, CreatePrograms extends Params["createPrograms"], OpenFiles extends Params["openFiles"], -> = Omit & { + IncrementalOperations extends Params["incrementalOperations"], +> = Omit & { createPrograms?: ContextualizeTuple | undefined; openFiles?: ContextualizeTuple | undefined; + incrementalOperations?: ContextualizeTuple | undefined; }; /** @@ -1461,17 +1528,20 @@ type SnapshotOperationParams< type SnapshotForOperationResults< CreatePrograms extends readonly unknown[] | undefined, OpenFiles extends readonly unknown[] | undefined, + IncrementalOperations extends readonly unknown[] | undefined, > = Snapshot & { readonly operation: & SnapshotOperation & (CreatePrograms extends readonly unknown[] ? { readonly createdPrograms: MapTupleTo>; } : unknown) - & (OpenFiles extends readonly unknown[] ? { readonly openedFiles: MapTupleTo; } : unknown); + & (OpenFiles extends readonly unknown[] ? { readonly openedFiles: MapTupleTo; } : unknown) + & (IncrementalOperations extends readonly unknown[] ? { readonly incrementalOperations: MapTupleTo; } : unknown); }; /** Derives the refined snapshot result type from a complete operation parameter type. */ export type SnapshotForOperation = SnapshotForOperationResults< Params extends { createPrograms: infer CreatePrograms extends readonly unknown[]; } ? CreatePrograms : undefined, - Params extends { openFiles: infer OpenFiles extends readonly unknown[]; } ? OpenFiles : undefined + Params extends { openFiles: infer OpenFiles extends readonly unknown[]; } ? OpenFiles : undefined, + Params extends { incrementalOperations: infer IncrementalOperations extends readonly unknown[]; } ? IncrementalOperations : undefined >; export class Snapshot { @@ -1515,13 +1585,20 @@ export class Snapshot { this.snapshotRegistry = new SnapshotObjectRegistry(client, this.id, projectId => this.projectMap.get(projectId)); for (const projData of this.projectDataMap.values()) { - const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry); + const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry, this.updateSnapshot); this.projectMap.set(projData.id, project); } this.operation = { createdPrograms: data.operation.createdPrograms?.map(projectId => this.requireProject(projectId).program), openedFiles: data.operation.openedFiles?.map(result => ({ project: this.requireProject(result.project) })), + incrementalOperations: data.operation.incrementalOperations?.map(operation => { + const program = this.requireProject(operation.program).program; + if (!(program instanceof IncrementalProgram)) { + throw new Error(`Snapshot operation returned non-incremental program '${operation.program}'`); + } + return { program, result: toEmitResult(operation.result) }; + }), }; this.internal = new SnapshotInternalAPI(this.id, client); @@ -1547,19 +1624,19 @@ export class Snapshot { } get update(): { - (params: SnapshotOperationParams): SnapshotForOperationResults; + (params: SnapshotOperationParams): SnapshotForOperationResults; (params: CreateSnapshotParams): Snapshot; - gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; + gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; gen(params: CreateSnapshotParams): Generator; } { const owner = this; - function update(params: SnapshotOperationParams): SnapshotForOperationResults; + function update(params: SnapshotOperationParams): SnapshotForOperationResults; function update(params: CreateSnapshotParams): Snapshot; function update(params: CreateSnapshotParams): Snapshot { owner.ensureNotDisposed(); return owner.updateSnapshot(params); } - function gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; + function gen(params: SnapshotOperationParams): Generator, ProtocolResponse["result"]>; function gen(params: CreateSnapshotParams): Generator; function* gen(params: CreateSnapshotParams): Generator { owner.ensureNotDisposed(); @@ -2382,6 +2459,7 @@ export class Project { toPath: (fileName: string) => Path, formatDiagnosticsHost: FormatDiagnosticsHost, snapshotRegistry: SnapshotObjectRegistry, + updateSnapshot: SnapshotUpdater, ) { this.id = data.id as Id; this.configFileName = data.configFileName; @@ -2395,14 +2473,32 @@ export class Project { this.rootFiles = this.parsedCommandLine.fileNames; this.client = client; this.snapshotId = snapshotId; - this.program = new Program( - snapshotId, - this, - client, - sourceFileCache, - toPath, - formatDiagnosticsHost, - ); + if (data.incremental) { + if (!data.incrementalStatus) { + throw new Error(`Incremental project '${data.id}' has no incremental status`); + } + this.program = new IncrementalProgram( + snapshotId, + this, + client, + sourceFileCache, + toPath, + formatDiagnosticsHost, + updateSnapshot, + toIncrementalStatus(data.dirty, data.incrementalStatus), + ); + } + else { + this.program = new Program( + snapshotId, + this, + client, + sourceFileCache, + toPath, + formatDiagnosticsHost, + updateSnapshot, + ); + } const objectRegistry = new ProjectObjectRegistry(client, snapshotId, this, snapshotRegistry); this.checker = new Checker( snapshotId, @@ -2703,10 +2799,11 @@ export class Program implements FormatDiagnost readonly snapshotId: number; readonly id: Id; private readonly project: Project; - private readonly client: Client; + protected readonly client: Client; private readonly sourceFileCache: SourceFileCache; private readonly toPath: (fileName: string) => Path; private readonly formatDiagnosticsHost: FormatDiagnosticsHost; + protected readonly updateSnapshot: SnapshotUpdater; private readonly decoder = new Wtf8Decoder(); private readonly sourceFileMetadataCache = new Map(); private ownedSnapshot: Snapshot | undefined; @@ -2719,6 +2816,7 @@ export class Program implements FormatDiagnost sourceFileCache: SourceFileCache, toPath: (fileName: string) => Path, formatDiagnosticsHost: FormatDiagnosticsHost, + updateSnapshot: SnapshotUpdater, ) { this.snapshotId = snapshotId; this.id = project.id; @@ -2727,6 +2825,7 @@ export class Program implements FormatDiagnost this.sourceFileCache = sourceFileCache; this.toPath = toPath; this.formatDiagnosticsHost = formatDiagnosticsHost; + this.updateSnapshot = updateSnapshot; } getCurrentDirectory(): string { @@ -3550,18 +3649,7 @@ export class Program implements FormatDiagnost project: owner.project.id, emitOnly, }); - const fileSystem = response.emittedFilesContents.length - ? { - kind: "layer" as const, - files: Object.fromEntries(response.emittedFiles.map((fileName, index) => [fileName, response.emittedFilesContents[index]])), - } - : undefined; - return { - emitSkipped: response.emitSkipped, - diagnostics: response.diagnostics, - emittedFiles: response.emittedFiles, - fileSystem, - }; + return toEmitResult(response); }, function* (emitOnly?: EmitOnly): Generator { const response = yield* apiRequest("emit", { @@ -3569,18 +3657,7 @@ export class Program implements FormatDiagnost project: owner.project.id, emitOnly, }); - const fileSystem = response.emittedFilesContents.length - ? { - kind: "layer" as const, - files: Object.fromEntries(response.emittedFiles.map((fileName, index) => [fileName, response.emittedFilesContents[index]])), - } - : undefined; - return { - emitSkipped: response.emitSkipped, - diagnostics: response.diagnostics, - emittedFiles: response.emittedFiles, - fileSystem, - }; + return toEmitResult(response); }, ); } @@ -3680,6 +3757,194 @@ export class Program implements FormatDiagnost } } +export class IncrementalProgram extends Program { + readonly status: IncrementalStatus; + + constructor( + snapshotId: number, + project: Project, + client: Client, + sourceFileCache: SourceFileCache, + toPath: (fileName: string) => Path, + formatDiagnosticsHost: FormatDiagnosticsHost, + updateSnapshot: SnapshotUpdater, + status: IncrementalStatus, + ) { + super(snapshotId, project, client, sourceFileCache, toPath, formatDiagnosticsHost, updateSnapshot); + this.status = status; + } + + /** + * Emits pending files and returns the new snapshot containing the advanced incremental state. + */ + override get emit(): { + (emitOnly?: EmitOnly): IncrementalEmitResult; + gen(emitOnly?: EmitOnly): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "emit", + function (emitOnly?: EmitOnly): IncrementalEmitResult { + return owner.runIncrementalOperation({ + program: owner.id as SyntheticProjectId, + kind: "emit", + emitOnly, + }); + }, + function* (emitOnly?: EmitOnly): Generator { + return yield* owner.runIncrementalOperation.gen({ + program: owner.id as SyntheticProjectId, + kind: "emit", + emitOnly, + }); + }, + ); + } + + /** + * Writes the current incremental build information and returns the new snapshot + * containing the advanced incremental state. + */ + get emitBuildInfo(): { + (): IncrementalEmitResult; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "emitBuildInfo", + function (): IncrementalEmitResult { + return owner.runIncrementalOperation({ + program: owner.id as SyntheticProjectId, + kind: "emitBuildInfo", + }); + }, + function* (): Generator { + return yield* owner.runIncrementalOperation.gen({ + program: owner.id as SyntheticProjectId, + kind: "emitBuildInfo", + }); + }, + ); + } + + /** + * Returns the current serialized incremental build information without writing it. + */ + get getBuildInfoEmit(): { + (): string; + gen(): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "getBuildInfoEmit", + function (): string { + return owner.client.apiRequest("getBuildInfoEmit", { + snapshot: owner.snapshotId, + project: owner.id, + }); + }, + function* (): Generator { + return yield* apiRequest("getBuildInfoEmit", { + snapshot: owner.snapshotId, + project: owner.id, + }); + }, + ); + } + + private get runIncrementalOperation(): { + (operation: IncrementalOperationParams): IncrementalEmitResult; + gen(operation: IncrementalOperationParams): Generator; + } { + const owner = this; + return cacheGeneratorMethod( + owner, + "runIncrementalOperation", + function (operation: IncrementalOperationParams): IncrementalEmitResult { + const snapshot = owner.updateSnapshot({ + incrementalOperations: [operation], + }); + const result = snapshot.operation.incrementalOperations?.[0]; + if (!result) { + snapshot.dispose(); + throw new Error("Snapshot update did not return an incremental operation result"); + } + return { + ...result.result, + snapshot, + program: result.program, + }; + }, + function* (operation: IncrementalOperationParams): Generator { + const snapshot = yield* owner.updateSnapshot.gen({ + incrementalOperations: [operation], + }); + const result = snapshot.operation.incrementalOperations?.[0]; + if (!result) { + yield* snapshot.dispose.gen(); + throw new Error("Snapshot update did not return an incremental operation result"); + } + return { + ...result.result, + snapshot, + program: result.program, + }; + }, + ); + } +} + +export interface IncrementalEmitResult extends EmitResult { + readonly snapshot: Snapshot; + readonly program: IncrementalProgram; +} + +export interface IncrementalPendingEmit { + readonly sourceFileName: string; + readonly kind: FileEmitKind; +} + +export interface IncrementalStatus { + readonly dirty: boolean; + readonly changedFiles: readonly string[]; + readonly pendingEmit: readonly IncrementalPendingEmit[]; + readonly pendingSemanticDiagnostics: readonly string[]; + readonly buildInfoEmitPending: boolean; + readonly latestChangedDtsFile?: string | undefined; +} + +function toIncrementalStatus(dirty: boolean, status: IncrementalStatusResponse): IncrementalStatus { + return { + dirty, + changedFiles: status.changedFiles, + pendingEmit: status.pendingEmit.map(emit => ({ + sourceFileName: emit.sourceFileName, + kind: emit.kind, + })), + pendingSemanticDiagnostics: status.pendingSemanticDiagnostics, + buildInfoEmitPending: status.buildInfoEmitPending, + latestChangedDtsFile: status.latestChangedDtsFile, + }; +} + +function toEmitResult(response: EmitResponse): EmitResult { + const fileSystem = response.emittedFilesContents.length + ? { + kind: "layer" as const, + files: Object.fromEntries(response.emittedFiles.map((fileName, index) => [fileName, response.emittedFilesContents[index]])), + } + : undefined; + return { + emitSkipped: response.emitSkipped, + diagnostics: response.diagnostics, + emittedFiles: response.emittedFiles, + fileSystem, + }; +} + export interface BuildOrchestratorOptions { cwd?: string | undefined; dry?: boolean; diff --git a/packages/typescript/src/enums/fileEmitKind.enum.ts b/packages/typescript/src/enums/fileEmitKind.enum.ts new file mode 100644 index 0000000000000..6c57c04b83a50 --- /dev/null +++ b/packages/typescript/src/enums/fileEmitKind.enum.ts @@ -0,0 +1,11 @@ +// Code generated by tools/scripts/tsc/generate-enums.ts from tsc/internal/execute/incremental/snapshot.go. DO NOT EDIT. + +export enum FileEmitKind { + None = 0, + Js = 1 << 0, + JsMap = 1 << 1, + JsInlineMap = 1 << 2, + DtsErrors = 1 << 3, + DtsEmit = 1 << 4, + DtsMap = 1 << 5, +} diff --git a/packages/typescript/src/enums/fileEmitKind.ts b/packages/typescript/src/enums/fileEmitKind.ts new file mode 100644 index 0000000000000..25f819bb61cf7 --- /dev/null +++ b/packages/typescript/src/enums/fileEmitKind.ts @@ -0,0 +1,11 @@ +// Code generated by tools/scripts/tsc/generate-enums.ts from tsc/internal/execute/incremental/snapshot.go. DO NOT EDIT. +export var FileEmitKind: any; +(function (FileEmitKind) { + FileEmitKind[FileEmitKind["None"] = 0] = "None"; + FileEmitKind[FileEmitKind["Js"] = 1] = "Js"; + FileEmitKind[FileEmitKind["JsMap"] = 2] = "JsMap"; + FileEmitKind[FileEmitKind["JsInlineMap"] = 4] = "JsInlineMap"; + FileEmitKind[FileEmitKind["DtsErrors"] = 8] = "DtsErrors"; + FileEmitKind[FileEmitKind["DtsEmit"] = 16] = "DtsEmit"; + FileEmitKind[FileEmitKind["DtsMap"] = 32] = "DtsMap"; +})(FileEmitKind || (FileEmitKind = {})); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 3226d409e17a8..8153f4d4780fe 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -58,8 +58,10 @@ import { DiagnosticCategory, type DocumentIdentifier, EmitOnly, + FileEmitKind, type FreshableType, type ImportAdderAction, + IncrementalProgram, type IndexedAccessType, IndexKind, type IndexType, @@ -999,6 +1001,166 @@ declare module "augmentation" {}`, await program.dispose(); }); + test("createIncrementalProgram restores build info and emits only affected files", async () => { + const fs = createVirtualFileSystem({ + "/src/main.ts": `import { value } from "./dependency"; export const result = value();`, + "/src/dependency.ts": `export function value() { return 1; }`, + }); + await using api = new API({ + cwd: "/", + fs, + }); + const options = { + declaration: true, + incremental: true, + noLib: true, + outDir: "/out", + rootDir: "/src", + tsBuildInfoFile: "/out/build.tsbuildinfo", + }; + + const firstProgram = await api.createIncrementalProgram(["/src/main.ts"], options); + const initialStatus = firstProgram.status; + assert.equal(initialStatus.dirty, false); + assert.equal(initialStatus.buildInfoEmitPending, true); + assert.deepEqual(initialStatus.changedFiles, []); + assert.deepEqual( + initialStatus.pendingEmit.map(emit => emit.sourceFileName), + ["/src/dependency.ts", "/src/main.ts"], + ); + assert.deepEqual( + initialStatus.pendingSemanticDiagnostics, + ["/src/dependency.ts", "/src/main.ts"], + ); + const buildInfoText = await firstProgram.getBuildInfoEmit(); + assert.equal(typeof JSON.parse(buildInfoText).version, "string"); + assert.equal(fs.readFile!("/out/build.tsbuildinfo"), undefined); + const buildInfoEmit = await firstProgram.emitBuildInfo(); + using buildInfoSnapshot = buildInfoEmit.snapshot; + assert.deepEqual(buildInfoEmit.emittedFiles, ["/out/build.tsbuildinfo"]); + assert.equal(buildInfoEmit.emitSkipped, false); + assert.deepEqual(buildInfoEmit.diagnostics, []); + assert.equal(fs.readFile!("/out/build.tsbuildinfo"), buildInfoText); + assert.equal(firstProgram.status.buildInfoEmitPending, true); + assert.equal(buildInfoEmit.program.status.buildInfoEmitPending, false); + + const firstEmit = await firstProgram.emit(); + using firstEmitSnapshot = firstEmit.snapshot; + assert.ok(firstEmit.emittedFiles.includes("/out/main.js")); + assert.ok(firstEmit.emittedFiles.includes("/out/dependency.js")); + assert.ok(firstEmit.emittedFiles.includes("/out/build.tsbuildinfo")); + assert.ok(fs.readFile!("/out/build.tsbuildinfo")); + assert.deepEqual(firstProgram.status, initialStatus); + assert.equal(firstEmit.program.status.dirty, false); + assert.equal(firstEmit.program.status.buildInfoEmitPending, false); + assert.deepEqual(firstEmit.program.status.changedFiles, []); + assert.deepEqual(firstEmit.program.status.pendingEmit, []); + const repeatedEmit = await firstEmit.program.emit(); + using repeatedEmitSnapshot = repeatedEmit.snapshot; + assert.deepEqual(repeatedEmit.emittedFiles, []); + const emitToString = await firstEmit.program.emitToString(); + assert.deepEqual([...emitToString.outputFiles.keys()].sort(), [ + "/out/dependency.d.ts", + "/out/dependency.js", + "/out/main.d.ts", + "/out/main.js", + ]); + await firstProgram.dispose(); + + fs.writeFile!("/src/dependency.ts", `export function value() { return 2; }`); + const secondProgram = await api.createIncrementalProgram(["/src/main.ts"], options); + const globalDiagnostics = await secondProgram.getGlobalDiagnostics(); + assert.ok(globalDiagnostics.some(diagnostic => diagnostic.code === 2318)); + const secondEmit = await secondProgram.emit(); + using secondEmitSnapshot = secondEmit.snapshot; + assert.ok(!secondEmit.emittedFiles.includes("/out/main.js"), JSON.stringify(secondEmit.emittedFiles)); + assert.ok(secondEmit.emittedFiles.includes("/out/dependency.js")); + assert.ok(secondEmit.emittedFiles.includes("/out/build.tsbuildinfo")); + await secondProgram.dispose(); + }); + + test("emitBuildInfo returns a filesystem layer for a full filesystem", async () => { + const hostWrites: string[] = []; + await using api = new API({ + cwd: "/", + fs: { + writeFile: path => { + hostWrites.push(path); + }, + }, + }); + using snapshot = await api.createSnapshot({ + fileSystem: createFileSystem(Object.entries({ + "/src/index.ts": `export const value = 1;`, + })), + createPrograms: [{ + rootFiles: ["/src/index.ts"], + compilerOptions: { + incremental: true, + noLib: true, + tsBuildInfoFile: "/out/build.tsbuildinfo", + }, + incremental: true, + }], + }); + const program = snapshot.operation.createdPrograms[0]; + assert.ok(program instanceof IncrementalProgram); + using dirtySnapshot = await snapshot.update({ + fileSystem: createFileSystemLayer([["/src/index.ts", `export const value = 2;`]]), + }); + const dirtyProgram = dirtySnapshot.getProgram(program.id); + assert.ok(dirtyProgram instanceof IncrementalProgram); + assert.equal(dirtyProgram.status.dirty, true); + assert.deepEqual( + { ...dirtyProgram.status, dirty: false }, + program.status, + ); + + const buildInfoText = await program.getBuildInfoEmit(); + const result = await program.emitBuildInfo(); + using emittedSnapshot = result.snapshot; + assert.deepEqual(result.emittedFiles, ["/out/build.tsbuildinfo"]); + assert.deepEqual(result.fileSystem, { + kind: "layer", + files: { + "/out/build.tsbuildinfo": buildInfoText, + }, + }); + assert.deepEqual(hostWrites, []); + }); + + test("incremental emitOnly preserves other pending output kinds", async () => { + const fs = createVirtualFileSystem({ + "/src/index.ts": `export const value = 1;`, + }); + await using api = new API({ cwd: "/", fs }); + const program = await api.createIncrementalProgram( + ["/src/index.ts"], + { + declaration: true, + incremental: true, + noLib: true, + outDir: "/out", + rootDir: "/src", + tsBuildInfoFile: "/out/build.tsbuildinfo", + }, + ); + + const jsEmit = await program.emit(EmitOnly.OnlyJs); + using jsSnapshot = jsEmit.snapshot; + assert.ok(jsEmit.emittedFiles.includes("/out/index.js")); + assert.ok(!jsEmit.emittedFiles.includes("/out/index.d.ts")); + assert.ok(jsEmit.program.status.pendingEmit.some(emit => emit.sourceFileName === "/src/index.ts" && (emit.kind & FileEmitKind.DtsEmit) !== 0)); + assert.ok(jsEmit.program.status.pendingEmit.every(emit => (emit.kind & FileEmitKind.Js) === 0)); + + const dtsEmit = await jsEmit.program.emit(EmitOnly.OnlyDts); + using dtsSnapshot = dtsEmit.snapshot; + assert.ok(!dtsEmit.emittedFiles.includes("/out/index.js")); + assert.ok(dtsEmit.emittedFiles.includes("/out/index.d.ts")); + assert.deepEqual(dtsEmit.program.status.pendingEmit, []); + await program.dispose(); + }); + test("createProgram includes project references", async () => { const reference = { path: "/lib/tsconfig.json", originalPath: "/lib/tsconfig.json", circular: false }; await using api = spawnAPI({ diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index b0e38d61e9ed8..669b4215d6a11 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -1409,6 +1409,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => const project = snapshot.getConfiguredProject("/tsconfig.json")!; const { checker, languageService, program } = project; const { printer } = api; + const incrementalProgram = api.createIncrementalProgram([], { incremental: true, noLib: true, tsBuildInfoFile: "/build.tsbuildinfo" }); const indexFile = program.getSourceFile("/src/index.ts")!; const modelsFile = program.getSourceFile("/src/models.ts")!; @@ -1568,6 +1569,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("API", "transpileDeclarationFromFile", api.transpileDeclarationFromFile, assertDeepEquivalent, "/src/index.ts"), parityCase("API", "createSnapshot", api.createSnapshot as GeneratorMethod<[params: { openProject: string; }], Snapshot>, assertSnapshotsEquivalent, { openProject: "/tsconfig.json" }), parityCase("API", "createProgram", api.createProgram, assertProgramsEquivalent, ["/src/index.ts"], { noLib: true }), + parityCase("API", "createIncrementalProgram", api.createIncrementalProgram, assertProgramsEquivalent, [], { incremental: true, noLib: true, tsBuildInfoFile: "/build.tsbuildinfo" }), parityCase("API", "createBuildOrchestrator", api.createBuildOrchestrator, assertBuildOrchestratorsEquivalent, ["/tsconfig.json"], { cwd: "/" }), parityCase("API", "runWithTemporaryFileUpdate", api.runWithTemporaryFileUpdate, assertDeepEquivalent, snapshot, "/src/index.ts", parityFiles["/src/index.ts"].replace("123", '"fixed"'), (temporarySnapshot: Snapshot) => { temporaryProjects.push(temporarySnapshot.getProjects()[0].configFileName); @@ -1615,6 +1617,7 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => parityCase("Program", "emitToString", program.emitToString, assertDeepEquivalent), parityCase("Program", "getJavaScriptEmit", program.getJavaScriptEmit, assertDeepEquivalent, ["/src/index.ts"]), parityCase("Program", "getDeclarationEmit", program.getDeclarationEmit, assertDeepEquivalent, ["/src/index.ts"]), + parityCase("IncrementalProgram", "getBuildInfoEmit", incrementalProgram.getBuildInfoEmit, assertDeepEquivalent), parityCase("Checker", "getSymbolAtLocation", selectGeneratorMethod<[node: Node], Symbol | undefined>(checker.getSymbolAtLocation), assertOptionalSymbolsEquivalent, importedDerived), parityCase("Checker", "getSymbolAtLocation", checker.getSymbolAtLocation, assertOptionalSymbolArraysEquivalent, [importedDerived, combineDeclaration.name!]), @@ -1767,6 +1770,23 @@ describe("API - generator batching", { concurrency: areTestsFiltered() }, () => runParityBatch(api, cases); assert.deepEqual(temporaryProjects, ["/tsconfig.json", "/tsconfig.json"]); + for ( + const [methodName, method] of [ + ["emit", incrementalProgram.emit], + ["emitBuildInfo", incrementalProgram.emitBuildInfo], + ] as const + ) { + const generated = api.batch(method.gen())[0]; + const direct = method(); + assert.deepEqual(generated.emittedFiles, direct.emittedFiles); + assert.deepEqual(generated.diagnostics, direct.diagnostics); + assert.deepEqual(generated.fileSystem, direct.fileSystem); + assertProgramsEquivalent(generated.program, direct.program); + generated.snapshot.dispose(); + direct.snapshot.dispose(); + exercisedMethods.add(`IncrementalProgram.${methodName}`); + } + const snapshotGeneratorAPI = spawnAPI(parityFiles); const snapshotSyncAPI = spawnAPI(parityFiles); try { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index aaa550e6ba456..24e348110580c 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -73,8 +73,10 @@ import { DiagnosticCategory, type DocumentIdentifier, EmitOnly, + FileEmitKind, type FreshableType, type ImportAdderAction, + IncrementalProgram, type IndexedAccessType, IndexKind, type IndexType, @@ -987,6 +989,166 @@ declare module "augmentation" {}`, program.dispose(); }); + test("createIncrementalProgram restores build info and emits only affected files", () => { + const fs = createVirtualFileSystem({ + "/src/main.ts": `import { value } from "./dependency"; export const result = value();`, + "/src/dependency.ts": `export function value() { return 1; }`, + }); + using api = new API({ + cwd: "/", + fs, + }); + const options = { + declaration: true, + incremental: true, + noLib: true, + outDir: "/out", + rootDir: "/src", + tsBuildInfoFile: "/out/build.tsbuildinfo", + }; + + const firstProgram = api.createIncrementalProgram(["/src/main.ts"], options); + const initialStatus = firstProgram.status; + assert.equal(initialStatus.dirty, false); + assert.equal(initialStatus.buildInfoEmitPending, true); + assert.deepEqual(initialStatus.changedFiles, []); + assert.deepEqual( + initialStatus.pendingEmit.map(emit => emit.sourceFileName), + ["/src/dependency.ts", "/src/main.ts"], + ); + assert.deepEqual( + initialStatus.pendingSemanticDiagnostics, + ["/src/dependency.ts", "/src/main.ts"], + ); + const buildInfoText = firstProgram.getBuildInfoEmit(); + assert.equal(typeof JSON.parse(buildInfoText).version, "string"); + assert.equal(fs.readFile!("/out/build.tsbuildinfo"), undefined); + const buildInfoEmit = firstProgram.emitBuildInfo(); + using buildInfoSnapshot = buildInfoEmit.snapshot; + assert.deepEqual(buildInfoEmit.emittedFiles, ["/out/build.tsbuildinfo"]); + assert.equal(buildInfoEmit.emitSkipped, false); + assert.deepEqual(buildInfoEmit.diagnostics, []); + assert.equal(fs.readFile!("/out/build.tsbuildinfo"), buildInfoText); + assert.equal(firstProgram.status.buildInfoEmitPending, true); + assert.equal(buildInfoEmit.program.status.buildInfoEmitPending, false); + + const firstEmit = firstProgram.emit(); + using firstEmitSnapshot = firstEmit.snapshot; + assert.ok(firstEmit.emittedFiles.includes("/out/main.js")); + assert.ok(firstEmit.emittedFiles.includes("/out/dependency.js")); + assert.ok(firstEmit.emittedFiles.includes("/out/build.tsbuildinfo")); + assert.ok(fs.readFile!("/out/build.tsbuildinfo")); + assert.deepEqual(firstProgram.status, initialStatus); + assert.equal(firstEmit.program.status.dirty, false); + assert.equal(firstEmit.program.status.buildInfoEmitPending, false); + assert.deepEqual(firstEmit.program.status.changedFiles, []); + assert.deepEqual(firstEmit.program.status.pendingEmit, []); + const repeatedEmit = firstEmit.program.emit(); + using repeatedEmitSnapshot = repeatedEmit.snapshot; + assert.deepEqual(repeatedEmit.emittedFiles, []); + const emitToString = firstEmit.program.emitToString(); + assert.deepEqual([...emitToString.outputFiles.keys()].sort(), [ + "/out/dependency.d.ts", + "/out/dependency.js", + "/out/main.d.ts", + "/out/main.js", + ]); + firstProgram.dispose(); + + fs.writeFile!("/src/dependency.ts", `export function value() { return 2; }`); + const secondProgram = api.createIncrementalProgram(["/src/main.ts"], options); + const globalDiagnostics = secondProgram.getGlobalDiagnostics(); + assert.ok(globalDiagnostics.some(diagnostic => diagnostic.code === 2318)); + const secondEmit = secondProgram.emit(); + using secondEmitSnapshot = secondEmit.snapshot; + assert.ok(!secondEmit.emittedFiles.includes("/out/main.js"), JSON.stringify(secondEmit.emittedFiles)); + assert.ok(secondEmit.emittedFiles.includes("/out/dependency.js")); + assert.ok(secondEmit.emittedFiles.includes("/out/build.tsbuildinfo")); + secondProgram.dispose(); + }); + + test("emitBuildInfo returns a filesystem layer for a full filesystem", () => { + const hostWrites: string[] = []; + using api = new API({ + cwd: "/", + fs: { + writeFile: path => { + hostWrites.push(path); + }, + }, + }); + using snapshot = api.createSnapshot({ + fileSystem: createFileSystem(Object.entries({ + "/src/index.ts": `export const value = 1;`, + })), + createPrograms: [{ + rootFiles: ["/src/index.ts"], + compilerOptions: { + incremental: true, + noLib: true, + tsBuildInfoFile: "/out/build.tsbuildinfo", + }, + incremental: true, + }], + }); + const program = snapshot.operation.createdPrograms[0]; + assert.ok(program instanceof IncrementalProgram); + using dirtySnapshot = snapshot.update({ + fileSystem: createFileSystemLayer([["/src/index.ts", `export const value = 2;`]]), + }); + const dirtyProgram = dirtySnapshot.getProgram(program.id); + assert.ok(dirtyProgram instanceof IncrementalProgram); + assert.equal(dirtyProgram.status.dirty, true); + assert.deepEqual( + { ...dirtyProgram.status, dirty: false }, + program.status, + ); + + const buildInfoText = program.getBuildInfoEmit(); + const result = program.emitBuildInfo(); + using emittedSnapshot = result.snapshot; + assert.deepEqual(result.emittedFiles, ["/out/build.tsbuildinfo"]); + assert.deepEqual(result.fileSystem, { + kind: "layer", + files: { + "/out/build.tsbuildinfo": buildInfoText, + }, + }); + assert.deepEqual(hostWrites, []); + }); + + test("incremental emitOnly preserves other pending output kinds", () => { + const fs = createVirtualFileSystem({ + "/src/index.ts": `export const value = 1;`, + }); + using api = new API({ cwd: "/", fs }); + const program = api.createIncrementalProgram( + ["/src/index.ts"], + { + declaration: true, + incremental: true, + noLib: true, + outDir: "/out", + rootDir: "/src", + tsBuildInfoFile: "/out/build.tsbuildinfo", + }, + ); + + const jsEmit = program.emit(EmitOnly.OnlyJs); + using jsSnapshot = jsEmit.snapshot; + assert.ok(jsEmit.emittedFiles.includes("/out/index.js")); + assert.ok(!jsEmit.emittedFiles.includes("/out/index.d.ts")); + assert.ok(jsEmit.program.status.pendingEmit.some(emit => emit.sourceFileName === "/src/index.ts" && (emit.kind & FileEmitKind.DtsEmit) !== 0)); + assert.ok(jsEmit.program.status.pendingEmit.every(emit => (emit.kind & FileEmitKind.Js) === 0)); + + const dtsEmit = jsEmit.program.emit(EmitOnly.OnlyDts); + using dtsSnapshot = dtsEmit.snapshot; + assert.ok(!dtsEmit.emittedFiles.includes("/out/index.js")); + assert.ok(dtsEmit.emittedFiles.includes("/out/index.d.ts")); + assert.deepEqual(dtsEmit.program.status.pendingEmit, []); + program.dispose(); + }); + test("createProgram includes project references", () => { const reference = { path: "/lib/tsconfig.json", originalPath: "/lib/tsconfig.json", circular: false }; using api = spawnAPI({ diff --git a/tools/gen-proto/main.go b/tools/gen-proto/main.go index c2541415b255f..d16436346952b 100644 --- a/tools/gen-proto/main.go +++ b/tools/gen-proto/main.go @@ -527,6 +527,8 @@ func (r *typeRenderer) namedType(named *types.Named) string { return r.importType("ScriptTarget", "#enums/scriptTarget") case "github.com/microsoft/TypeScript/tsc/internal/core.ScriptKind": return r.importType("ScriptKind", "#enums/scriptKind") + case "github.com/microsoft/TypeScript/tsc/internal/execute/incremental.FileEmitKind": + return r.importType("FileEmitKind", "#enums/fileEmitKind") case "github.com/microsoft/TypeScript/tsc/internal/collections.OrderedMap": if named.TypeArgs().Len() != 2 { return "Record" diff --git a/tools/scripts/tsc/generate-enums.ts b/tools/scripts/tsc/generate-enums.ts index e79cfc8c571a8..b74c002058529 100644 --- a/tools/scripts/tsc/generate-enums.ts +++ b/tools/scripts/tsc/generate-enums.ts @@ -57,6 +57,7 @@ const enumDefs = [ { name: "NodeBuilderFlags", goPrefix: "Flags", goFile: "tsc/internal/nodebuilder/types.go", outDir: "packages/typescript/src/enums" }, { name: "CompletionItemKind", goPrefix: "CompletionItemKind", goFile: "tsc/internal/lsp/lsproto/lsp_generated.go", outDir: "packages/typescript/src/enums" }, { name: "EmitOnly", goPrefix: "Emit", goFile: "tsc/internal/compiler/emitter.go", outDir: "packages/typescript/src/enums", excludeMembers: ["OnlyBuilderSignature"] }, + { name: "FileEmitKind", goPrefix: "FileEmitKind", goFile: "tsc/internal/execute/incremental/snapshot.go", outDir: "packages/typescript/src/enums", excludeMembers: ["Dts", "AllJs", "AllDtsEmit", "AllDts", "All"] }, // 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: "tsc/internal/ast/symbol.go", outDir: "packages/typescript/src/enums", stringEnum: true, valueReplacements: { InternalSymbolNamePrefix: "__" } }, diff --git a/tsc/internal/api/enum_values_generated.go b/tsc/internal/api/enum_values_generated.go index cd38442117003..9421bfd1c4d82 100644 --- a/tsc/internal/api/enum_values_generated.go +++ b/tsc/internal/api/enum_values_generated.go @@ -17,6 +17,7 @@ import ( compiler "github.com/microsoft/TypeScript/tsc/internal/compiler" core "github.com/microsoft/TypeScript/tsc/internal/core" diagnostics "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + incremental "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" lsproto "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" nodebuilder "github.com/microsoft/TypeScript/tsc/internal/nodebuilder" spanmap "github.com/microsoft/TypeScript/tsc/internal/spanmap" @@ -1007,6 +1008,15 @@ func main() { "OnlyJs": toInt32(compiler.EmitOnlyJs), "OnlyDts": toInt32(compiler.EmitOnlyDts), }, + "FileEmitKind": { + "None": toInt32(incremental.FileEmitKindNone), + "Js": toInt32(incremental.FileEmitKindJs), + "JsMap": toInt32(incremental.FileEmitKindJsMap), + "JsInlineMap": toInt32(incremental.FileEmitKindJsInlineMap), + "DtsErrors": toInt32(incremental.FileEmitKindDtsErrors), + "DtsEmit": toInt32(incremental.FileEmitKindDtsEmit), + "DtsMap": toInt32(incremental.FileEmitKindDtsMap), + }, } if err := json.NewEncoder(os.Stdout).Encode(values); err != nil { panic(err) diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 818ce96e35816..b09b6c126d43c 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -14,6 +14,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/diagnosticwriter" + "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" "github.com/microsoft/TypeScript/tsc/internal/jsnum" "github.com/microsoft/TypeScript/tsc/internal/json" @@ -231,6 +232,7 @@ const ( MethodPrintNode Method = "printNode" MethodFormatNodeForInsertion Method = "formatNodeForInsertion" MethodEmit Method = "emit" + MethodGetBuildInfoEmit Method = "getBuildInfoEmit" MethodEmitToString Method = "emitToString" MethodGetJavaScriptEmit Method = "getJavaScriptEmit" MethodGetDeclarationEmit Method = "getDeclarationEmit" @@ -393,6 +395,21 @@ type SnapshotRequestChangesParams struct { // EnsurePrograms identifies projects whose programs should be updated if dirty, // or all contained projects when true. EnsurePrograms *EnsurePrograms `json:"ensurePrograms,omitempty"` + // IncrementalOperations advances incremental program state while constructing the snapshot. + IncrementalOperations []*IncrementalOperationParams `json:"incrementalOperations,omitempty"` +} + +type IncrementalOperationKind string + +const ( + IncrementalOperationKindEmit IncrementalOperationKind = "emit" + IncrementalOperationKindEmitBuildInfo IncrementalOperationKind = "emitBuildInfo" +) + +type IncrementalOperationParams struct { + Program project.SyntheticProjectID `json:"program"` + Kind IncrementalOperationKind `json:"kind"` + EmitOnly *uint32 `json:"emitOnly,omitempty"` } type EnsurePrograms struct { @@ -432,6 +449,8 @@ type CreateSnapshotProgramParams struct { RootFiles []DocumentIdentifier `json:"rootFiles"` CompilerOptions core.CompilerOptions `json:"compilerOptions"` Options *CreateProgramOptions `json:"options,omitempty"` + // Incremental restores persistent diagnostic and emit state from the configured build info file. + Incremental bool `json:"incremental,omitempty"` } type ReconfigureSnapshotProgramParams struct { @@ -558,8 +577,14 @@ type CreateSnapshotResponse struct { } type SnapshotOperationResponse struct { - CreatedPrograms *[]project.SyntheticProjectID `json:"createdPrograms,omitzero"` - OpenedFiles *[]*OpenedFileOperationResult `json:"openedFiles,omitzero"` + CreatedPrograms *[]project.SyntheticProjectID `json:"createdPrograms,omitzero"` + OpenedFiles *[]*OpenedFileOperationResult `json:"openedFiles,omitzero"` + IncrementalOperations *[]*IncrementalOperationResultResponse `json:"incrementalOperations,omitzero"` +} + +type IncrementalOperationResultResponse struct { + Program project.ID `json:"program"` + Result *EmitResponse `json:"result" nonnil:"true"` } type OpenedFileOperationResult struct { @@ -713,6 +738,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodPrintNode: unmarshallerFor[PrintNodeParams], MethodFormatNodeForInsertion: unmarshallerFor[FormatNodeForInsertionParams], MethodEmit: unmarshallerFor[EmitParams], + MethodGetBuildInfoEmit: unmarshallerFor[GetProjectDiagnosticsParams], MethodEmitToString: unmarshallerFor[EmitParams], MethodGetJavaScriptEmit: unmarshallerFor[SelectedFilesEmitParams], MethodGetDeclarationEmit: unmarshallerFor[SelectedFilesEmitParams], @@ -969,17 +995,32 @@ type GetDefaultProjectForFileParams struct { } type ProjectResponse struct { - Id project.ID `json:"id"` - ConfigFileName string `json:"configFileName"` - CurrentDirectory string `json:"currentDirectory"` - Dirty bool `json:"dirty"` - ParsedCommandLine *ConfigFileResponse `json:"parsedCommandLine" nonnil:"true"` + Id project.ID `json:"id"` + ConfigFileName string `json:"configFileName"` + CurrentDirectory string `json:"currentDirectory"` + Dirty bool `json:"dirty"` + Incremental bool `json:"incremental"` + IncrementalStatus *IncrementalStatusResponse `json:"incrementalStatus,omitempty"` + ParsedCommandLine *ConfigFileResponse `json:"parsedCommandLine" nonnil:"true"` // Deprecated: Use parsedCommandLine.fileNames. RootFiles []string `json:"rootFiles" nonnil:"true"` // Deprecated: Use parsedCommandLine.options. CompilerOptions *core.CompilerOptions `json:"compilerOptions" nonnil:"true"` } +type IncrementalStatusResponse struct { + ChangedFiles []string `json:"changedFiles" nonnil:"true"` + PendingEmit []*IncrementalPendingEmitResponse `json:"pendingEmit" nonnil:"true"` + PendingSemanticDiagnostics []string `json:"pendingSemanticDiagnostics" nonnil:"true"` + BuildInfoEmitPending bool `json:"buildInfoEmitPending"` + LatestChangedDtsFile string `json:"latestChangedDtsFile,omitempty"` +} + +type IncrementalPendingEmitResponse struct { + SourceFileName string `json:"sourceFileName"` + Kind incremental.FileEmitKind `json:"kind"` +} + func NewConfigFileResponse(parsedCommandLine *tsoptions.ParsedCommandLine) *ConfigFileResponse { if parsedCommandLine == nil { return nil @@ -1041,11 +1082,25 @@ func NewProjectResponse(p *project.Project) *ProjectResponse { if p.Kind == project.KindConfigured { configFileName = p.ConfigFileName() } + var incrementalStatus *IncrementalStatusResponse + if status := p.IncrementalStatus(); status != nil { + incrementalStatus = &IncrementalStatusResponse{ + ChangedFiles: status.ChangedFiles, + PendingEmit: core.Map(status.PendingEmit, func(emit *incremental.PendingEmit) *IncrementalPendingEmitResponse { + return &IncrementalPendingEmitResponse{SourceFileName: emit.SourceFileName, Kind: emit.Kind} + }), + PendingSemanticDiagnostics: status.PendingSemanticDiagnostics, + BuildInfoEmitPending: status.BuildInfoEmitPending, + LatestChangedDtsFile: status.LatestChangedDtsFile, + } + } return &ProjectResponse{ Id: p.ID(), ConfigFileName: configFileName, CurrentDirectory: p.CurrentDirectory(), Dirty: p.IsDirty(), + Incremental: p.IsIncremental(), + IncrementalStatus: incrementalStatus, ParsedCommandLine: NewConfigFileResponse(p.CommandLine), RootFiles: p.CommandLine.FileNames(), CompilerOptions: p.CommandLine.CompilerOptions(), diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d6f39a7df04f1..17047c4b1be5d 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -24,6 +24,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/execute/build" + "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" "github.com/microsoft/TypeScript/tsc/internal/format" "github.com/microsoft/TypeScript/tsc/internal/ipc" @@ -111,6 +112,29 @@ func (sd *snapshotData) getProgram(projectHandle project.ID) (*compiler.Program, return program, nil } +func (sd *snapshotData) getProgramLike(projectHandle project.ID) (compiler.ProgramLike, error) { + proj, err := sd.getProject(projectHandle) + if err != nil { + return nil, err + } + if proj.GetProgram() == nil { + return nil, fmt.Errorf("%w: project has no program", ErrClientError) + } + return proj.GetProgramLike(), nil +} + +func (sd *snapshotData) getIncrementalProgram(projectHandle project.ID) (*incremental.Program, error) { + proj, err := sd.getProject(projectHandle) + if err != nil { + return nil, err + } + program := proj.GetIncrementalProgram() + if program == nil { + return nil, fmt.Errorf("%w: project %s is not incremental", ErrClientError, projectHandle) + } + return program, nil +} + // getProject looks up a project from a project handle within this snapshot. func (sd *snapshotData) getProject(projectHandle project.ID) (*project.Project, error) { proj := sd.snapshot.ProjectCollection.GetProject(projectHandle) @@ -925,6 +949,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleFormatNodeForInsertion(ctx, parsed.(*FormatNodeForInsertionParams)) case string(MethodEmit): return s.handleEmit(ctx, parsed.(*EmitParams)) + case string(MethodGetBuildInfoEmit): + return s.handleGetBuildInfoEmit(ctx, parsed.(*GetProjectDiagnosticsParams)) case string(MethodEmitToString): return s.handleEmitToString(ctx, parsed.(*EmitParams)) case string(MethodGetJavaScriptEmit): @@ -1241,6 +1267,7 @@ func (s *Session) handleCreateSnapshot(ctx context.Context, params *CreateSnapsh apiRequest.FileSystem = fileSystem apiRequest.ReplaceFileSystem = params.FileSystem.Kind == requestfilesystem.KindFull } + s.configureIncrementalOperations(apiRequest, snapshotFileSystem) root := s.snapshotHost.NewRootSnapshot() snapshot, err := s.snapshotHost.CloneSnapshot(ctx, root, fileChanges, apiRequest) root.Deref() @@ -1253,7 +1280,7 @@ func (s *Session) handleCreateSnapshot(ctx context.Context, params *CreateSnapsh return nil, err } - response := s.createSnapshotResponse(snapshot, nil, ¶ms.SnapshotRequestChangesParams) + response := s.createSnapshotResponse(snapshot, nil, ¶ms.SnapshotRequestChangesParams, apiRequest) s.registerSnapshot(snapshot, openState, snapshotFileSystem) return response, nil } @@ -1291,6 +1318,7 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh apiRequest.FileSystem = snapshotFileSystem apiRequest.ReplaceFileSystem = changes.FileSystem != nil && changes.FileSystem.Kind == requestfilesystem.KindFull } + s.configureIncrementalOperations(apiRequest, snapshotFileSystem) snapshot, err := s.snapshotHost.CloneSnapshot(ctx, baseSD.snapshot, fileChanges, apiRequest) if err != nil { snapshot.Deref() @@ -1301,7 +1329,7 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh return nil, err } - response := s.createSnapshotResponse(snapshot, baseSD.snapshot, &changes.SnapshotRequestChangesParams) + response := s.createSnapshotResponse(snapshot, baseSD.snapshot, &changes.SnapshotRequestChangesParams, apiRequest) s.registerSnapshot(snapshot, openState, snapshotFileSystem) return response, nil } @@ -1366,6 +1394,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq request := &project.APICreateProgramRequest{ RootFileNames: rootFileNames, CompilerOptions: &programParams.CompilerOptions, + Incremental: programParams.Incremental, } if programParams.Options != nil { request.ProjectReferences = programParams.Options.ProjectReferences @@ -1432,9 +1461,47 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq apiRequest.EnsurePrograms.Add(program) } } + apiRequest.IncrementalOperations = make([]*project.APIIncrementalOperationRequest, len(changes.IncrementalOperations)) + for i, operation := range changes.IncrementalOperations { + if operation == nil { + return nil, fmt.Errorf("%w: incrementalOperations[%d] must not be null", ErrClientError, i) + } + programID, ok := project.ID(operation.Program).Synthetic() + if !ok { + return nil, fmt.Errorf("%w: invalid synthetic project handle: %s", ErrClientError, operation.Program) + } + emitOnly, err := getEmitOnly(operation.EmitOnly) + if err != nil { + return nil, err + } + apiRequest.IncrementalOperations[i] = &project.APIIncrementalOperationRequest{ + ProgramID: programID, + Kind: project.APIIncrementalOperationKind(operation.Kind), + EmitOnly: emitOnly, + } + } return apiRequest, nil } +func (s *Session) configureIncrementalOperations(apiRequest *project.APISnapshotRequest, fileSystem vfs.FS) { + for _, operation := range apiRequest.IncrementalOperations { + if requestfilesystem.HasFullFileSystem(fileSystem) { + var outputMu sync.Mutex + operation.EmittedFilesContents = make(map[string]string) + operation.WriteFile = func(fileName string, text string, _ *compiler.WriteFileData) error { + outputMu.Lock() + operation.EmittedFilesContents[fileName] = text + outputMu.Unlock() + return nil + } + } else { + operation.WriteFile = func(fileName string, text string, _ *compiler.WriteFileData) error { + return s.snapshotHost.FS().WriteFile(fileName, text) + } + } + } +} + type languageServerSnapshotUpdate struct { request *project.APISnapshotRequest openState snapshotOpenState @@ -1463,6 +1530,11 @@ func (s *Session) toLanguageServerSnapshotUpdate(ctx context.Context, changes *S return nil, fmt.Errorf("%w: synthetic program is not owned by this API session: %s", ErrClientError, reconfigure.ProgramID) } } + for _, operation := range apiRequest.IncrementalOperations { + if !s.createdPrograms.Has(operation.ProgramID) { + return nil, fmt.Errorf("%w: incremental program is not owned by this API session: %s", ErrClientError, operation.ProgramID) + } + } return update, nil } @@ -1575,6 +1647,7 @@ func (s *Session) handleGetCurrentLanguageServerSnapshot(ctx context.Context, pa if err != nil { return nil, err } + s.configureIncrementalOperations(update.request, nil) snapshot, err := s.projectSession.APIUpdate(ctx, project.FileChangeSummary{}, update.request) if err != nil { @@ -1582,7 +1655,7 @@ func (s *Session) handleGetCurrentLanguageServerSnapshot(ctx context.Context, pa } update.commit(s, snapshot) - response := s.createSnapshotResponse(snapshot, baseSnapshot, &changes.SnapshotRequestChangesParams) + response := s.createSnapshotResponse(snapshot, baseSnapshot, &changes.SnapshotRequestChangesParams, update.request) s.registerSnapshot(snapshot, snapshotOpenState{openProjects: s.openProjects, openFiles: s.openFiles}, nil) return response, nil } @@ -1953,8 +2026,8 @@ func transpileOutput(ctx context.Context, input string, options TranspileOptions output = transpile.TranspileModule(ctx, input, transpileOptions) } if output == nil { - if err := ctx.Err(); err != nil { - return nil, err + if contextErr := ctx.Err(); contextErr != nil { + return nil, contextErr } return nil, errors.New("transpilation produced no output") } @@ -3562,12 +3635,35 @@ func newPrinter(params *PrintNodeParams) *printer.Printer { } func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResponse, error) { - program, options, err := s.getEmitOptions(params) + program, options, err := s.getIncrementalEmitOptions(params) if err != nil { return nil, err } - var outputFiles map[string]string + return s.handleEmitWorker(ctx, params.Snapshot, options, func(options compiler.EmitOptions) (*compiler.EmitResult, error) { + return emitProgram(ctx, program, options) + }) +} + +func (s *Session) handleGetBuildInfoEmit(ctx context.Context, params *GetProjectDiagnosticsParams) (string, error) { sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return "", err + } + program, err := sd.getIncrementalProgram(params.Project) + if err != nil { + return "", err + } + return program.GetBuildInfoEmit(ctx) +} + +func (s *Session) handleEmitWorker( + ctx context.Context, + snapshot SnapshotID, + options compiler.EmitOptions, + emit func(options compiler.EmitOptions) (*compiler.EmitResult, error), +) (*EmitResponse, error) { + var outputFiles map[string]string + sd, err := s.getSnapshotData(snapshot) if err != nil { return nil, err } @@ -3585,10 +3681,14 @@ func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResp return s.snapshotHost.FS().WriteFile(fileName, text) } } - result, err := emitProgram(ctx, program, options) + result, err := emit(options) if err != nil { return nil, err } + return newEmitResponse(result, outputFiles), nil +} + +func newEmitResponse(result *compiler.EmitResult, outputFiles map[string]string) *EmitResponse { emittedFiles := slices.Clone(result.EmittedFiles) if emittedFiles == nil { emittedFiles = []string{} @@ -3605,7 +3705,7 @@ func (s *Session) handleEmit(ctx context.Context, params *EmitParams) (*EmitResp Diagnostics: nonNilDiagnostics(result.Diagnostics), EmittedFiles: emittedFiles, EmittedFilesContents: emittedFilesContents, - }, nil + } } func (s *Session) handleEmitToString(ctx context.Context, params *EmitParams) (*EmitOutputResponse, error) { @@ -3639,7 +3739,7 @@ func (s *Session) handleSelectedFilesEmit(ctx context.Context, params *SelectedF }) } -func emitToOutput(ctx context.Context, program *compiler.Program, options compiler.EmitOptions) (*EmitOutputResponse, error) { +func emitToOutput(ctx context.Context, program compiler.ProgramLike, options compiler.EmitOptions) (*EmitOutputResponse, error) { var mu sync.Mutex outputFiles := make([]*EmitOutputFile, 0) options.WriteFile = func(fileName string, text string, data *compiler.WriteFileData) error { @@ -3682,6 +3782,20 @@ func (s *Session) getEmitOptions(params *EmitParams) (*compiler.Program, compile }, nil } +func (s *Session) getIncrementalEmitOptions(params *EmitParams) (compiler.ProgramLike, compiler.EmitOptions, error) { + program, err := s.getEmitProgramLike(params.Snapshot, params.Project) + if err != nil { + return nil, compiler.EmitOptions{}, err + } + emitOnly, err := getEmitOnly(params.EmitOnly) + if err != nil { + return nil, compiler.EmitOptions{}, err + } + return program, compiler.EmitOptions{ + EmitOnly: emitOnly, + }, nil +} + func (s *Session) getEmitProgram(snapshot SnapshotID, projectID project.ID) (*compiler.Program, error) { sd, err := s.getSnapshotData(snapshot) if err != nil { @@ -3690,6 +3804,14 @@ func (s *Session) getEmitProgram(snapshot SnapshotID, projectID project.ID) (*co return sd.getProgram(projectID) } +func (s *Session) getEmitProgramLike(snapshot SnapshotID, projectID project.ID) (compiler.ProgramLike, error) { + sd, err := s.getSnapshotData(snapshot) + if err != nil { + return nil, err + } + return sd.getProgramLike(projectID) +} + func getEmitOnly(value *uint32) (compiler.EmitOnly, error) { if value == nil { return compiler.EmitAll, nil @@ -3701,7 +3823,7 @@ func getEmitOnly(value *uint32) (compiler.EmitOnly, error) { return emitOnly, nil } -func emitProgram(ctx context.Context, program *compiler.Program, options compiler.EmitOptions) (*compiler.EmitResult, error) { +func emitProgram(ctx context.Context, program compiler.ProgramLike, options compiler.EmitOptions) (*compiler.EmitResult, error) { result := program.Emit(ctx, options) if result != nil { return result, nil @@ -4606,8 +4728,13 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna return &changes } -func (s *Session) createSnapshotResponse(snapshot *project.Snapshot, base *project.Snapshot, request *SnapshotRequestChangesParams) *CreateSnapshotResponse { - operation := s.createSnapshotOperationResponse(snapshot, request) +func (s *Session) createSnapshotResponse( + snapshot *project.Snapshot, + base *project.Snapshot, + request *SnapshotRequestChangesParams, + apiRequest *project.APISnapshotRequest, +) *CreateSnapshotResponse { + operation := s.createSnapshotOperationResponse(snapshot, request, apiRequest) if base == nil { projects := snapshot.ProjectCollection.Projects() projectResponses := make([]*ProjectResponse, 0, len(projects)) @@ -4642,7 +4769,11 @@ func (s *Session) createSnapshotResponse(snapshot *project.Snapshot, base *proje } } -func (s *Session) createSnapshotOperationResponse(snapshot *project.Snapshot, request *SnapshotRequestChangesParams) *SnapshotOperationResponse { +func (s *Session) createSnapshotOperationResponse( + snapshot *project.Snapshot, + request *SnapshotRequestChangesParams, + apiRequest *project.APISnapshotRequest, +) *SnapshotOperationResponse { operation := &SnapshotOperationResponse{} if request == nil { return operation @@ -4675,6 +4806,16 @@ func (s *Session) createSnapshotOperationResponse(snapshot *project.Snapshot, re } operation.OpenedFiles = &results } + if request.IncrementalOperations != nil { + results := make([]*IncrementalOperationResultResponse, len(apiRequest.IncrementalOperations)) + for i, result := range apiRequest.IncrementalOperations { + results[i] = &IncrementalOperationResultResponse{ + Program: request.IncrementalOperations[i].Program.AsID(), + Result: newEmitResponse(result.Result, result.EmittedFilesContents), + } + } + operation.IncrementalOperations = &results + } return operation } @@ -4776,21 +4917,33 @@ func (s *Session) toFileChangeSummary(changes *FileNotifications) project.FileCh return summary } -func (s *Session) getDiagnostics(ctx context.Context, params *GetDiagnosticsParams, getter func(*compiler.Program, context.Context, *ast.SourceFile) []*ast.Diagnostic) ([]*DiagnosticResponse, error) { +func (s *Session) getDiagnostics( + ctx context.Context, + params *GetDiagnosticsParams, + incremental bool, + getter func(compiler.ProgramLike, context.Context, *ast.SourceFile) []*ast.Diagnostic, +) ([]*DiagnosticResponse, error) { sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err } - program, err := sd.getProgram(params.Project) + compilerProgram, err := sd.getProgram(params.Project) if err != nil { return nil, err } + var program compiler.ProgramLike = compilerProgram + if incremental { + program, err = sd.getProgramLike(params.Project) + if err != nil { + return nil, err + } + } if params.Files != nil { var allDiags []*ast.Diagnostic for _, file := range params.Files { - sourceFile, err := s.resolveOptionalSourceFile(program, &file) + sourceFile, err := s.resolveOptionalSourceFile(compilerProgram, &file) if err != nil { return nil, err } @@ -4805,31 +4958,31 @@ func (s *Session) getDiagnostics(ctx context.Context, params *GetDiagnosticsPara // @gen-proto-nullable func (s *Session) handleGetSyntacticDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) - return s.getDiagnostics(ctx, params, (*compiler.Program).GetSyntacticDiagnostics) + return s.getDiagnostics(ctx, params, false, compiler.ProgramLike.GetSyntacticDiagnostics) } // @gen-proto-nullable func (s *Session) handleGetBindDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) - return s.getDiagnostics(ctx, params, (*compiler.Program).GetBindDiagnostics) + return s.getDiagnostics(ctx, params, false, compiler.ProgramLike.GetBindDiagnostics) } // @gen-proto-nullable func (s *Session) handleGetSemanticDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) - return s.getDiagnostics(ctx, params, (*compiler.Program).GetSemanticDiagnostics) + return s.getDiagnostics(ctx, params, true, compiler.ProgramLike.GetSemanticDiagnostics) } // @gen-proto-nullable func (s *Session) handleGetSuggestionDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) - return s.getDiagnostics(ctx, params, (*compiler.Program).GetSuggestionDiagnostics) + return s.getDiagnostics(ctx, params, false, compiler.ProgramLike.GetSuggestionDiagnostics) } // @gen-proto-nullable func (s *Session) handleGetDeclarationDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) - return s.getDiagnostics(ctx, params, (*compiler.Program).GetDeclarationDiagnostics) + return s.getDiagnostics(ctx, params, true, compiler.ProgramLike.GetDeclarationDiagnostics) } // handleGetConfigFileParsingDiagnostics returns config file parsing diagnostics. @@ -4880,9 +5033,9 @@ func (s *Session) handleGetGlobalDiagnostics(ctx context.Context, params *GetPro return nil, err } - program := proj.GetProgram() - if program == nil { - return nil, fmt.Errorf("%w: project has no program", ErrClientError) + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err } // Global diagnostics are accumulated lazily by the project's checker pool as diff --git a/tsc/internal/api/session_createprogram_test.go b/tsc/internal/api/session_createprogram_test.go index cd6fc3ef38f3b..eb067e0bd9c2d 100644 --- a/tsc/internal/api/session_createprogram_test.go +++ b/tsc/internal/api/session_createprogram_test.go @@ -2,10 +2,12 @@ package api import ( "context" + "slices" "testing" "github.com/microsoft/TypeScript/tsc/internal/bundled" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" @@ -173,6 +175,152 @@ func TestUpdateSnapshotReconfiguresSyntheticProgram(t *testing.T) { assert.Equal(t, reconfigured.Projects[0].CompilerOptions.Strict, core.TSTrue) } +func TestIncrementalProgramComposesWithSnapshotUpdates(t *testing.T) { + t.Parallel() + + const ( + mainFile = "/home/projects/p/main.ts" + dependencyFile = "/home/projects/p/dependency.ts" + buildInfoFile = "/home/projects/p/out/build.tsbuildinfo" + ) + projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ + mainFile: `import { value } from "./dependency"; export const result = value();`, + dependencyFile: `export function value() { return 1; }`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + ctx := context.Background() + compilerOptions := func() core.CompilerOptions { + return core.CompilerOptions{ + Declaration: core.TSTrue, + Incremental: core.TSTrue, + NoLib: core.TSTrue, + OutDir: "/home/projects/p/out", + RootDir: "/home/projects/p", + TsBuildInfoFile: buildInfoFile, + } + } + + created, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: mainFile}}, + CompilerOptions: compilerOptions(), + Incremental: true, + }}, + }) + assert.NilError(t, err) + programID := (*created.Operation.CreatedPrograms)[0] + buildInfoText, err := session.handleGetBuildInfoEmit(ctx, &GetProjectDiagnosticsParams{Snapshot: created.Snapshot, Project: programID.AsID()}) + assert.NilError(t, err) + assert.Assert(t, len(buildInfoText) > 0) + _, buildInfoExists := sessionUtils.FS().ReadFile(buildInfoFile) + assert.Assert(t, !buildInfoExists) + canceledContext, cancel := context.WithCancel(ctx) + cancel() + _, err = session.handleUpdateSnapshot(canceledContext, &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{ + IncrementalOperations: []*IncrementalOperationParams{{ + Program: programID, + Kind: IncrementalOperationKindEmitBuildInfo, + }}, + }, + }) + assert.ErrorIs(t, err, context.Canceled) + buildInfoAfterCancellation, err := session.handleGetBuildInfoEmit(ctx, &GetProjectDiagnosticsParams{Snapshot: created.Snapshot, Project: programID.AsID()}) + assert.NilError(t, err) + assert.Equal(t, buildInfoAfterCancellation, buildInfoText) + + emittedBuildInfoSnapshot, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{ + IncrementalOperations: []*IncrementalOperationParams{{ + Program: programID, + Kind: IncrementalOperationKindEmitBuildInfo, + }}, + }, + }) + assert.NilError(t, err) + buildInfoEmit := (*emittedBuildInfoSnapshot.Operation.IncrementalOperations)[0].Result + assert.DeepEqual(t, buildInfoEmit.EmittedFiles, []string{buildInfoFile}) + writtenBuildInfo, ok := sessionUtils.FS().ReadFile(buildInfoFile) + assert.Assert(t, ok) + assert.Equal(t, writtenBuildInfo, buildInfoText) + + firstEmit, err := session.handleEmit(ctx, &EmitParams{Snapshot: created.Snapshot, Project: programID.AsID()}) + assert.NilError(t, err) + assert.Assert(t, slices.Contains(firstEmit.EmittedFiles, buildInfoFile)) + buildInfoAfterEmit, err := session.handleGetBuildInfoEmit(ctx, &GetProjectDiagnosticsParams{Snapshot: created.Snapshot, Project: programID.AsID()}) + assert.NilError(t, err) + assert.Equal(t, buildInfoAfterEmit, buildInfoText) + + restored, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: mainFile}}, + CompilerOptions: compilerOptions(), + Incremental: true, + }}, + }) + assert.NilError(t, err) + restoredProgramID := (*restored.Operation.CreatedPrograms)[0] + + assert.NilError(t, sessionUtils.FS().WriteFile(dependencyFile, `export function value() { return 2; }`)) + dirty, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: restored.Snapshot, + Changes: &CreateSnapshotParams{ + FileNotifications: &FileNotifications{Changed: []DocumentIdentifier{{FileName: dependencyFile}}}, + }, + }) + assert.NilError(t, err) + dirtySnapshot, err := session.getSnapshotData(dirty.Snapshot) + assert.NilError(t, err) + dirtyProject, err := dirtySnapshot.getProject(restoredProgramID.AsID()) + assert.NilError(t, err) + _, isIncremental := dirtyProject.GetProgramLike().(*incremental.Program) + assert.Assert(t, isIncremental) + assert.Assert(t, dirtyProject.IsDirty()) + assert.Assert(t, dirtyProject.IncrementalStatus() != nil) + + emittedSnapshot, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: dirty.Snapshot, + Changes: &CreateSnapshotParams{ + IncrementalOperations: []*IncrementalOperationParams{{ + Program: restoredProgramID, + Kind: IncrementalOperationKindEmit, + }}, + }, + }) + assert.NilError(t, err) + emitted := (*emittedSnapshot.Operation.IncrementalOperations)[0].Result + assert.Assert(t, !slices.Contains(emitted.EmittedFiles, "/home/projects/p/out/main.js"), "unexpected emitted files: %v", emitted.EmittedFiles) + assert.Assert(t, slices.Contains(emitted.EmittedFiles, "/home/projects/p/out/dependency.js")) + emittedSnapshotData, err := session.getSnapshotData(emittedSnapshot.Snapshot) + assert.NilError(t, err) + emittedProject, err := emittedSnapshotData.getProject(restoredProgramID.AsID()) + assert.NilError(t, err) + assert.Equal(t, emittedProject.IncrementalStatus().BuildInfoEmitPending, false) + assert.Equal(t, len(emittedProject.IncrementalStatus().PendingEmit), 0) + + reconfigured, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: emittedSnapshot.Snapshot, + Changes: &CreateSnapshotParams{ + ReconfigurePrograms: []*ReconfigureSnapshotProgramParams{{ + Id: restoredProgramID, + RootFiles: []DocumentIdentifier{{FileName: mainFile}}, + CompilerOptions: compilerOptions(), + }}, + }, + }) + assert.NilError(t, err) + reconfiguredSnapshot, err := session.getSnapshotData(reconfigured.Snapshot) + assert.NilError(t, err) + reconfiguredProject, err := reconfiguredSnapshot.getProject(restoredProgramID.AsID()) + assert.NilError(t, err) + _, isIncremental = reconfiguredProject.GetProgramLike().(*incremental.Program) + assert.Assert(t, isIncremental) +} + func TestReconfigureSyntheticProgramValidation(t *testing.T) { t.Parallel() @@ -210,6 +358,117 @@ func TestReconfigureSyntheticProgramValidation(t *testing.T) { assert.ErrorContains(t, err, "not found for reconfiguration") } +func TestIncrementalOperationsValidateBeforeWriting(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ + fileName: `export const value = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + ctx := context.Background() + + created, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{ + { + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + CompilerOptions: core.CompilerOptions{ + Incremental: core.TSTrue, + NoLib: core.TSTrue, + OutDir: "/out/incremental", + RootDir: "/home/projects/p", + TsBuildInfoFile: "/out/incremental.tsbuildinfo", + }, + Incremental: true, + }, + { + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + CompilerOptions: core.CompilerOptions{ + NoLib: core.TSTrue, + OutDir: "/out/regular", + RootDir: "/home/projects/p", + }, + }, + }, + }) + assert.NilError(t, err) + programs := *created.Operation.CreatedPrograms + + _, err = session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{ + IncrementalOperations: []*IncrementalOperationParams{ + {Program: programs[0], Kind: IncrementalOperationKindEmit}, + {Program: programs[1], Kind: IncrementalOperationKindEmit}, + }, + }, + }) + assert.ErrorContains(t, err, "project is not incremental") + _, written := sessionUtils.FS().ReadFile("/out/incremental/index.js") + assert.Assert(t, !written) +} + +func TestIncrementalStatusPreservesSourceFileNameCasing(t *testing.T) { + t.Parallel() + + const fileName = "/Src/Foo.ts" + projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ + fileName: `export const value = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + ctx := context.Background() + + created, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + Incremental: true, + }}, + }) + assert.NilError(t, err) + programID := (*created.Operation.CreatedPrograms)[0] + createdSnapshot, err := session.getSnapshotData(created.Snapshot) + assert.NilError(t, err) + createdProject, err := createdSnapshot.getProject(programID.AsID()) + assert.NilError(t, err) + assert.Equal(t, createdProject.IncrementalStatus().PendingEmit[0].SourceFileName, fileName) + + assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value = 2;`)) + updated, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{ + FileNotifications: &FileNotifications{Changed: []DocumentIdentifier{{FileName: fileName}}}, + EnsurePrograms: &EnsurePrograms{Projects: []project.ID{programID.AsID()}}, + }, + }) + assert.NilError(t, err) + updatedSnapshot, err := session.getSnapshotData(updated.Snapshot) + assert.NilError(t, err) + updatedProject, err := updatedSnapshot.getProject(programID.AsID()) + assert.NilError(t, err) + assert.DeepEqual(t, updatedProject.IncrementalStatus().ChangedFiles, []string{fileName}) + + assert.NilError(t, sessionUtils.FS().Remove(fileName)) + deleted, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: updated.Snapshot, + Changes: &CreateSnapshotParams{ + FileNotifications: &FileNotifications{Deleted: []DocumentIdentifier{{FileName: fileName}}}, + EnsurePrograms: &EnsurePrograms{Projects: []project.ID{programID.AsID()}}, + }, + }) + assert.NilError(t, err) + deletedSnapshot, err := session.getSnapshotData(deleted.Snapshot) + assert.NilError(t, err) + deletedProject, err := deletedSnapshot.getProject(programID.AsID()) + assert.NilError(t, err) + assert.DeepEqual(t, deletedProject.IncrementalStatus().ChangedFiles, []string{}) + assert.DeepEqual(t, deletedProject.IncrementalStatus().PendingEmit, []*incremental.PendingEmit{}) +} + func TestCreateSyntheticProgramValidation(t *testing.T) { t.Parallel() diff --git a/tsc/internal/execute/incremental/emitfileshandler.go b/tsc/internal/execute/incremental/emitfileshandler.go index ba064548666c3..64a24f296786b 100644 --- a/tsc/internal/execute/incremental/emitfileshandler.go +++ b/tsc/internal/execute/incremental/emitfileshandler.go @@ -36,6 +36,9 @@ func (h *emitFilesHandler) getPendingEmitKindForEmitOptions(emitKind FileEmitKin if options.EmitOnly == compiler.EmitOnlyDts { pendingKind &= FileEmitKindAllDts } + if options.EmitOnly == compiler.EmitOnlyJs { + pendingKind &= FileEmitKindAllJs + } if h.isForDtsErrors { pendingKind &= FileEmitKindDtsErrors } diff --git a/tsc/internal/execute/incremental/program.go b/tsc/internal/execute/incremental/program.go index d8b6a5dbb1c2c..c9027a9eac97e 100644 --- a/tsc/internal/execute/incremental/program.go +++ b/tsc/internal/execute/incremental/program.go @@ -2,6 +2,7 @@ package incremental import ( "context" + "errors" "fmt" "slices" "strings" @@ -66,6 +67,15 @@ func NewProgram(program *compiler.Program, oldProgram *Program, host Host, neste return incrementalProgram } +func (p *Program) Fork() *Program { + return &Program{ + snapshot: p.snapshot.clone(), + program: p.program, + host: p.host, + nestedEmitNow: p.nestedEmitNow, + } +} + type TestingData struct { SemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] OldProgramSemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] @@ -122,6 +132,59 @@ func (p *Program) HasChangedDtsFile() bool { return p.snapshot.hasChangedDtsFile } +type PendingEmit struct { + SourceFileName string + Kind FileEmitKind +} + +type Status struct { + ChangedFiles []string + PendingEmit []*PendingEmit + PendingSemanticDiagnostics []string + BuildInfoEmitPending bool + LatestChangedDtsFile string +} + +func (p *Program) Status() *Status { + status := &Status{ + ChangedFiles: []string{}, + PendingEmit: []*PendingEmit{}, + PendingSemanticDiagnostics: []string{}, + BuildInfoEmitPending: p.snapshot.buildInfoEmitPending.Load(), + LatestChangedDtsFile: p.snapshot.latestChangedDtsFile, + } + p.snapshot.changedFilesSet.Range(func(path tspath.Path) bool { + status.ChangedFiles = append(status.ChangedFiles, p.sourceFileName(path)) + return true + }) + p.snapshot.affectedFilesPendingEmit.Range(func(path tspath.Path, kind FileEmitKind) bool { + status.PendingEmit = append(status.PendingEmit, &PendingEmit{ + SourceFileName: p.sourceFileName(path), + Kind: kind, + }) + return true + }) + for _, file := range p.program.GetSourceFiles() { + if _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); !ok { + status.PendingSemanticDiagnostics = append(status.PendingSemanticDiagnostics, file.FileName()) + } + } + slices.Sort(status.ChangedFiles) + slices.SortFunc(status.PendingEmit, func(a, b *PendingEmit) int { + return strings.Compare(a.SourceFileName, b.SourceFileName) + }) + slices.Sort(status.PendingSemanticDiagnostics) + return status +} + +func (p *Program) sourceFileName(path tspath.Path) string { + file := p.program.GetSourceFileByPath(path) + if file == nil { + panic(fmt.Sprintf("incremental state contains source file path not present in program: %s", path)) + } + return file.FileName() +} + // Options implements compiler.AnyProgram interface. func (p *Program) Options() *core.CompilerOptions { return p.snapshot.options @@ -330,6 +393,66 @@ func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOption if buildInfoFileName == "" || p.program.IsEmitBlocked(buildInfoFileName) { return nil } + if err := p.prepareBuildInfoState(ctx); err != nil { + return nil + } + if !p.snapshot.buildInfoEmitPending.Load() { + return nil + } + text, buildInfo, err := p.serializeBuildInfo(buildInfoFileName) + if err != nil { + return &compiler.EmitResult{ + EmitSkipped: true, + Diagnostics: []*ast.Diagnostic{ + compiler.ContentMapperProjectDiagnostic(err), + }, + } + } + return p.writeBuildInfo(buildInfoFileName, text, buildInfo, options) +} + +func (p *Program) GetBuildInfoEmit(ctx context.Context) (string, error) { + buildInfoFileName := outputpaths.GetBuildInfoFileName(p.snapshot.options, tspath.ComparePathsOptions{ + CurrentDirectory: p.program.GetCurrentDirectory(), + UseCaseSensitiveFileNames: p.program.UseCaseSensitiveFileNames(), + }) + if buildInfoFileName == "" { + return "", errors.New("build info emit is not configured") + } + if p.program.IsEmitBlocked(buildInfoFileName) { + return "", fmt.Errorf("build info emit is blocked for %s", buildInfoFileName) + } + if err := p.prepareBuildInfoState(ctx); err != nil { + return "", err + } + text, _, err := p.serializeBuildInfo(buildInfoFileName) + return text, err +} + +func (p *Program) EmitBuildInfo(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult { + buildInfoFileName := outputpaths.GetBuildInfoFileName(p.snapshot.options, tspath.ComparePathsOptions{ + CurrentDirectory: p.program.GetCurrentDirectory(), + UseCaseSensitiveFileNames: p.program.UseCaseSensitiveFileNames(), + }) + if buildInfoFileName == "" || p.program.IsEmitBlocked(buildInfoFileName) { + return &compiler.EmitResult{EmitSkipped: true} + } + if err := p.prepareBuildInfoState(ctx); err != nil { + return nil + } + text, buildInfo, err := p.serializeBuildInfo(buildInfoFileName) + if err != nil { + return &compiler.EmitResult{ + EmitSkipped: true, + Diagnostics: []*ast.Diagnostic{ + compiler.ContentMapperProjectDiagnostic(err), + }, + } + } + return p.writeBuildInfo(buildInfoFileName, text, buildInfo, options) +} + +func (p *Program) prepareBuildInfoState(ctx context.Context) error { if p.snapshot.hasErrors == core.TSUnknown { p.ensureHasErrorsForState(ctx, p.program) if p.snapshot.hasErrors != p.snapshot.hasErrorsFromOldState || p.snapshot.hasSemanticErrors != p.snapshot.hasSemanticErrorsFromOldState { @@ -343,31 +466,29 @@ func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOption p.snapshot.buildInfoEmitPending.Store(true) } } - if !p.snapshot.buildInfoEmitPending.Load() { - return nil - } - if ctx.Err() != nil { - return nil - } + return ctx.Err() +} + +func (p *Program) serializeBuildInfo(buildInfoFileName string) (string, *BuildInfo, error) { buildInfo, err := snapshotToBuildInfo(p.snapshot, p.program, buildInfoFileName) if err != nil { - return &compiler.EmitResult{ - EmitSkipped: true, - Diagnostics: []*ast.Diagnostic{ - compiler.ContentMapperProjectDiagnostic(err), - }, - } + return "", nil, err } text, err := json.Marshal(buildInfo) if err != nil { panic(fmt.Sprintf("Failed to marshal build info: %v", err)) } + return string(text), buildInfo, nil +} + +func (p *Program) writeBuildInfo(buildInfoFileName string, text string, buildInfo *BuildInfo, options compiler.EmitOptions) *compiler.EmitResult { + var err error if options.WriteFile != nil { - err = options.WriteFile(buildInfoFileName, string(text), &compiler.WriteFileData{ + err = options.WriteFile(buildInfoFileName, text, &compiler.WriteFileData{ BuildInfo: buildInfo, }) } else { - err = p.program.Host().FS().WriteFile(buildInfoFileName, string(text)) + err = p.program.Host().FS().WriteFile(buildInfoFileName, text) } if err != nil { return &compiler.EmitResult{ diff --git a/tsc/internal/execute/incremental/programtosnapshot.go b/tsc/internal/execute/incremental/programtosnapshot.go index 27bfffb552b1c..3ad7c3b8c0b23 100644 --- a/tsc/internal/execute/incremental/programtosnapshot.go +++ b/tsc/internal/execute/incremental/programtosnapshot.go @@ -17,6 +17,10 @@ func programToSnapshot(program *compiler.Program, oldProgram *Program, hashWithT if oldProgram != nil && oldProgram.program == program { return oldProgram.snapshot } + return createProgramSnapshot(program, oldProgram, hashWithText) +} + +func createProgramSnapshot(program *compiler.Program, oldProgram *Program, hashWithText bool) *snapshot { snapshot := &snapshot{ options: program.Options(), hashWithText: hashWithText, @@ -164,6 +168,8 @@ func (t *toProgramSnapshot) handleFileDelete() { // If the global file is removed, add all files as changed t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.Path, oldInfo *FileInfo) bool { if _, ok := t.snapshot.fileInfos.Load(filePath); !ok { + t.snapshot.changedFilesSet.Delete(filePath) + t.snapshot.affectedFilesPendingEmit.Delete(filePath) if oldInfo.affectsGlobalScope { for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) { t.snapshot.addFileToChangeSet(file.Path()) diff --git a/tsc/internal/execute/incremental/referencemap.go b/tsc/internal/execute/incremental/referencemap.go index 70e67977a033a..0c03f423976d4 100644 --- a/tsc/internal/execute/incremental/referencemap.go +++ b/tsc/internal/execute/incremental/referencemap.go @@ -16,6 +16,13 @@ type referenceMap struct { referenceBy sync.Once } +func (r *referenceMap) cloneInto(target *referenceMap) { + r.references.Range(func(path tspath.Path, refs *collections.Set[tspath.Path]) bool { + target.references.Store(path, refs) + return true + }) +} + func (r *referenceMap) storeReferences(path tspath.Path, refs *collections.Set[tspath.Path]) { r.references.Store(path, refs) } diff --git a/tsc/internal/execute/incremental/snapshot.go b/tsc/internal/execute/incremental/snapshot.go index 61aa15a4230d6..5217e44f27335 100644 --- a/tsc/internal/execute/incremental/snapshot.go +++ b/tsc/internal/execute/incremental/snapshot.go @@ -3,6 +3,7 @@ package incremental import ( "encoding/hex" "fmt" + "slices" "strings" "sync" "sync/atomic" @@ -351,6 +352,64 @@ type snapshot struct { hashWithText bool } +func (s *snapshot) clone() *snapshot { + clone := &snapshot{ + options: s.options, + latestChangedDtsFile: s.latestChangedDtsFile, + hasErrors: s.hasErrors, + hasSemanticErrors: s.hasSemanticErrors, + checkPending: s.checkPending, + packageJsons: slices.Clone(s.packageJsons), + missingPackageJsons: slices.Clone(s.missingPackageJsons), + hasErrorsFromOldState: s.hasErrorsFromOldState, + hasSemanticErrorsFromOldState: s.hasSemanticErrorsFromOldState, + packageJsonsFromOldState: slices.Clone(s.packageJsonsFromOldState), + missingPackageJsonsFromOldState: slices.Clone(s.missingPackageJsonsFromOldState), + hasChangedDtsFile: s.hasChangedDtsFile, + hasEmitDiagnostics: s.hasEmitDiagnostics, + hashWithText: s.hashWithText, + } + s.referencedMap.cloneInto(&clone.referencedMap) + clone.buildInfoEmitPending.Store(s.buildInfoEmitPending.Load()) + s.fileInfos.Range(func(path tspath.Path, info *FileInfo) bool { + infoClone := *info + clone.fileInfos.Store(path, &infoClone) + return true + }) + cloneDiagnosticsMap(&s.semanticDiagnosticsPerFile, &clone.semanticDiagnosticsPerFile) + cloneDiagnosticsMap(&s.emitDiagnosticsPerFile, &clone.emitDiagnosticsPerFile) + s.changedFilesSet.Range(func(path tspath.Path) bool { + clone.changedFilesSet.Add(path) + return true + }) + s.affectedFilesPendingEmit.Range(func(path tspath.Path, kind FileEmitKind) bool { + clone.affectedFilesPendingEmit.Store(path, kind) + return true + }) + s.emitSignatures.Range(func(path tspath.Path, signature *emitSignature) bool { + signatureClone := &emitSignature{ + signature: signature.signature, + signatureWithDifferentOptions: slices.Clone(signature.signatureWithDifferentOptions), + } + clone.emitSignatures.Store(path, signatureClone) + return true + }) + return clone +} + +func cloneDiagnosticsMap( + source *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName], + target *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName], +) { + source.Range(func(path tspath.Path, value *DiagnosticsOrBuildInfoDiagnosticsWithFileName) bool { + target.Store(path, &DiagnosticsOrBuildInfoDiagnosticsWithFileName{ + diagnostics: value.diagnostics, + buildInfoDiagnostics: value.buildInfoDiagnostics, + }) + return true + }) +} + func (s *snapshot) addFileToChangeSet(filePath tspath.Path) { s.changedFilesSet.Add(filePath) s.buildInfoEmitPending.Store(true) diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 79954831210c9..99ab8111b22c6 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/contentmapper" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/ls" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" @@ -150,6 +151,8 @@ type Project struct { commandLineWithTypingsFiles *tsoptions.ParsedCommandLine commandLineWithTypingsFilesOnce sync.Once Program *compiler.Program + incrementalProgram *incremental.Program + incremental bool // The kind of update that was performed on the program last time it was updated. ProgramUpdateKind ProgramUpdateKind // The ID of the snapshot that created the program stored in this project. @@ -358,6 +361,31 @@ func (p *Project) GetProgram() *compiler.Program { return p.Program } +func (p *Project) GetProgramLike() compiler.ProgramLike { + if p.incrementalProgram != nil { + return p.incrementalProgram.Fork() + } + return p.Program +} + +func (p *Project) GetIncrementalProgram() *incremental.Program { + if p.incrementalProgram == nil { + return nil + } + return p.incrementalProgram.Fork() +} + +func (p *Project) IsIncremental() bool { + return p.incremental +} + +func (p *Project) IncrementalStatus() *incremental.Status { + if p.incrementalProgram == nil { + return nil + } + return p.incrementalProgram.Status() +} + func (p *Project) IsDirty() bool { return p.dirty } @@ -404,6 +432,8 @@ func (p *Project) Clone() *Project { CommandLine: p.CommandLine, commandLineWithTypingsFiles: p.commandLineWithTypingsFiles, Program: p.Program, + incrementalProgram: p.incrementalProgram, + incremental: p.incremental, ProgramUpdateKind: ProgramUpdateKindNone, ProgramLastUpdate: p.ProgramLastUpdate, potentialProjectReferences: p.potentialProjectReferences, @@ -525,6 +555,10 @@ func (p *Project) CreateProgram() CreateProgramResult { // Create the command line, potentially augmented with typing files commandLine := p.getCommandLineWithTypingsFiles() + oldIncrementalProgram := p.incrementalProgram + if p.incremental && oldIncrementalProgram == nil { + oldIncrementalProgram = incremental.ReadBuildInfoProgram(commandLine, incremental.NewBuildInfoReader(p.host), p.host) + } if p.dirtyFilePath != "" && p.Program != nil && p.Program.CommandLine() == commandLine { var dirtyFile *ast.SourceFile newProgram, dirtyFile, programCloned = p.Program.UpdateProgram(p.dirtyFilePath, p.host, createCheckerPool, createModuleResolver) @@ -583,6 +617,9 @@ func (p *Project) CreateProgram() CreateProgramResult { } newProgram.BindSourceFiles() + if p.incremental { + p.incrementalProgram = incremental.NewProgram(newProgram, oldIncrementalProgram, incremental.CreateHost(p.host), nil, false) + } return CreateProgramResult{ Program: newProgram, diff --git a/tsc/internal/project/projectcollectionbuilder.go b/tsc/internal/project/projectcollectionbuilder.go index eb3752678485c..c2b9cfab383bf 100644 --- a/tsc/internal/project/projectcollectionbuilder.go +++ b/tsc/internal/project/projectcollectionbuilder.go @@ -302,6 +302,26 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque } b.deleteProject(project, logger) } + seenIncrementalOperations := collections.Set[SyntheticProjectID]{} + for _, request := range apiRequest.IncrementalOperations { + if seenIncrementalOperations.Has(request.ProgramID) { + return fmt.Errorf("incremental program operated on more than once: %s", request.ProgramID) + } + seenIncrementalOperations.Add(request.ProgramID) + if apiRequest.RemovePrograms.Has(request.ProgramID) { + return fmt.Errorf("incremental program cannot be operated on and removed: %s", request.ProgramID) + } + if request.Kind != APIIncrementalOperationKindEmit && request.Kind != APIIncrementalOperationKindEmitBuildInfo { + return fmt.Errorf("unsupported incremental operation: %s", request.Kind) + } + entry, ok := b.syntheticProjects.Load(request.ProgramID) + if !ok { + return fmt.Errorf("incremental program not found: %s", request.ProgramID) + } + if entry.Value().incrementalProgram == nil { + return fmt.Errorf("project is not incremental: %s", request.ProgramID) + } + } createdPrograms := make([]*Project, len(apiRequest.CreatePrograms)) createdEntries := make([]dirty.Value[*Project], len(apiRequest.CreatePrograms)) for i, request := range apiRequest.CreatePrograms { @@ -314,6 +334,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque request.ModuleResolverFactory, request.ModuleResolverID, b.inferredContentMappers, + &request.Incremental, logger, ) createdEntries[i] = entry @@ -329,6 +350,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque request.ModuleResolverFactory, request.ModuleResolverID, b.inferredContentMappers, + nil, logger, ) } @@ -365,6 +387,37 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque return true }) } + for _, request := range apiRequest.IncrementalOperations { + entry, _ := b.syntheticProjects.Load(request.ProgramID) + if entry.Value().dirty { + b.updateProgram(entry, logger) + } + var operationError error + entry.Change(func(project *Project) { + program := project.incrementalProgram.Fork() + options := compiler.EmitOptions{ + EmitOnly: request.EmitOnly, + WriteFile: request.WriteFile, + } + switch request.Kind { + case APIIncrementalOperationKindEmit: + request.Result = program.Emit(b.ctx, options) + case APIIncrementalOperationKindEmitBuildInfo: + request.Result = program.EmitBuildInfo(b.ctx, options) + } + if request.Result == nil { + operationError = b.ctx.Err() + if operationError == nil { + operationError = fmt.Errorf("incremental operation returned no result: %s", request.Kind) + } + return + } + project.incrementalProgram = program + }) + if operationError != nil { + return operationError + } + } var moduleResolutionError error b.forEachProject(func(entry dirty.Value[*Project]) bool { project := entry.Value() @@ -1292,11 +1345,13 @@ func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( moduleResolverFactory ModuleResolverFactory, moduleResolverID uint64, contentMappers []*contentmapper.Mapper, + incremental *bool, logger *logging.LogTree, ) *dirty.SyncMapEntry[SyntheticProjectID, *Project] { project, loaded := b.syntheticProjects.Load(projectID) if !loaded { syntheticProject := newSyntheticProject(projectID, b.sessionOptions.CurrentDirectory, compilerOptions, rootFileNames, projectReferences, contentMappers, b, logger) + syntheticProject.incremental = incremental != nil && *incremental syntheticProject.CommandLine.Errors = configFileParsingDiagnostics syntheticProject.moduleResolverFactory = moduleResolverFactory syntheticProject.moduleResolverID = moduleResolverID diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index 6040a639a2697..02768b470655d 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/contentmapper" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/ls" @@ -324,6 +325,7 @@ type APICreateProgramRequest struct { CompilerOptions *core.CompilerOptions ProjectReferences []*core.ProjectReference ConfigFileParsingDiagnostics []*ast.Diagnostic + Incremental bool ModuleResolverFactory ModuleResolverFactory ModuleResolverID uint64 } @@ -337,18 +339,35 @@ type APIReconfigureProgramRequest struct { APICreateProgramRequest } +type APIIncrementalOperationKind string + +const ( + APIIncrementalOperationKindEmit APIIncrementalOperationKind = "emit" + APIIncrementalOperationKindEmitBuildInfo APIIncrementalOperationKind = "emitBuildInfo" +) + +type APIIncrementalOperationRequest struct { + ProgramID SyntheticProjectID + Kind APIIncrementalOperationKind + EmitOnly compiler.EmitOnly + WriteFile func(fileName string, text string, data *compiler.WriteFileData) error + Result *compiler.EmitResult + EmittedFilesContents map[string]string +} + type APISnapshotRequest struct { - OpenProjects *collections.Set[string] - CloseProjects *collections.Set[tspath.Path] - OpenFiles map[tspath.Path]string - CloseFiles *collections.Set[tspath.Path] - CreatePrograms []*APICreateProgramRequest - ReconfigurePrograms []*APIReconfigureProgramRequest - RemovePrograms *collections.Set[SyntheticProjectID] - EnsurePrograms *collections.Set[ID] - EnsureAllPrograms bool - EnsureFiles map[tspath.Path]string - FileSystem vfs.FS + OpenProjects *collections.Set[string] + CloseProjects *collections.Set[tspath.Path] + OpenFiles map[tspath.Path]string + CloseFiles *collections.Set[tspath.Path] + CreatePrograms []*APICreateProgramRequest + ReconfigurePrograms []*APIReconfigureProgramRequest + RemovePrograms *collections.Set[SyntheticProjectID] + EnsurePrograms *collections.Set[ID] + EnsureAllPrograms bool + EnsureFiles map[tspath.Path]string + IncrementalOperations []*APIIncrementalOperationRequest + FileSystem vfs.FS // ReplaceFileSystem indicates a total filesystem replacement. Layers use // per-path file changes instead of invalidating all inherited state. ReplaceFileSystem bool