Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"packageManager": "npm@11.17.0+sha512.3eeaf18997b11070d313849268b23766b9db0068997dec9471073170fe43fa17f2b4d0337bf0f52330ee2274e7f5754b21b01052742e48f5c9c74d8b1e32ef43",
"volta": {
"node": "22.22.0",
"node": "24.20.0",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out we need Node 24 for running api-generators.test.ts

"npm": "11.17.0"
},
"allowScripts": {
Expand Down
96 changes: 88 additions & 8 deletions packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ import {
toPath,
} from "../path.ts";
import type {
APIFileChanges,
CompilerOptions,
CreateProgramOptions,
CreateProgramResponse,
Diagnostic,
DocumentIdentifier,
DocumentPosition,
Expand Down Expand Up @@ -138,6 +141,7 @@ export { formatDiagnostics, formatDiagnosticsWithColorAndContext } from "../diag
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";
export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind };
export type {
APIFileChanges,
APIImportAdderAction as ImportAdderAction,
APIOptions,
AssertsIdentifierTypePredicate,
Expand All @@ -151,6 +155,7 @@ export type {
CompletionInfo,
CompletionOptions,
ConditionalType,
CreateProgramOptions,
Diagnostic,
DocumentIdentifier,
DocumentPosition,
Expand Down Expand Up @@ -436,6 +441,60 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
resetTimingInfo(): Promise<void> {
return this.client.resetTimingInfo();
}

private isProgramActive(program: Program): boolean {
const project = program.getProject();
for (const snapshot of this.activeSnapshots) {
if (!snapshot.isDisposed() && snapshot.getProject(project.configFileName)?.program === program) {
return true;
}
}
return false;
}

/**
* Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges.
*/
async createProgram(
rootFiles: readonly DocumentIdentifier[],
createProgramOptions: CreateProgramOptions,
oldProgram?: Program,
fileChanges?: APIFileChanges,
): Promise<Program> {
await this.ensureInitialized();

if (fileChanges && !oldProgram) {
throw new Error("fileChanges requires an oldProgram");
}
if (oldProgram && !this.isProgramActive(oldProgram)) {
throw new Error("oldProgram must belong to this API instance and reference an active snapshot");
}

const data: CreateProgramResponse = await this.client.apiRequest("createProgram", {
rootFiles,
createProgramOptions,
...(oldProgram ? { oldProgram: { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } } : {}),
...(fileChanges ? { fileChanges } : {}),
Comment thread
gabritto marked this conversation as resolved.
});
if (!data.project) {
throw new Error("createProgram did not return a project");
}
const snapshot = new Snapshot(
{ snapshot: data.snapshot, projects: [data.project] },
this.client,
this.sourceFileCache,
this.toPath!,
this,
() => {
this.activeSnapshots.delete(snapshot);
this.sourceFileCache.releaseSnapshot(snapshot.id);
},
);
const program = snapshot.getProjects()[0].program;
program.setOwnedSnapshot(snapshot);
this.activeSnapshots.add(snapshot);
return program;
}
}

type EnsureInitialized = () => Promise<void>; // @sync: type EnsureInitialized = (() => void) & { gen(): Generator<ProtocolRequest, void, ProtocolResponse["result"]>; };
Expand Down Expand Up @@ -1003,14 +1062,16 @@ export class LanguageService {
}

export class Program implements FormatDiagnosticsHost {
private snapshotId: number;
private project: Project;
private client: Client;
private sourceFileCache: SourceFileCache;
private toPath: (fileName: string) => Path;
private formatDiagnosticsHost: FormatDiagnosticsHost;
private decoder = new Wtf8Decoder();
private sourceFileMetadataCache = new Map<Path, Promise<SourceFileMetadata | undefined>>();
/** @internal */
readonly snapshotId: number;
private readonly project: Project;
private readonly client: Client;
private readonly sourceFileCache: SourceFileCache;
private readonly toPath: (fileName: string) => Path;
private readonly formatDiagnosticsHost: FormatDiagnosticsHost;
private readonly decoder = new Wtf8Decoder();
private readonly sourceFileMetadataCache = new Map<Path, Promise<SourceFileMetadata | undefined>>();
private ownedSnapshot: Snapshot | undefined;

constructor(
snapshotId: number,
Expand Down Expand Up @@ -1040,6 +1101,21 @@ export class Program implements FormatDiagnosticsHost {
return this.project.compilerOptions.newLine === NewLineKind.CRLF ? "\r\n" : "\n";
}

/** @internal */
setOwnedSnapshot(snapshot: Snapshot): void {
this.ownedSnapshot = snapshot;
}

[globalThis.Symbol.dispose](): void {
this.dispose();
}

async dispose(): Promise<void> {
const snapshot = this.ownedSnapshot;
this.ownedSnapshot = undefined;
if (snapshot) await snapshot.dispose();
}

getCompilerOptions(): CompilerOptions {
return this.project.compilerOptions;
}
Expand Down Expand Up @@ -1330,6 +1406,10 @@ export class Program implements FormatDiagnosticsHost {
});
return toEmitOutput(response);
}

getProject(): Project {
return this.project;
}
}

function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput {
Expand Down
26 changes: 26 additions & 0 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface APIMethodInfo {
initialize: APIMethod<null, InitializeResponse>;
updateSnapshot: APIMethod<UpdateSnapshotParams, UpdateSnapshotResponse>;
updateTemporarySnapshot: APIMethod<UpdateTemporarySnapshotParams, UpdateSnapshotResponse>;
createProgram: APIMethod<CreateProgramParams, CreateProgramResponse>;
parseCommandLine: APIMethod<ParseCommandLineParams, ConfigFileResponse>;
readConfigFile: APIMethod<ReadConfigFileParams, ReadConfigFileResponse>;
parseJsonConfigFileContent: APIMethod<ParseJsonConfigFileContentParams, ConfigFileResponse>;
Expand Down Expand Up @@ -236,6 +237,18 @@ export interface UpdateTemporarySnapshotParams {
newText: string;
}

export interface CreateProgramParams {
rootFiles: readonly DocumentIdentifier[] | null;
createProgramOptions: CreateProgramOptions;
oldProgram?: CreateProgramOldProgramParams;
fileChanges?: APIFileChanges;
}

export interface CreateProgramResponse {
snapshot: number;
project: ProjectResponse | null;
}

export interface ParseCommandLineParams {
commandLine: readonly string[] | null;
}
Expand Down Expand Up @@ -882,6 +895,7 @@ export interface ProfileResult {
export interface BatchRequest {
method:
| "batchRequests"
| "createProgram"
| "emit"
| "emitToString"
| "formatNodeForInsertion"
Expand Down Expand Up @@ -1029,6 +1043,7 @@ export interface BatchRequest {
export interface BatchResponse {
method:
| "batchRequests"
| "createProgram"
| "emit"
| "emitToString"
| "formatNodeForInsertion"
Expand Down Expand Up @@ -1204,6 +1219,17 @@ export interface SnapshotChanges {
removedProjects?: string[];
}

export interface CreateProgramOptions {
compilerOptions: CompilerOptions;
projectReferences?: ProjectReference[];
configFileParsingDiagnostics?: DiagnosticResponse[];
}

export interface CreateProgramOldProgramParams {
snapshot?: number;
project?: string;
}

/** CompilerOptions contains the compiler options exposed by the API. */
export interface CompilerOptions {
allowJs?: boolean;
Expand Down
Loading