diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index ad787fb62495d..a8b9d79696d79 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -240,6 +240,14 @@ export class API { } } + async createBuildOrchestrator(host: ClientSpawnOptions, rootNames: readonly string[], defaultOptions: ParsedCommandLine): Promise { + await this.ensureInitialized(); + const orchestratorResponse = await this.client.apiRequest("createBuildOrchestrator", { hostOptions: host, rootNames, defaultOptions }); + + const orchestrator = new BuildOrchestrator(this.client, orchestratorResponse.buildOrchestratorID); + return orchestrator; + } + async parseConfigFile(file: DocumentIdentifier): Promise { await this.ensureInitialized(); return this.client.apiRequest("parseConfigFile", { file }); @@ -1261,6 +1269,44 @@ export class Program { } } +export class BuildOrchestrator { + private client: Client; + private id: number; + + constructor(client: Client, id: number) { + this.client = client; + this.id = id; + } + async build(project?: string): Promise { // , cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus{ + const response = await this.client.apiRequest("build", { + buildOrchestratorID: this.id, + ...(project !== undefined ? { project } : {}), + }); + return response.exitStatus; + } + async buildReferences(project: string): Promise { + const response = await this.client.apiRequest("buildReferences", { + buildOrchestratorID: this.id, + project, + }); + return response.exitStatus; + } + async clean(project?: string): Promise { + const response = await this.client.apiRequest("cleanBuild", { + buildOrchestratorID: this.id, + ...(project !== undefined ? { project } : {}), + }); + return response.exitStatus; + } + async cleanReferences(project?: string): Promise { + const response = await this.client.apiRequest("cleanReferences", { + buildOrchestratorID: this.id, + ...(project !== undefined ? { project } : {}), + }); + return response.exitStatus; + } +} + function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput { const outputFiles = new Map(); for (const { fileName, ...outputFile } of response.outputFiles) { diff --git a/packages/typescript/src/api/fs.ts b/packages/typescript/src/api/fs.ts index 0deb7b6a668e2..765bb7ec994b6 100644 --- a/packages/typescript/src/api/fs.ts +++ b/packages/typescript/src/api/fs.ts @@ -22,7 +22,7 @@ export interface FileSystem { } /** The callback names supported by the Go server for virtual FS delegation. */ -export const fsCallbackNames = ["readFile", "fileExists", "directoryExists", "getAccessibleEntries", "realpath", "writeFile"] as const; +export const fsCallbackNames = ["readFile", "fileExists", "directoryExists", "getAccessibleEntries", "realpath", "writeFile", "removeFile"] as const; interface VDirectory { type: "directory"; diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index 35c08679d2aa4..d9d368671f125 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -14,6 +14,11 @@ export interface APIMethodInfo { initialize: APIMethod; updateSnapshot: APIMethod; updateTemporarySnapshot: APIMethod; + createBuildOrchestrator: APIMethod; + build: APIMethod; + buildReferences: APIMethod; + cleanBuild: APIMethod; + cleanReferences: APIMethod; parseCommandLine: APIMethod; readConfigFile: APIMethod; parseJsonConfigFileContent: APIMethod; @@ -224,6 +229,34 @@ export interface UpdateTemporarySnapshotParams { newText: string; } +export interface CreateBuildOrchestratorParams { + hostOptions: BuildOrchestratorHostOptions; + rootNames: readonly string[] | null; + defaultOptions: ConfigFileResponse; +} + +export interface CreateBuildOrchestratorResponse { + buildOrchestratorID: number; +} + +export interface BuildParams { + buildOrchestratorID: number; + project?: string; +} + +export interface BuildResponse { + exitStatus: number; +} + +export interface CleanBuildParams { + buildOrchestratorID: number; + project?: string; +} + +export interface CleanBuildResponse { + exitStatus: number; +} + export interface ParseCommandLineParams { commandLine: readonly string[] | null; } @@ -231,6 +264,8 @@ export interface ParseCommandLineParams { export interface ConfigFileResponse { fileNames: string[]; options: CompilerOptions; + buildOptions?: BuildOptions; + watchOptions?: WatchOptions; projectReferences?: ProjectReference[]; typeAcquisition?: TypeAcquisition; compileOnSave?: boolean; @@ -888,6 +923,10 @@ export interface SnapshotChanges { removedProjects?: string[]; } +export interface BuildOrchestratorHostOptions { + cwd?: string; +} + /** CompilerOptions contains the compiler options exposed by the API. */ export interface CompilerOptions { allowJs?: boolean; @@ -994,6 +1033,26 @@ export interface CompilerOptions { configFilePath?: string; } +export interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + builders?: number; + stopBuildOnErrors?: boolean; + /** Internal fields */ + clean?: boolean; +} + +export interface WatchOptions { + watchInterval: number | null; + watchFile: number; + watchDirectory: number; + fallbackPolling: number; + synchronousWatchDirectory: boolean; + excludeDirectories: string[] | null; + excludeFiles: string[] | null; +} + export interface ProjectReference { /** Path is a normalized path on disk. */ path: string; diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 074471e86cfbb..e386aec9bbec4 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -20,6 +20,7 @@ import { SymbolFlags } from "#enums/symbolFlags"; import { TypeFlags } from "#enums/typeFlags"; import { TypeFormatFlags } from "#enums/typeFormatFlags"; import { TypePredicateKind } from "#enums/typePredicateKind"; +import type { CancellationToken } from "../../../vendor/vscode-jsonrpc/lib/common/cancellation.js"; import { type __String, type Declaration, @@ -220,6 +221,7 @@ export class API { private initialized: boolean = false; private activeSnapshots: Set = new Set(); private latestSnapshot: Snapshot | undefined; + private buildOrchestrators: Map = new Map(); readonly internal: InternalAPI; constructor(options: APIOptions | LSPConnectionOptions = {}) { @@ -248,6 +250,14 @@ export class API { } } + createBuildOrchestrator(host: ClientSpawnOptions, rootNames: readonly string[], defaultOptions: ParsedCommandLine): BuildOrchestrator { + this.ensureInitialized(); + const orchestratorResponse = this.client.apiRequest("createBuildOrchestrator", { hostOptions: host, rootNames, defaultOptions }); + + const orchestrator = new BuildOrchestrator(this.client, orchestratorResponse.buildOrchestratorID); + return orchestrator; + } + parseConfigFile(file: DocumentIdentifier): ParsedCommandLine { this.ensureInitialized(); return this.client.apiRequest("parseConfigFile", { file }); @@ -1269,6 +1279,45 @@ export class Program { } } +export class BuildOrchestrator { + private client: Client; + private id: number; + + constructor(client: Client, id: number) { + this.client = client; + this.id = id; + } + build(project?: string): number { // , cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus{ + const response = this.client.apiRequest("build", { + buildOrchestratorID: this.id, + ...(project !== undefined ? { project } : {}), + }); + return response.exitStatus; + } + buildReferences(project: string): number { + const response = this.client.apiRequest("buildReferences", { + buildOrchestratorID: this.id, + project, + }); + return response.exitStatus; + } + clean(project?: string): number { + const response = this.client.apiRequest("cleanBuild", { + buildOrchestratorID: this.id, + ...(project !== undefined ? { project } : {}), + }); + return response.exitStatus; + } + cleanReferences(project?: string): number { + const response = this.client.apiRequest("cleanReferences", { + buildOrchestratorID: this.id, + ...(project !== undefined ? { project } : {}), + }); + return response.exitStatus; + } + // getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; +} + function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput { const outputFiles = new Map(); for (const { fileName, ...outputFile } of response.outputFiles) { diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index fe08206bc486b..5ccb60a93f34e 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -380,6 +380,196 @@ describe("API", () => { }); }); +describe("BuildOrchestrator", () => { + const files = { + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/a/src/index.ts": `export const a = 1;`, + "/b/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/b/src/index.ts": `export const b = 2;`, + "/c/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + references: [{ path: "../a" }, { path: "../b" }], + }), + "/c/src/index.ts": `export const c = 3;`, + }; + + test("builds the configured root projects", async () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = await api.parseCommandLine([]); + const orchestrator = await api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/a/tsconfig.json", "/b/tsconfig.json"], + defaultOptions, + ); + + assert.equal(await orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + } + finally { + await api.close(); + } + }); + + test("rebuilds projects after multiple file system changes", async () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = await api.parseCommandLine([]); + const orchestrator = await api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/a/tsconfig.json", "/b/tsconfig.json"], + defaultOptions, + ); + + assert.equal(await orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + fs.writeFile!("/a/src/index.ts", `export const a = 10;`); + assert.equal(await orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 10/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + fs.writeFile!("/b/src/index.ts", `export const b = 20;`); + assert.equal(await orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 10/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 20/); + + fs.writeFile!("/a/src/index.ts", `export const a = 100;`); + fs.writeFile!("/b/src/index.ts", `export const b = 200;`); + assert.equal(await orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 100/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 200/); + } + finally { + await api.close(); + } + }); + + test("clean removes build outputs", async () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = await api.parseCommandLine([]); + const orchestrator = await api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/c/tsconfig.json"], + defaultOptions, + ); + + assert.equal(await orchestrator.build(), 0); + assert.ok(fs.readFile!("/c/dist/index.js")); + assert.ok(fs.readFile!("/b/dist/index.js")); + assert.ok(fs.readFile!("/a/dist/index.js")); + assert.equal(await orchestrator.clean(), 0); + assert.equal(fs.readFile!("/c/dist/index.js"), undefined); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + assert.equal(fs.readFile!("/a/dist/index.js"), undefined); + } + finally { + await api.close(); + } + }); + + test("builds and cleans selected projects after file system changes", async () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = await api.parseCommandLine([]); + const orchestrator = await api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/c/tsconfig.json"], + defaultOptions, + ); + + assert.equal(await orchestrator.build("/a/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + assert.equal(fs.readFile!("/c/dist/index.js"), undefined); + + fs.writeFile!("/a/src/index.ts", `export const a = 10;`); + assert.equal(await orchestrator.build("/b/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + assert.equal(fs.readFile!("/c/dist/index.js"), undefined); + + fs.writeFile!("/b/src/index.ts", `export const b = 20;`); + assert.equal(await orchestrator.clean("/a/tsconfig.json"), 0); + assert.equal(fs.readFile!("/a/dist/index.js"), undefined); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + assert.equal(fs.readFile!("/c/dist/index.js"), undefined); + + assert.equal(await orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 10/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + assert.match(fs.readFile!("/c/dist/index.js")!, /export const c = 3/); + + assert.equal(await orchestrator.clean("/b/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 10/); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + assert.match(fs.readFile!("/c/dist/index.js")!, /export const c = 3/); + } + finally { + await api.close(); + } + }); + + test("builds only references of a selected project", async () => { + const { api, fs } = spawnAPIWithFS({ + ...files, + }); + try { + const defaultOptions = await api.parseCommandLine([]); + const orchestrator = await api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/c/tsconfig.json"], + defaultOptions, + ); + + assert.equal(await orchestrator.buildReferences("/c/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + assert.equal(fs.readFile!("/c/dist/index.js"), undefined); + } + finally { + await api.close(); + } + }); + + test("cleans only references of a selected project", async () => { + const { api, fs } = spawnAPIWithFS({ + ...files, + }); + try { + const defaultOptions = await api.parseCommandLine([]); + const orchestrator = await api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/c/tsconfig.json"], + defaultOptions, + ); + + assert.equal(await orchestrator.build(), 0); + assert.ok(fs.readFile!("/a/dist/index.js")); + assert.ok(fs.readFile!("/b/dist/index.js")); + assert.ok(fs.readFile!("/c/dist/index.js")); + + assert.equal(await orchestrator.cleanReferences("/c/tsconfig.json"), 0); + assert.equal(fs.readFile!("/a/dist/index.js"), undefined); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + assert.ok(fs.readFile!("/c/dist/index.js")); + } + finally { + await api.close(); + } + }); +}); + describe("Checker - getImmediateAliasedSymbol", () => { test("resolves one level of alias indirection", async () => { const api = spawnAPI({ diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index 6974061a4a6fb..bddfb46d2a42b 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -388,6 +388,206 @@ describe("API", () => { }); }); +describe("BuildOrchestrator", () => { + const files = { + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/a/src/index.ts": `export const a = 1;`, + "/b/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/b/src/index.ts": `export const b = 2;`, + }; + + test("builds the configured root projects", () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = api.parseCommandLine([]); + const orchestrator = api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/a/tsconfig.json", "/b/tsconfig.json"], + defaultOptions, + ); + + assert.equal(orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + } + finally { + api.close(); + } + }); + + test("rebuilds projects after multiple file system changes", () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = api.parseCommandLine([]); + const orchestrator = api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/a/tsconfig.json", "/b/tsconfig.json"], + defaultOptions, + ); + + assert.equal(orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + fs.writeFile!("/a/src/index.ts", `export const a = 10;`); + assert.equal(orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 10/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + fs.writeFile!("/b/src/index.ts", `export const b = 20;`); + assert.equal(orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 10/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 20/); + + fs.writeFile!("/a/src/index.ts", `export const a = 100;`); + fs.writeFile!("/b/src/index.ts", `export const b = 200;`); + assert.equal(orchestrator.build(), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 100/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 200/); + } + finally { + api.close(); + } + }); + + test("clean removes build outputs", () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = api.parseCommandLine([]); + const orchestrator = api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/a/tsconfig.json"], + defaultOptions, + ); + + assert.equal(orchestrator.build(), 0); + assert.ok(fs.readFile!("/a/dist/index.js")); + assert.equal(orchestrator.clean(), 0); + assert.equal(fs.readFile!("/a/dist/index.js"), undefined); + } + finally { + api.close(); + } + }); + + test("builds and cleans selected projects after file system changes", () => { + const { api, fs } = spawnAPIWithFS({ ...files }); + try { + const defaultOptions = api.parseCommandLine([]); + const orchestrator = api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/a/tsconfig.json", "/b/tsconfig.json"], + defaultOptions, + ); + + assert.equal(orchestrator.build("/a/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + + fs.writeFile!("/a/src/index.ts", `export const a = 10;`); + assert.equal(orchestrator.build("/b/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + fs.writeFile!("/b/src/index.ts", `export const b = 20;`); + assert.equal(orchestrator.clean("/a/tsconfig.json"), 0); + assert.equal(fs.readFile!("/a/dist/index.js"), undefined); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + fs.writeFile!("/a/src/index.ts", `export const a = 100;`); + assert.equal(orchestrator.build("/a/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 100/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + + assert.equal(orchestrator.clean("/b/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 100/); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + + assert.equal(orchestrator.build("/b/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 100/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 20/); + } + finally { + api.close(); + } + }); + + test("builds only references of a selected project", () => { + const { api, fs } = spawnAPIWithFS({ + ...files, + "/c/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + references: [{ path: "../a" }, { path: "../b" }], + }), + "/c/src/index.ts": `export const c = 3;`, + }); + try { + const defaultOptions = api.parseCommandLine([]); + const orchestrator = api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/c/tsconfig.json"], + defaultOptions, + ); + + assert.equal(orchestrator.buildReferences("/c/tsconfig.json"), 0); + assert.match(fs.readFile!("/a/dist/index.js")!, /export const a = 1/); + assert.match(fs.readFile!("/b/dist/index.js")!, /export const b = 2/); + assert.equal(fs.readFile!("/c/dist/index.js"), undefined); + } + finally { + api.close(); + } + }); + + test("cleans only references of a selected project", () => { + const { api, fs } = spawnAPIWithFS({ + ...files, + "/a/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/b/tsconfig.json": JSON.stringify({ + compilerOptions: { composite: true, outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + }), + "/c/tsconfig.json": JSON.stringify({ + compilerOptions: { outDir: "dist", rootDir: "src" }, + files: ["src/index.ts"], + references: [{ path: "../a" }, { path: "../b" }], + }), + "/c/src/index.ts": `export const c = 3;`, + }); + try { + const defaultOptions = api.parseCommandLine([]); + const orchestrator = api.createBuildOrchestrator( + { cwd: "/", fs }, + ["/c/tsconfig.json"], + defaultOptions, + ); + + assert.equal(orchestrator.build(), 0); + assert.ok(fs.readFile!("/a/dist/index.js")); + assert.ok(fs.readFile!("/b/dist/index.js")); + assert.ok(fs.readFile!("/c/dist/index.js")); + + assert.equal(orchestrator.cleanReferences("/c/tsconfig.json"), 0); + assert.equal(fs.readFile!("/a/dist/index.js"), undefined); + assert.equal(fs.readFile!("/b/dist/index.js"), undefined); + assert.ok(fs.readFile!("/c/dist/index.js")); + } + finally { + api.close(); + } + }); +}); + describe("Checker - getImmediateAliasedSymbol", () => { test("resolves one level of alias indirection", () => { const api = spawnAPI({ diff --git a/tsc/internal/api/callbackfs.go b/tsc/internal/api/callbackfs.go index 9d2e1b9ce5659..c69e420275ad6 100644 --- a/tsc/internal/api/callbackfs.go +++ b/tsc/internal/api/callbackfs.go @@ -34,6 +34,7 @@ const ( callbackGetAccessibleEntries = "getAccessibleEntries" callbackRealpath = "realpath" callbackWriteFile = "writeFile" + callbackRemoveFile = "removeFile" ) func isCallbackName(name string) bool { @@ -43,7 +44,8 @@ func isCallbackName(name string) bool { callbackDirectoryExists, callbackGetAccessibleEntries, callbackRealpath, - callbackWriteFile: + callbackWriteFile, + callbackRemoveFile: return true default: return false @@ -221,8 +223,12 @@ func (fs *callbackFS) AppendFile(path string, data string) error { return fs.base.AppendFile(path, data) } -// Remove implements vfs.FS - always delegates to base (no callback support). +// Remove implements vfs.FS. func (fs *callbackFS) Remove(path string) error { + if fs.isEnabled(callbackRemoveFile) { + _, err := fs.call(callbackRemoveFile, path) + return err + } return fs.base.Remove(path) } diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 45793d96a61be..6aa6302045b9e 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -5,12 +5,14 @@ package api import ( "errors" "fmt" + "sync/atomic" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/checker" "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" "github.com/microsoft/TypeScript/tsc/internal/jsnum" "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/locale" @@ -30,14 +32,21 @@ var ( type Method string type ( - SnapshotID uint64 - ProjectID string - SymbolID uint64 - TypeID uint32 - SignatureID uint64 - NodeHandle string + SnapshotID uint64 + ProjectID string + BuildOrchestratorID uint64 + SymbolID uint64 + TypeID uint32 + SignatureID uint64 + NodeHandle string ) +var nextBuildOrchestratorId atomic.Uint64 + +func NewBuildOrchestratorID() BuildOrchestratorID { + return BuildOrchestratorID(nextBuildOrchestratorId.Add(1)) +} + func ProjectHandle(p *project.Project) ProjectID { return ProjectID(p.ID()) } @@ -64,6 +73,11 @@ const ( MethodInitialize Method = "initialize" MethodUpdateSnapshot Method = "updateSnapshot" MethodUpdateTemporarySnapshot Method = "updateTemporarySnapshot" + MethodCreateBuildOrchestrator Method = "createBuildOrchestrator" + MethodBuild Method = "build" + MethodBuildReferences Method = "buildReferences" + MethodCleanBuild Method = "cleanBuild" + MethodCleanReferences Method = "cleanReferences" MethodParseCommandLine Method = "parseCommandLine" MethodReadConfigFile Method = "readConfigFile" MethodParseJsonConfigFile Method = "parseJsonConfigFileContent" @@ -405,6 +419,11 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodInitialize: noParams, MethodUpdateSnapshot: unmarshallerFor[UpdateSnapshotParams], MethodUpdateTemporarySnapshot: unmarshallerFor[UpdateTemporarySnapshotParams], + MethodCreateBuildOrchestrator: unmarshallerFor[CreateBuildOrchestratorParams], + MethodBuild: unmarshallerFor[BuildParams], + MethodBuildReferences: unmarshallerFor[BuildParams], + MethodCleanBuild: unmarshallerFor[CleanBuildParams], + MethodCleanReferences: unmarshallerFor[CleanBuildParams], MethodParseCommandLine: unmarshallerFor[ParseCommandLineParams], MethodReadConfigFile: unmarshallerFor[ReadConfigFileParams], MethodParseJsonConfigFile: unmarshallerFor[ParseJsonConfigFileContentParams], @@ -623,9 +642,49 @@ type ProfileResult struct { File string `json:"file"` } +type CreateBuildOrchestratorParams struct { + HostOptions BuildOrchestratorHostOptions `json:"hostOptions"` + RootNames []string `json:"rootNames"` + ConfigFileResponse `json:"defaultOptions"` +} + +type BuildOrchestratorHostOptions struct { + Cwd string `json:"cwd,omitempty"` +} + +type CreateBuildOrchestratorResponse struct { + BuildOrchestratorID BuildOrchestratorID `json:"buildOrchestratorID"` +} + +type BuildParams struct { + BuildOrchestratorID BuildOrchestratorID `json:"buildOrchestratorID"` + Project ProjectID `json:"project,omitempty"` +} + +type BuildResponse struct { + tsc.ExitStatus `json:"exitStatus"` +} +type CleanBuildParams struct { + BuildOrchestratorID BuildOrchestratorID `json:"buildOrchestratorID"` + Project ProjectID `json:"project,omitempty"` +} + +type CleanBuildResponse struct { + tsc.ExitStatus `json:"exitStatus"` +} + +type BuildOrchestrator struct { + Build func(project ProjectID) tsc.ExitStatus //, cancellationToken *CancellationToken, writeFile WriteFileCallback, getCustomTransformers func(project string) CustomTransformers) + BuildReferences func(project ProjectID) tsc.ExitStatus //, cancellationToken *CancellationToken, writeFile WriteFileCallback, getCustomTransformers func(project string) CustomTransformers) + Clean func(project ProjectID) tsc.ExitStatus + CleanReferences func(project ProjectID) tsc.ExitStatus +} + type ConfigFileResponse struct { FileNames []string `json:"fileNames" nonnil:"true"` Options *core.CompilerOptions `json:"options" nonnil:"true"` + BuildOptions *core.BuildOptions `json:"buildOptions,omitempty"` + WatchOptions *core.WatchOptions `json:"watchOptions,omitempty"` ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` TypeAcquisition *core.TypeAcquisition `json:"typeAcquisition,omitempty"` CompileOnSave *bool `json:"compileOnSave,omitempty"` @@ -633,6 +692,14 @@ type ConfigFileResponse struct { Errors []*DiagnosticResponse `json:"errors" nonnil:"true"` } +func (c *ConfigFileResponse) toParsedCommandLine() *tsoptions.ParsedBuildCommandLine { + return &tsoptions.ParsedBuildCommandLine{ + CompilerOptions: c.Options, + BuildOptions: c.BuildOptions, + WatchOptions: c.WatchOptions, + } +} + type ReadConfigFileResponse struct { Config any `json:"config"` Error *DiagnosticResponse `json:"error,omitempty"` diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d5c668385b849..f1b3fdcf98a55 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -5,11 +5,13 @@ import ( "encoding/base64" "errors" "fmt" + "io" "slices" "strconv" "strings" "sync" "sync/atomic" + "time" "github.com/microsoft/TypeScript/tsc/internal/api/encoder" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -19,6 +21,8 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/compiler" "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/tsc" "github.com/microsoft/TypeScript/tsc/internal/format" "github.com/microsoft/TypeScript/tsc/internal/ipc" "github.com/microsoft/TypeScript/tsc/internal/json" @@ -33,6 +37,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/transpile" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" "github.com/microsoft/TypeScript/tsc/internal/tspath" + "github.com/microsoft/TypeScript/tsc/internal/vfs" ) var sessionIDCounter atomic.Uint64 @@ -407,6 +412,9 @@ type Session struct { // snapshots. Lock ordering is updateMu -> snapshotsMu (never the reverse). updateMu sync.Mutex + buildOrchestrators map[BuildOrchestratorID]*build.Orchestrator + buildMu sync.Mutex + cpuProfiler pprof.CPUProfiler } @@ -423,9 +431,10 @@ type SessionOptions struct { func NewSession(projectSession *project.Session, options *SessionOptions) *Session { id := sessionIDCounter.Add(1) s := &Session{ - id: formatSessionID(id), - projectSession: projectSession, - snapshots: make(map[SnapshotID]*snapshotData), + id: formatSessionID(id), + projectSession: projectSession, + snapshots: make(map[SnapshotID]*snapshotData), + buildOrchestrators: make(map[BuildOrchestratorID]*build.Orchestrator), } if options != nil { s.useBinaryResponses = options.UseBinaryResponses @@ -607,6 +616,16 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleUpdateSnapshot(ctx, parsed.(*UpdateSnapshotParams)) case string(MethodUpdateTemporarySnapshot): return s.handleUpdateTemporarySnapshot(ctx, parsed.(*UpdateTemporarySnapshotParams)) + case string(MethodCreateBuildOrchestrator): + return s.handleCreateBuildOrchestrator(ctx, parsed.(*CreateBuildOrchestratorParams)) + case string(MethodBuild): + return s.handleBuild(ctx, parsed.(*BuildParams)) + case string(MethodBuildReferences): + return s.handleBuildReferences(ctx, parsed.(*BuildParams)) + case string(MethodCleanBuild): + return s.handleCleanBuild(ctx, parsed.(*CleanBuildParams)) + case string(MethodCleanReferences): + return s.handleCleanReferences(ctx, parsed.(*CleanBuildParams)) case string(MethodParseCommandLine): return s.handleParseCommandLine(ctx, parsed.(*ParseCommandLineParams)) case string(MethodReadConfigFile): @@ -1159,6 +1178,97 @@ func (s *Session) handleGetDefaultProjectForFile(ctx context.Context, params *Ge return NewProjectResponse(proj), nil } +func (s *Session) handleCreateBuildOrchestrator(ctx context.Context, params *CreateBuildOrchestratorParams) (*CreateBuildOrchestratorResponse, error) { + command := tsoptions.ParseBuildCommandLine(params.RootNames, s.projectSession) + if params.Options != nil { + command.CompilerOptions = params.Options + } + if params.BuildOptions != nil { + command.BuildOptions = params.BuildOptions + } + if params.WatchOptions != nil { + command.WatchOptions = params.WatchOptions + } + orchestrator := build.NewOrchestrator(build.Options{ + Sys: s.getBuildSys(params), + Command: command, + }) + orchestratorId := NewBuildOrchestratorID() + s.buildMu.Lock() + s.buildOrchestrators[orchestratorId] = orchestrator + s.buildMu.Unlock() + return &CreateBuildOrchestratorResponse{ + BuildOrchestratorID: orchestratorId, + }, nil +} + +func (s *Session) getBuildSys(params *CreateBuildOrchestratorParams) tsc.System { + currentDirectory := params.HostOptions.Cwd + if currentDirectory == "" { + currentDirectory = s.projectSession.GetCurrentDirectory() + } + return &apiBuildSystem{ + session: s.projectSession, + currentDirectory: currentDirectory, + start: time.Now(), + } +} + +func (s *Session) handleBuild(ctx context.Context, params *BuildParams) (*BuildResponse, error) { + s.buildMu.Lock() + defer s.buildMu.Unlock() + return &BuildResponse{ + ExitStatus: s.buildOrchestrators[params.BuildOrchestratorID].Build(ctx, string(params.Project)).Status, + }, nil +} + +func (s *Session) handleBuildReferences(ctx context.Context, params *BuildParams) (*BuildResponse, error) { + s.buildMu.Lock() + defer s.buildMu.Unlock() + return &BuildResponse{ + ExitStatus: s.buildOrchestrators[params.BuildOrchestratorID].BuildReferences(ctx, string(params.Project)).Status, + }, nil +} + +func (s *Session) handleCleanBuild(ctx context.Context, params *CleanBuildParams) (*CleanBuildResponse, error) { + s.buildMu.Lock() + defer s.buildMu.Unlock() + return &CleanBuildResponse{ + ExitStatus: s.buildOrchestrators[params.BuildOrchestratorID].Clean(string(params.Project)), + }, nil +} + +func (s *Session) handleCleanReferences(ctx context.Context, params *CleanBuildParams) (*CleanBuildResponse, error) { + s.buildMu.Lock() + defer s.buildMu.Unlock() + return &CleanBuildResponse{ + ExitStatus: s.buildOrchestrators[params.BuildOrchestratorID].CleanReferences(string(params.Project)), + }, nil +} + +type apiBuildSystem struct { + session *project.Session + currentDirectory string + start time.Time +} + +func (s *apiBuildSystem) Writer() io.Writer { return io.Discard } +func (s *apiBuildSystem) ErrorWriter() io.Writer { return io.Discard } +func (s *apiBuildSystem) FS() vfs.FS { return s.session.FS() } +func (s *apiBuildSystem) DefaultLibraryPath() string { return s.session.DefaultLibraryPath() } +func (s *apiBuildSystem) GetCurrentDirectory() string { return s.currentDirectory } +func (s *apiBuildSystem) WriteOutputIsTTY() bool { return false } +func (s *apiBuildSystem) GetWidthOfTerminal() int { return 0 } +func (s *apiBuildSystem) GetEnvironmentVariable(name string) (string, bool) { + return "", false +} + +func (s *apiBuildSystem) Spawn(command []string, dir string, stderr io.Writer) (io.ReadWriteCloser, error) { + return nil, errors.New("spawning processes is not supported by the API build orchestrator") +} +func (s *apiBuildSystem) Now() time.Time { return time.Now() } +func (s *apiBuildSystem) SinceStart() time.Duration { return time.Since(s.start) } + // handleParseCommandLine parses command-line arguments. func (s *Session) handleParseCommandLine(ctx context.Context, params *ParseCommandLineParams) (*ConfigFileResponse, error) { return NewConfigFileResponse(tsoptions.ParseCommandLine(params.CommandLine, s.projectSession)), nil diff --git a/tsc/internal/execute/build/clean_test.go b/tsc/internal/execute/build/clean_test.go new file mode 100644 index 0000000000000..07297d951072b --- /dev/null +++ b/tsc/internal/execute/build/clean_test.go @@ -0,0 +1,115 @@ +package build_test + +import ( + "io" + "strings" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/execute/build" + "github.com/microsoft/TypeScript/tsc/internal/execute/tsc" + "github.com/microsoft/TypeScript/tsc/internal/execute/tsctests" + "github.com/microsoft/TypeScript/tsc/internal/tsoptions" + "gotest.tools/v3/assert" +) + +func TestClean(t *testing.T) { + t.Parallel() + + t.Run("cleans selected project and references", func(t *testing.T) { + t.Parallel() + sys := newCleanTestSystem() + orchestrator := newCleanTestOrchestrator(sys, "a", "c") + + assert.Equal(t, orchestrator.Clean("a"), tsc.ExitStatusSuccess) + assert.Assert(t, !sys.FS().FileExists("/project/a/dist/index.js")) + assert.Assert(t, !sys.FS().FileExists("/project/b/dist/index.js")) + assert.Assert(t, sys.FS().FileExists("/project/c/dist/index.js")) + }) + + t.Run("dry run preserves outputs", func(t *testing.T) { + t.Parallel() + sys := newCleanTestSystem() + orchestrator := newCleanTestOrchestrator(sys, "--dry", "a") + + assert.Equal(t, orchestrator.Clean("a"), tsc.ExitStatusSuccess) + assert.Assert(t, sys.FS().FileExists("/project/a/dist/index.js")) + assert.Assert(t, sys.FS().FileExists("/project/b/dist/index.js")) + }) + + t.Run("rejects project outside build", func(t *testing.T) { + t.Parallel() + sys := newCleanTestSystem() + orchestrator := newCleanTestOrchestrator(sys, "a") + + assert.Equal(t, orchestrator.Clean("c"), tsc.ExitStatusInvalidProject_OutputsSkipped) + assert.Assert(t, sys.FS().FileExists("/project/a/dist/index.js")) + assert.Assert(t, sys.FS().FileExists("/project/b/dist/index.js")) + assert.Assert(t, sys.FS().FileExists("/project/c/dist/index.js")) + }) + + t.Run("rejects circular build", func(t *testing.T) { + t.Parallel() + sys := newCleanTestSystem() + orchestrator := newCleanTestOrchestrator(sys, "cycle1") + + assert.Equal(t, orchestrator.Clean("cycle1"), tsc.ExitStatusProjectReferenceCycle_OutputsSkipped) + assert.Assert(t, sys.FS().FileExists("/project/cycle1/dist/index.js")) + assert.Assert(t, sys.FS().FileExists("/project/cycle2/dist/index.js")) + }) +} + +type cleanTestSystem struct { + *tsctests.TestSys + output strings.Builder +} + +func (s *cleanTestSystem) Writer() io.Writer { + return &s.output +} + +func (s *cleanTestSystem) ErrorWriter() io.Writer { + return &s.output +} + +func newCleanTestSystem() *cleanTestSystem { + return &cleanTestSystem{TestSys: tsctests.NewTscSystem(tsctests.FileMap{ + "/project/a/tsconfig.json": `{ + "compilerOptions": { "composite": true, "noLib": true, "outDir": "dist" }, + "files": ["index.ts"], + "references": [{ "path": "../b" }] + }`, + "/project/a/index.ts": "export const a = 1;", + "/project/a/dist/index.js": "export const a = 1;", + "/project/a/dist/index.d.ts": "export declare const a = 1;", + "/project/b/tsconfig.json": `{ "compilerOptions": { "composite": true, "noLib": true, "outDir": "dist" }, "files": ["index.ts"] }`, + "/project/b/index.ts": "export const b = 1;", + "/project/b/dist/index.js": "export const b = 1;", + "/project/b/dist/index.d.ts": "export declare const b = 1;", + "/project/c/tsconfig.json": `{ "compilerOptions": { "composite": true, "noLib": true, "outDir": "dist" }, "files": ["index.ts"] }`, + "/project/c/index.ts": "export const c = 1;", + "/project/c/dist/index.js": "export const c = 1;", + "/project/c/dist/index.d.ts": "export declare const c = 1;", + "/project/cycle1/tsconfig.json": `{ + "compilerOptions": { "composite": true, "noLib": true, "outDir": "dist" }, + "files": ["index.ts"], + "references": [{ "path": "../cycle2" }] + }`, + "/project/cycle1/index.ts": "export const cycle1 = 1;", + "/project/cycle1/dist/index.js": "export const cycle1 = 1;", + "/project/cycle2/tsconfig.json": `{ + "compilerOptions": { "composite": true, "noLib": true, "outDir": "dist" }, + "files": ["index.ts"], + "references": [{ "path": "../cycle1" }] + }`, + "/project/cycle2/index.ts": "export const cycle2 = 1;", + "/project/cycle2/dist/index.js": "export const cycle2 = 1;", + }, true, "/project")} +} + +func newCleanTestOrchestrator(sys tsc.System, args ...string) *build.Orchestrator { + command := tsoptions.ParseBuildCommandLine(append([]string{"--build"}, args...), sys) + return build.NewOrchestrator(build.Options{ + Sys: sys, + Command: command, + }) +} diff --git a/tsc/internal/execute/build/orchestrator.go b/tsc/internal/execute/build/orchestrator.go index a5ca48c30c234..ac7b85de5aa35 100644 --- a/tsc/internal/execute/build/orchestrator.go +++ b/tsc/internal/execute/build/orchestrator.go @@ -29,7 +29,6 @@ type Options struct { Command *tsoptions.ParsedBuildCommandLine Testing tsc.CommandLineTesting } - type orchestratorResult struct { result tsc.CommandLineResult errors []*ast.Diagnostic @@ -71,9 +70,10 @@ type Orchestrator struct { contentMapperHost contentmapper.Host // order generation result - tasks *collections.SyncMap[tspath.Path, *BuildTask] - order []string - errors []*ast.Diagnostic + tasks *collections.SyncMap[tspath.Path, *BuildTask] + order []string + errors []*ast.Diagnostic + graphGenerated bool errorSummaryReporter tsc.DiagnosticsReporter watchStatusReporter tsc.DiagnosticReporter @@ -242,9 +242,27 @@ func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.Path, return true }) } + o.graphGenerated = true } +// tsc -b entrypoint func (o *Orchestrator) Start(ctx context.Context) tsc.CommandLineResult { + return o.start(ctx, "", false) +} + +// orchestrator.Build() entrypoint for api +func (o *Orchestrator) Build(ctx context.Context, project string) tsc.CommandLineResult { + o.recheckAllProjects(project) + return o.start(ctx, project, false) +} + +// orchestrator.BuildReferences() entrypoint for api +func (o *Orchestrator) BuildReferences(ctx context.Context, project string) tsc.CommandLineResult { + o.recheckAllProjects(project) + return o.start(ctx, project, true) +} + +func (o *Orchestrator) start(ctx context.Context, project string, onlyReferences bool) tsc.CommandLineResult { o.contentMapperHost = tsc.NewContentMapperHost(ctx, o.opts.Sys, o.opts.Command.CompilerOptions) if o.contentMapperHost != nil && (!o.opts.Command.CompilerOptions.Watch.IsTrue() || o.opts.Testing == nil) { defer o.contentMapperHost.Close() @@ -252,8 +270,22 @@ func (o *Orchestrator) Start(ctx context.Context) tsc.CommandLineResult { if o.opts.Command.CompilerOptions.Watch.IsTrue() { o.watchStatusReporter(ast.NewCompilerDiagnostic(diagnostics.Starting_compilation_in_watch_mode)) } - o.GenerateGraph(nil) - result := o.buildOrClean() + if o.graphGenerated { + o.GenerateGraphReusingOldTasks() + } else { + o.GenerateGraph(nil) + } + order, ok := o.getBuildOrderFor(project) + if !ok { + return tsc.CommandLineResult{Status: tsc.ExitStatusInvalidProject_OutputsSkipped} + } + if onlyReferences && len(o.errors) == 0 { + if project == "" { + return tsc.CommandLineResult{Status: tsc.ExitStatusInvalidProject_OutputsSkipped} + } + order = order[:len(order)-1] + } + result := o.buildOrCleanOrder(order) if o.opts.Command.CompilerOptions.Watch.IsTrue() { o.Watch(ctx) result.Watcher = o @@ -261,6 +293,149 @@ func (o *Orchestrator) Start(ctx context.Context) tsc.CommandLineResult { return result } +func (o *Orchestrator) recheckAllProjects(project string) { + if !o.graphGenerated { + return + } + order, ok := o.getBuildOrderFor(project) + if !ok { + return + } + o.rangeTasks(order, func(path tspath.Path, task *BuildTask) { + task.resetStatus() + }) + o.host.mTimes = &collections.SyncMap[tspath.Path, time.Time]{} + o.resetCaches() +} + +// orchestrator.Clean() entrypoint for api +func (o *Orchestrator) Clean(project string) tsc.ExitStatus { + return o.clean(project, false) +} + +// orchestrator.CleanReferences() entrypoint for api +func (o *Orchestrator) CleanReferences(project string) tsc.ExitStatus { + return o.clean(project, true) +} + +func (o *Orchestrator) clean(project string, onlyReferences bool) tsc.ExitStatus { + if !o.graphGenerated { + o.GenerateGraph(nil) + } + if len(o.errors) != 0 { + reportDiagnostic := o.createDiagnosticReporter(nil) + for _, err := range o.errors { + reportDiagnostic(err) + } + return tsc.ExitStatusProjectReferenceCycle_OutputsSkipped + } + + order, ok := o.getBuildOrderFor(project) + if !ok { + return tsc.ExitStatusInvalidProject_OutputsSkipped + } + if onlyReferences { + if project == "" { + return tsc.ExitStatusInvalidProject_OutputsSkipped + } + order = order[:len(order)-1] + } + + dry := o.opts.Command.BuildOptions.Dry.IsTrue() + var filesToDelete []string + reportDiagnostic := o.createDiagnosticReporter(nil) + for _, config := range order { + task := o.getTask(o.toPath(config)) + if task.resolved == nil { + reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.File_0_not_found, task.config)) + continue + } + + inputs := collections.NewSetFromItems(core.Map(task.resolved.FileNames(), o.toPath)...) + projectOutputs := task.resolved.GetOutputFileNames() + deleted := false + for outputFile := range projectOutputs { + deleted = o.cleanProjectOutput(outputFile, inputs, dry, &filesToDelete, reportDiagnostic) || deleted + } + deleted = o.cleanProjectOutput(task.resolved.GetBuildInfoFileName(), inputs, dry, &filesToDelete, reportDiagnostic) || deleted + if deleted { + task.resetStatus() + task.buildInfoEntryMu.Lock() + task.buildInfoEntry = nil + task.buildInfoEntryMu.Unlock() + } + } + + if dry { + o.createBuilderStatusReporter(nil)(ast.NewCompilerDiagnostic( + diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, + strings.Join(core.Map(filesToDelete, func(file string) string { + return "\r\n * " + file + }), ""), + )) + } else { + o.resetCaches() + } + return tsc.ExitStatusSuccess +} + +func (o *Orchestrator) getBuildOrderFor(project string) ([]string, bool) { + if project == "" { + return o.order, true + } + + config := core.ResolveConfigFileNameOfProjectReference( + tspath.ResolvePath(o.opts.Sys.GetCurrentDirectory(), project), + ) + target, ok := o.tasks.Load(o.toPath(config)) + if !ok { + return nil, false + } + + projects := collections.Set[tspath.Path]{} + var addProjectAndReferences func(*BuildTask) + addProjectAndReferences = func(task *BuildTask) { + path := o.toPath(task.config) + if projects.Has(path) { + return + } + projects.Add(path) + for _, upstream := range task.upStream { + addProjectAndReferences(upstream.task) + } + } + addProjectAndReferences(target) + + order := make([]string, 0, len(projects.M)) + for _, config := range o.order { + if projects.Has(o.toPath(config)) { + order = append(order, config) + } + } + return order, true +} + +func (o *Orchestrator) cleanProjectOutput( + outputFile string, + inputs *collections.Set[tspath.Path], + dry bool, + filesToDelete *[]string, + reportDiagnostic tsc.DiagnosticReporter, +) bool { + if outputFile == "" || inputs.Has(o.toPath(outputFile)) || !o.host.FS().FileExists(outputFile) { + return false + } + if dry { + *filesToDelete = append(*filesToDelete, outputFile) + return false + } + if err := o.host.FS().Remove(outputFile); err != nil { + reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Failed_to_delete_file_0, outputFile)) + return false + } + return true +} + func (o *Orchestrator) Watch(ctx context.Context) { o.wm.Lock() @@ -658,18 +833,28 @@ func (o *Orchestrator) DoCycle() { } func (o *Orchestrator) buildOrClean() tsc.CommandLineResult { + return o.buildOrCleanOrder(o.order) +} + +func (o *Orchestrator) buildOrCleanOrder(order []string) tsc.CommandLineResult { if !o.opts.Command.BuildOptions.Clean.IsTrue() && o.opts.Command.BuildOptions.Verbose.IsTrue() { o.createBuilderStatusReporter(nil)(ast.NewCompilerDiagnostic( diagnostics.Projects_in_this_build_Colon_0, - strings.Join(core.Map(o.Order(), func(p string) string { + strings.Join(core.Map(order, func(p string) string { return "\r\n * " + o.relativeFileName(p) }), ""), )) } var buildResult orchestratorResult if len(o.errors) == 0 { - buildResult.statistics.Projects = len(o.Order()) - o.rangeTask(func(path tspath.Path, task *BuildTask) { + buildResult.statistics.Projects = len(order) + var prevReporter *BuildTask + for _, config := range order { + task := o.getTask(o.toPath(config)) + task.prevReporter = prevReporter + prevReporter = task + } + o.rangeTasks(order, func(path tspath.Path, task *BuildTask) { o.buildOrCleanProject(task, path, &buildResult) }) } else { @@ -686,6 +871,10 @@ func (o *Orchestrator) buildOrClean() tsc.CommandLineResult { } func (o *Orchestrator) rangeTask(f func(path tspath.Path, task *BuildTask)) { + o.rangeTasks(o.order, f) +} + +func (o *Orchestrator) rangeTasks(order []string, f func(path tspath.Path, task *BuildTask)) { numRoutines := 4 if o.opts.Command.CompilerOptions.SingleThreaded.IsTrue() { numRoutines = 1 @@ -696,10 +885,10 @@ func (o *Orchestrator) rangeTask(f func(path tspath.Path, task *BuildTask)) { var currentTaskIndex atomic.Int64 getNextTask := func() (tspath.Path, *BuildTask, bool) { index := int(currentTaskIndex.Add(1) - 1) - if index >= len(o.order) { + if index >= len(order) { return "", nil, false } - config := o.order[index] + config := order[index] path := o.toPath(config) task := o.getTask(path) return path, task, true diff --git a/tsc/internal/project/session.go b/tsc/internal/project/session.go index d19fa70d16645..353f451148a7f 100644 --- a/tsc/internal/project/session.go +++ b/tsc/internal/project/session.go @@ -320,6 +320,10 @@ func (s *Session) GetCurrentDirectory() string { return s.options.CurrentDirectory } +func (s *Session) DefaultLibraryPath() string { + return s.options.DefaultLibraryPath +} + // Gets copy of current configuration func (s *Session) Config() lsutil.UserPreferences { s.userConfigRWMu.Lock()