diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index d66874f3a41..f30800c100d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -264,15 +264,10 @@ namespace ts { export interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } - export interface SolutionBuilderResult { - project: ResolvedConfigFileName; - result: T; - } - - export interface SolutionBuilder { + export interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; - buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined; + getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; // Currently used for testing but can be made public if needed: /*@internal*/ getBuildOrder(): ReadonlyArray; @@ -325,11 +320,11 @@ namespace ts { return result; } - export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { + export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions); } - export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { + export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); } @@ -380,6 +375,7 @@ namespace ts { allProjectBuildPending: boolean; needsSummary: boolean; watchAllProjectsPending: boolean; + currentInvalidatedProject: InvalidatedProject | undefined; // Watch state readonly watch: boolean; @@ -452,6 +448,7 @@ namespace ts { allProjectBuildPending: true, needsSummary: true, watchAllProjectsPending: watch, + currentInvalidatedProject: undefined, // Watch state watch, @@ -654,28 +651,31 @@ namespace ts { } } - const enum InvalidatedProjectKind { + export enum InvalidatedProjectKind { Build, UpdateBundle, UpdateOutputFileStamps } - interface InvalidatedProjectBase { + export interface InvalidatedProjectBase { readonly kind: InvalidatedProjectKind; readonly project: ResolvedConfigFileName; - readonly projectPath: ResolvedConfigFilePath; + /*@internal*/ readonly projectPath: ResolvedConfigFilePath; + /*@internal*/ readonly buildOrder: readonly ResolvedConfigFileName[]; /** * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly */ - done(cancellationToken?: CancellationToken): void; + done(cancellationToken?: CancellationToken): ExitStatus; + getCompilerOptions(): CompilerOptions; + getCurrentDirectory(): string; } - interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { + export interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; updateOutputFileStatmps(): void; } - interface BuildInvalidedProject extends InvalidatedProjectBase { + export interface BuildInvalidedProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.Build; /* * Emitting with this builder program without the api provided for this project @@ -683,7 +683,6 @@ namespace ts { */ getBuilderProgram(): T | undefined; getProgram(): Program | undefined; - getCompilerOptions(): CompilerOptions; getSourceFile(fileName: string): SourceFile | undefined; getSourceFiles(): ReadonlyArray; getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; @@ -704,22 +703,41 @@ namespace ts { emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined; // TODO(shkamat):: investigate later if we can emit even when there are declaration diagnostics // emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; - getCurrentDirectory(): string; } - interface UpdateBundleProject extends InvalidatedProjectBase { + export interface UpdateBundleProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.UpdateBundle; emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; } - type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; + export type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; - function createUpdateOutputFileStampsProject(state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, config: ParsedCommandLine): UpdateOutputFileStampsProject { + function doneInvalidatedProject( + state: SolutionBuilderState, + projectPath: ResolvedConfigFilePath + ) { + state.projectPendingBuild.delete(projectPath); + state.currentInvalidatedProject = undefined; + return state.diagnostics.has(projectPath) ? + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; + } + + function createUpdateOutputFileStampsProject( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[] + ): UpdateOutputFileStampsProject { let updateOutputFileStampsPending = true; return { kind: InvalidatedProjectKind.UpdateOutputFileStamps, project, projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, updateOutputFileStatmps: () => { updateOutputTimestamps(state, config, projectPath); updateOutputFileStampsPending = false; @@ -728,7 +746,7 @@ namespace ts { if (updateOutputFileStampsPending) { updateOutputTimestamps(state, config, projectPath); } - state.projectPendingBuild.delete(projectPath); + return doneInvalidatedProject(state, projectPath); } }; } @@ -763,12 +781,14 @@ namespace ts { kind, project, projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, getBuilderProgram: () => withProgramOrUndefined(identity), getProgram: () => withProgramOrUndefined( program => program.getProgramOrUndefined() ), - getCompilerOptions: () => config.options, getSourceFile: fileName => withProgramOrUndefined( program => program.getSourceFile(fileName) @@ -817,26 +837,27 @@ namespace ts { if (step !== Step.Emit) return undefined; return emit(writeFile, cancellationToken, customTransformers); }, - getCurrentDirectory: () => state.currentDirectory, - done: cancellationToken => { - executeSteps(Step.Done, cancellationToken); - state.projectPendingBuild.delete(projectPath); - } + done } : { kind, project, projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, emit: (writeFile: WriteFileCallback | undefined, customTransformers: CustomTransformers | undefined) => { if (step !== Step.EmitBundle) return invalidatedProjectOfBundle; return emitBundle(writeFile, customTransformers); }, - done: cancellationToken => { - executeSteps(Step.Done, cancellationToken); - state.projectPendingBuild.delete(projectPath); - } + done, }; + function done(cancellationToken?: CancellationToken) { + executeSteps(Step.Done, cancellationToken); + return doneInvalidatedProject(state, projectPath); + } + function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { executeSteps(Step.CreateProgram); return program && action(program); @@ -1152,6 +1173,12 @@ namespace ts { function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { if (!state.projectPendingBuild.size) return undefined; + if (state.currentInvalidatedProject) { + // Only if same buildOrder the currentInvalidated project can be sent again + return arrayIsEqualTo(state.currentInvalidatedProject.buildOrder, buildOrder) ? + state.currentInvalidatedProject : + undefined; + } const { options, projectPendingBuild } = state; for (let projectIndex = 0; projectIndex < buildOrder.length; projectIndex++) { @@ -1200,7 +1227,8 @@ namespace ts { state, project, projectPath, - config + config, + buildOrder ); } } @@ -1635,20 +1663,6 @@ namespace ts { } } - function buildNextProject(state: SolutionBuilderState, cancellationToken?: CancellationToken): SolutionBuilderResult | undefined { - setupInitialBuild(state, cancellationToken); - const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); - if (!invalidatedProject) return undefined; - - invalidatedProject.done(cancellationToken); - return { - project: invalidatedProject.project, - result: state.diagnostics.has(invalidatedProject.projectPath) ? - ExitStatus.DiagnosticsPresent_OutputsSkipped : - ExitStatus.Success - }; - } - function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken): ExitStatus { const buildOrder = getBuildOrderFor(state, project); if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; @@ -1883,14 +1897,17 @@ namespace ts { * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references */ - function createSolutionBuilderWorker(watch: false, host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, options: BuildOptions): SolutionBuilder { + function createSolutionBuilderWorker(watch: false, host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, options: BuildOptions): SolutionBuilder { const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options); return { build: (project, cancellationToken) => build(state, project, cancellationToken), clean: project => clean(state, project), - buildNextProject: cancellationToken => buildNextProject(state, cancellationToken), + getNextInvalidatedProject: cancellationToken => { + setupInitialBuild(state, cancellationToken); + return getNextInvalidatedProject(state, getBuildOrder(state)); + }, getBuildOrder: () => getBuildOrder(state), getUpToDateStatusOfProject: project => { const configFileName = resolveProjectName(state, project); diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 056e894bf53..130ebd9b8eb 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -350,7 +350,12 @@ namespace ts { assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); }); - it("building using buildNextProject", () => { + it("building using getNextInvalidatedProject", () => { + interface SolutionBuilderResult { + project: ResolvedConfigFileName; + result: T; + } + const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], {}); @@ -376,8 +381,9 @@ namespace ts { presentOutputs: readonly string[], absentOutputs: readonly string[] ) { - const result = builder.buildNextProject(); - assert.deepEqual(result, expected); + const project = builder.getNextInvalidatedProject(); + const result = project && project.done(); + assert.deepEqual(project && { project: project.project, result }, expected); verifyOutputsPresent(fs, presentOutputs); verifyOutputsAbsent(fs, absentOutputs); } diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index f67b575e979..54f174356b5 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -676,7 +676,7 @@ let x: string = 10;`); } function verifyScenario( - edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, + edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, expectedFilesAfterEdit: ReadonlyArray ) { it("with tsc-watch", () => { @@ -899,7 +899,7 @@ export function gfoo() { } function verifyScenario( - edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, + edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, expectedEditErrors: ReadonlyArray, expectedProgramFiles: ReadonlyArray, expectedWatchedFiles: ReadonlyArray, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index df479429c17..05d823728e4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4594,14 +4594,10 @@ declare namespace ts { } interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } - interface SolutionBuilderResult { - project: ResolvedConfigFileName; - result: T; - } - interface SolutionBuilder { + interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; - buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined; + getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; } /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic @@ -4609,8 +4605,47 @@ declare namespace ts { function createBuilderStatusReporter(system: System, pretty?: boolean): DiagnosticReporter; function createSolutionBuilderHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary): SolutionBuilderHost; function createSolutionBuilderWithWatchHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): SolutionBuilderWithWatchHost; - function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + enum InvalidatedProjectKind { + Build = 0, + UpdateBundle = 1, + UpdateOutputFileStamps = 2 + } + interface InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind; + readonly project: ResolvedConfigFileName; + /** + * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly + */ + done(cancellationToken?: CancellationToken): ExitStatus; + getCompilerOptions(): CompilerOptions; + getCurrentDirectory(): string; + } + interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; + updateOutputFileStatmps(): void; + } + interface BuildInvalidedProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.Build; + getBuilderProgram(): T | undefined; + getProgram(): Program | undefined; + getSourceFile(fileName: string): SourceFile | undefined; + getSourceFiles(): ReadonlyArray; + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getConfigFileParsingDiagnostics(): ReadonlyArray; + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getAllDependencies(sourceFile: SourceFile): ReadonlyArray; + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined; + } + interface UpdateBundleProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateBundle; + emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; + } + type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; } declare namespace ts.server { type ActionSet = "action::set"; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 4062969e8cd..357186f140d 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4594,14 +4594,10 @@ declare namespace ts { } interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } - interface SolutionBuilderResult { - project: ResolvedConfigFileName; - result: T; - } - interface SolutionBuilder { + interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; - buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined; + getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; } /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic @@ -4609,8 +4605,47 @@ declare namespace ts { function createBuilderStatusReporter(system: System, pretty?: boolean): DiagnosticReporter; function createSolutionBuilderHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary): SolutionBuilderHost; function createSolutionBuilderWithWatchHost(system?: System, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter): SolutionBuilderWithWatchHost; - function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + enum InvalidatedProjectKind { + Build = 0, + UpdateBundle = 1, + UpdateOutputFileStamps = 2 + } + interface InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind; + readonly project: ResolvedConfigFileName; + /** + * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly + */ + done(cancellationToken?: CancellationToken): ExitStatus; + getCompilerOptions(): CompilerOptions; + getCurrentDirectory(): string; + } + interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; + updateOutputFileStatmps(): void; + } + interface BuildInvalidedProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.Build; + getBuilderProgram(): T | undefined; + getProgram(): Program | undefined; + getSourceFile(fileName: string): SourceFile | undefined; + getSourceFiles(): ReadonlyArray; + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getConfigFileParsingDiagnostics(): ReadonlyArray; + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getAllDependencies(sourceFile: SourceFile): ReadonlyArray; + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined; + } + interface UpdateBundleProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateBundle; + emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; + } + type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; } declare namespace ts.server { type ActionSet = "action::set";