From 5443447eb2efa1dd48b2fc52824ceeb0ff5164a5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 14:42:59 -0700 Subject: [PATCH 01/41] Instead of maintaining queue for invalidated projects, use the pendingSet and graph queue --- src/compiler/tsbuild.ts | 70 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 8bc66e95edc45..41031d34fc742 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -411,8 +411,6 @@ namespace ts { const diagnostics = createFileMap>(toPath); const projectPendingBuild = createFileMap(toPath); const projectErrorsReported = createFileMap(toPath); - const invalidatedProjectQueue = [] as ResolvedConfigFileName[]; - let nextProjectToBuild = 0; let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); @@ -456,8 +454,6 @@ namespace ts { diagnostics.clear(); projectPendingBuild.clear(); projectErrorsReported.clear(); - invalidatedProjectQueue.length = 0; - nextProjectToBuild = 0; if (timerToBuildInvalidatedProject) { clearTimeout(timerToBuildInvalidatedProject); timerToBuildInvalidatedProject = undefined; @@ -498,8 +494,8 @@ namespace ts { } function startWatching() { - const graph = getGlobalDependencyGraph(); - for (const resolved of graph.buildQueue) { + const { buildQueue } = getGlobalDependencyGraph(); + for (const resolved of buildQueue) { // Watch this file watchConfigFile(resolved); @@ -855,10 +851,10 @@ namespace ts { } function invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel) { - invalidateResolvedProject(resolveProjectName(configFileName), reloadLevel); + invalidateResolvedProject(resolveProjectName(configFileName), reloadLevel || ConfigFileProgramReloadLevel.None); } - function invalidateResolvedProject(resolved: ResolvedConfigFileName, reloadLevel?: ConfigFileProgramReloadLevel) { + function invalidateResolvedProject(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { if (reloadLevel === ConfigFileProgramReloadLevel.Full) { configFileCache.removeKey(resolved); globalDependencyGraph = undefined; @@ -872,28 +868,24 @@ namespace ts { /** * return true if new addition */ - function addProjToQueue(proj: ResolvedConfigFileName, reloadLevel?: ConfigFileProgramReloadLevel) { + function addProjToQueue(proj: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { const value = projectPendingBuild.getValue(proj); if (value === undefined) { - projectPendingBuild.setValue(proj, reloadLevel || ConfigFileProgramReloadLevel.None); - invalidatedProjectQueue.push(proj); + projectPendingBuild.setValue(proj, reloadLevel); } - else if (value < (reloadLevel || ConfigFileProgramReloadLevel.None)) { - projectPendingBuild.setValue(proj, reloadLevel || ConfigFileProgramReloadLevel.None); + else if (value < reloadLevel) { + projectPendingBuild.setValue(proj, reloadLevel); } } function getNextInvalidatedProject() { - if (nextProjectToBuild < invalidatedProjectQueue.length) { - const project = invalidatedProjectQueue[nextProjectToBuild]; - nextProjectToBuild++; - const reloadLevel = projectPendingBuild.getValue(project)!; - projectPendingBuild.removeKey(project); - if (!projectPendingBuild.getSize()) { - invalidatedProjectQueue.length = 0; - nextProjectToBuild = 0; + const { buildQueue } = getGlobalDependencyGraph(); + for (const project of buildQueue) { + const reloadLevel = projectPendingBuild.getValue(project); + if (reloadLevel !== undefined) { + projectPendingBuild.removeKey(project); + return { project, reloadLevel }; } - return { project, reloadLevel }; } } @@ -990,15 +982,20 @@ namespace ts { updateBundle(resolved); // Fake that files have been built by manipulating prepend and existing output if (buildResult & BuildResultFlags.AnyErrors) return; - const { referencingProjectsMap, buildQueue } = getGlobalDependencyGraph(); - const referencingProjects = referencingProjectsMap.getValue(resolved); - if (!referencingProjects) return; + // Only composite projects can be referenced by other projects + if (!proj.options.composite) return; + const { buildQueue } = getGlobalDependencyGraph(); // Always use build order to queue projects for (let index = buildQueue.indexOf(resolved) + 1; index < buildQueue.length; index++) { const project = buildQueue[index]; - const prepend = referencingProjects.getValue(project); - if (prepend !== undefined) { + if (projectPendingBuild.hasKey(project)) continue; + + const config = parseConfigFile(project); + if (!config || !config.projectReferences) continue; + for (const ref of config.projectReferences) { + const resolvedRefPath = resolveProjectName(ref.path); + if (resolvedRefPath !== resolved) continue; // If the project is referenced with prepend, always build downstream projects, // If declaration output is changed, build the project // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps @@ -1013,7 +1010,7 @@ namespace ts { } } else if (status && status.type === UpToDateStatusType.UpToDate) { - if (prepend) { + if (ref.prepend) { projectStatus.setValue(project, { type: UpToDateStatusType.OutOfDateWithPrepend, outOfDateOutputFileName: status.oldestOutputFileName, @@ -1024,7 +1021,8 @@ namespace ts { status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; } } - addProjToQueue(project); + addProjToQueue(project, ConfigFileProgramReloadLevel.None); + break; } } } @@ -1341,9 +1339,9 @@ namespace ts { function getFilesToClean(): string[] { // Get the same graph for cleaning we'd use for building - const graph = getGlobalDependencyGraph(); + const { buildQueue } = getGlobalDependencyGraph(); const filesToDelete: string[] = []; - for (const proj of graph.buildQueue) { + for (const proj of buildQueue) { const parsed = parseConfigFile(proj); if (parsed === undefined) { // File has gone missing; fine to ignore here @@ -1403,10 +1401,10 @@ namespace ts { loadWithLocalCache(Debug.assertEachDefined(moduleNames), containingFile, redirectedReference, loader); } - const graph = getGlobalDependencyGraph(); - reportBuildQueue(graph); + const { buildQueue } = getGlobalDependencyGraph(); + reportBuildQueue(buildQueue); let anyFailed = false; - for (const next of graph.buildQueue) { + for (const next of buildQueue) { const proj = parseConfigFile(next); if (proj === undefined) { reportParseConfigFileDiagnostic(next); @@ -1494,9 +1492,9 @@ namespace ts { /** * Report the build ordering inferred from the current project graph if we're in verbose mode */ - function reportBuildQueue(graph: DependencyGraph) { + function reportBuildQueue(buildQueue: readonly ResolvedConfigFileName[]) { if (options.verbose) { - reportStatus(Diagnostics.Projects_in_this_build_Colon_0, graph.buildQueue.map(s => "\r\n * " + relName(s)).join("")); + reportStatus(Diagnostics.Projects_in_this_build_Colon_0, buildQueue.map(s => "\r\n * " + relName(s)).join("")); } } From 1de70de09917f18248eafa920405ed4d196e712b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 15:31:09 -0700 Subject: [PATCH 02/41] No need to calculate and store project references graph --- src/compiler/tsbuild.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 41031d34fc742..81d382a4c6d1d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -13,8 +13,6 @@ namespace ts { interface DependencyGraph { buildQueue: ResolvedConfigFileName[]; - /** value in config File map is true if project is referenced using prepend */ - referencingProjectsMap: ConfigFileMap>; } export interface BuildOptions extends OptionsBase { @@ -1032,14 +1030,12 @@ namespace ts { const permanentMarks = createFileMap(toPath); const circularityReportStack: string[] = []; const buildOrder: ResolvedConfigFileName[] = []; - const referencingProjectsMap = createFileMap>(toPath); for (const root of roots) { visit(root); } return { buildQueue: buildOrder, - referencingProjectsMap }; function visit(projPath: ResolvedConfigFileName, inCircularContext?: boolean) { @@ -1061,9 +1057,6 @@ namespace ts { for (const ref of parsed.projectReferences) { const resolvedRefPath = resolveProjectName(ref.path); visit(resolvedRefPath, inCircularContext || ref.circular); - // Get projects referencing resolvedRefPath and add projPath to it - const referencingProjects = getOrCreateValueFromConfigFileMap(referencingProjectsMap, resolvedRefPath, () => createFileMap(toPath)); - referencingProjects.setValue(projPath, !!ref.prepend); } } From b9eeaab050f9b491b2446cf08cc3a91638592554 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 15:32:22 -0700 Subject: [PATCH 03/41] Remove unused variable --- src/compiler/tsbuild.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 81d382a4c6d1d..12a1436ec6335 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -390,7 +390,6 @@ namespace ts { const unchangedOutputs = createFileMap(toPath as ToPath); /** Map from config file name to up-to-date status */ const projectStatus = createFileMap(toPath); - const missingRoots = createMap(); let globalDependencyGraph: DependencyGraph | undefined; const writeFileName = host.trace ? (s: string) => host.trace!(s) : undefined; let readFileWithCache = (f: string) => host.readFile(f); @@ -445,7 +444,6 @@ namespace ts { configFileCache.clear(); unchangedOutputs.clear(); projectStatus.clear(); - missingRoots.clear(); globalDependencyGraph = undefined; buildInfoChecked.clear(); From 3e77b96824b5a11b6f4b216f3016d5f2213129f3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 15:45:45 -0700 Subject: [PATCH 04/41] Fix the graph ordering test case to check actual order and not just members as set --- src/testRunner/unittests/tsbuild/graphOrdering.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/testRunner/unittests/tsbuild/graphOrdering.ts b/src/testRunner/unittests/tsbuild/graphOrdering.ts index bc1af3f71da94..bb29892bc6176 100644 --- a/src/testRunner/unittests/tsbuild/graphOrdering.ts +++ b/src/testRunner/unittests/tsbuild/graphOrdering.ts @@ -22,19 +22,19 @@ namespace ts { }); it("orders the graph correctly - specify two roots", () => { - checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]); + checkGraphOrdering(["A", "G"], ["D", "E", "C", "B", "A", "G"]); }); it("orders the graph correctly - multiple parts of the same graph in various orders", () => { - checkGraphOrdering(["A"], ["A", "B", "C", "D", "E"]); - checkGraphOrdering(["A", "C", "D"], ["A", "B", "C", "D", "E"]); - checkGraphOrdering(["D", "C", "A"], ["A", "B", "C", "D", "E"]); + checkGraphOrdering(["A"], ["D", "E", "C", "B", "A"]); + checkGraphOrdering(["A", "C", "D"], ["D", "E", "C", "B", "A"]); + checkGraphOrdering(["D", "C", "A"], ["D", "E", "C", "B", "A"]); }); it("orders the graph correctly - other orderings", () => { - checkGraphOrdering(["F"], ["F", "E"]); + checkGraphOrdering(["F"], ["E", "F"]); checkGraphOrdering(["E"], ["E"]); - checkGraphOrdering(["F", "C", "A"], ["A", "B", "C", "D", "E", "F"]); + checkGraphOrdering(["F", "C", "A"], ["E", "F", "D", "C", "B", "A"]); }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { @@ -43,7 +43,7 @@ namespace ts { const projFileNames = rootNames.map(getProjectFileName); const graph = builder.getBuildGraph(projFileNames); - assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); + assert.deepEqual(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); for (const dep of deps) { const child = getProjectFileName(dep[0]); From 845f67a39413e2baea7b065086c9b59e47575ed5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 15:39:25 -0700 Subject: [PATCH 05/41] Make api to return build order --- src/compiler/tsbuild.ts | 55 +++++++------------ .../unittests/tsbuild/graphOrdering.ts | 12 ++-- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 12a1436ec6335..f995b8d1b4d1f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -11,10 +11,6 @@ namespace ts { message(diag: DiagnosticMessage, ...args: string[]): void; } - interface DependencyGraph { - buildQueue: ResolvedConfigFileName[]; - } - export interface BuildOptions extends OptionsBase { dry?: boolean; force?: boolean; @@ -313,7 +309,7 @@ namespace ts { // TODO:: All the below ones should technically only be in watch mode. but thats for later time /*@internal*/ resolveProjectName(name: string): ResolvedConfigFileName; /*@internal*/ getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus; - /*@internal*/ getBuildGraph(configFileNames: ReadonlyArray): DependencyGraph; + /*@internal*/ getBuildOrder(): ReadonlyArray; /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildInvalidatedProject(): void; @@ -390,7 +386,7 @@ namespace ts { const unchangedOutputs = createFileMap(toPath as ToPath); /** Map from config file name to up-to-date status */ const projectStatus = createFileMap(toPath); - let globalDependencyGraph: DependencyGraph | undefined; + let buildOrder: readonly ResolvedConfigFileName[] | undefined; const writeFileName = host.trace ? (s: string) => host.trace!(s) : undefined; let readFileWithCache = (f: string) => host.readFile(f); let projectCompilerOptions = baseCompilerOptions; @@ -422,7 +418,7 @@ namespace ts { getUpToDateStatusOfFile, cleanAllProjects, resetBuildContext, - getBuildGraph, + getBuildOrder, invalidateProject, buildInvalidatedProject, @@ -444,7 +440,7 @@ namespace ts { configFileCache.clear(); unchangedOutputs.clear(); projectStatus.clear(); - globalDependencyGraph = undefined; + buildOrder = undefined; buildInfoChecked.clear(); diagnostics.clear(); @@ -490,8 +486,7 @@ namespace ts { } function startWatching() { - const { buildQueue } = getGlobalDependencyGraph(); - for (const resolved of buildQueue) { + for (const resolved of getBuildOrder()) { // Watch this file watchConfigFile(resolved); @@ -615,12 +610,8 @@ namespace ts { return getUpToDateStatus(parseConfigFile(configFileName)); } - function getBuildGraph(configFileNames: ReadonlyArray) { - return createDependencyGraph(resolveProjectNames(configFileNames)); - } - - function getGlobalDependencyGraph() { - return globalDependencyGraph || (globalDependencyGraph = getBuildGraph(rootNames)); + function getBuildOrder() { + return buildOrder || (buildOrder = createBuildOrder(resolveProjectNames(rootNames))); } function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { @@ -853,7 +844,7 @@ namespace ts { function invalidateResolvedProject(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { if (reloadLevel === ConfigFileProgramReloadLevel.Full) { configFileCache.removeKey(resolved); - globalDependencyGraph = undefined; + buildOrder = undefined; } projectStatus.removeKey(resolved); diagnostics.removeKey(resolved); @@ -875,8 +866,7 @@ namespace ts { } function getNextInvalidatedProject() { - const { buildQueue } = getGlobalDependencyGraph(); - for (const project of buildQueue) { + for (const project of getBuildOrder()) { const reloadLevel = projectPendingBuild.getValue(project); if (reloadLevel !== undefined) { projectPendingBuild.removeKey(project); @@ -923,7 +913,7 @@ namespace ts { function reportErrorSummary() { if (options.watch || (host as SolutionBuilderHost).reportErrorSummary) { // Report errors from the other projects - getGlobalDependencyGraph().buildQueue.forEach(project => { + getBuildOrder().forEach(project => { if (!projectErrorsReported.hasKey(project)) { reportErrors(diagnostics.getValue(project) || emptyArray); } @@ -980,11 +970,11 @@ namespace ts { // Only composite projects can be referenced by other projects if (!proj.options.composite) return; - const { buildQueue } = getGlobalDependencyGraph(); + const buildOrder = getBuildOrder(); // Always use build order to queue projects - for (let index = buildQueue.indexOf(resolved) + 1; index < buildQueue.length; index++) { - const project = buildQueue[index]; + for (let index = buildOrder.indexOf(resolved) + 1; index < buildOrder.length; index++) { + const project = buildOrder[index]; if (projectPendingBuild.hasKey(project)) continue; const config = parseConfigFile(project); @@ -1023,18 +1013,16 @@ namespace ts { } } - function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph { + function createBuildOrder(roots: ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { const temporaryMarks = createFileMap(toPath); const permanentMarks = createFileMap(toPath); const circularityReportStack: string[] = []; - const buildOrder: ResolvedConfigFileName[] = []; + let buildOrder: ResolvedConfigFileName[] | undefined; for (const root of roots) { visit(root); } - return { - buildQueue: buildOrder, - }; + return buildOrder || emptyArray; function visit(projPath: ResolvedConfigFileName, inCircularContext?: boolean) { // Already visited @@ -1060,7 +1048,7 @@ namespace ts { circularityReportStack.pop(); permanentMarks.setValue(projPath, true); - buildOrder.push(projPath); + (buildOrder || (buildOrder = [])).push(projPath); } } @@ -1330,9 +1318,8 @@ namespace ts { function getFilesToClean(): string[] { // Get the same graph for cleaning we'd use for building - const { buildQueue } = getGlobalDependencyGraph(); const filesToDelete: string[] = []; - for (const proj of buildQueue) { + for (const proj of getBuildOrder()) { const parsed = parseConfigFile(proj); if (parsed === undefined) { // File has gone missing; fine to ignore here @@ -1392,10 +1379,10 @@ namespace ts { loadWithLocalCache(Debug.assertEachDefined(moduleNames), containingFile, redirectedReference, loader); } - const { buildQueue } = getGlobalDependencyGraph(); - reportBuildQueue(buildQueue); + const buildOrder = getBuildOrder(); + reportBuildQueue(buildOrder); let anyFailed = false; - for (const next of buildQueue) { + for (const next of buildOrder) { const proj = parseConfigFile(next); if (proj === undefined) { reportParseConfigFileDiagnostic(next); diff --git a/src/testRunner/unittests/tsbuild/graphOrdering.ts b/src/testRunner/unittests/tsbuild/graphOrdering.ts index bb29892bc6176..a8f4c3a57a992 100644 --- a/src/testRunner/unittests/tsbuild/graphOrdering.ts +++ b/src/testRunner/unittests/tsbuild/graphOrdering.ts @@ -38,18 +38,16 @@ namespace ts { }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { - const builder = createSolutionBuilder(host!, rootNames, { dry: true, force: false, verbose: false }); + const builder = createSolutionBuilder(host!, rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false }); + const buildQueue = builder.getBuildOrder(); - const projFileNames = rootNames.map(getProjectFileName); - const graph = builder.getBuildGraph(projFileNames); - - assert.deepEqual(graph.buildQueue, expectedBuildSet.map(getProjectFileName)); + assert.deepEqual(buildQueue, expectedBuildSet.map(getProjectFileName)); for (const dep of deps) { const child = getProjectFileName(dep[0]); - if (graph.buildQueue.indexOf(child) < 0) continue; + if (buildQueue.indexOf(child) < 0) continue; const parent = getProjectFileName(dep[1]); - assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); + assert.isAbove(buildQueue.indexOf(child), buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); } } From e9d4e0b104307f8055f5e7023de821f1daaca9c1 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 16:04:43 -0700 Subject: [PATCH 06/41] Unchanged output time is no more required --- src/compiler/tsbuild.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index f995b8d1b4d1f..ede9354165d56 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -382,8 +382,6 @@ namespace ts { let baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; const configFileCache = createFileMap(toPath); - /** Map from output file name to its pre-build timestamp */ - const unchangedOutputs = createFileMap(toPath as ToPath); /** Map from config file name to up-to-date status */ const projectStatus = createFileMap(toPath); let buildOrder: readonly ResolvedConfigFileName[] | undefined; @@ -438,7 +436,6 @@ namespace ts { options = opts; baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); configFileCache.clear(); - unchangedOutputs.clear(); projectStatus.clear(); buildOrder = undefined; buildInfoChecked.clear(); @@ -697,14 +694,8 @@ namespace ts { // had its file touched but not had its contents changed - this allows us // to skip a downstream typecheck if (isDeclarationFile(output)) { - const unchangedTime = unchangedOutputs.getValue(output); - if (unchangedTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); - } - else { - const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); - } + const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); } } @@ -1161,7 +1152,6 @@ namespace ts { writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); if (priorChangeTime !== undefined) { newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - unchangedOutputs.setValue(name, priorChangeTime); } }); From 92365027a1804c767b63b220b84c1dd350678c1e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 16:22:13 -0700 Subject: [PATCH 07/41] Remove `getUpToDateStatusOfFile` from solution builder since that test anyways is tested with the --watch mode --- src/compiler/tsbuild.ts | 20 +++----- src/testRunner/unittests/tsbuild/sample.ts | 55 ---------------------- 2 files changed, 7 insertions(+), 68 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index ede9354165d56..18eaeed188d5f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -306,10 +306,13 @@ namespace ts { buildAllProjects(): ExitStatus; cleanAllProjects(): ExitStatus; + // Currently used for testing but can be made public if needed: + /*@internal*/ getBuildOrder(): ReadonlyArray; + + // Testing only + // TODO:: All the below ones should technically only be in watch mode. but thats for later time /*@internal*/ resolveProjectName(name: string): ResolvedConfigFileName; - /*@internal*/ getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus; - /*@internal*/ getBuildOrder(): ReadonlyArray; /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildInvalidatedProject(): void; @@ -413,7 +416,6 @@ namespace ts { return { buildAllProjects, - getUpToDateStatusOfFile, cleanAllProjects, resetBuildContext, getBuildOrder, @@ -603,12 +605,8 @@ namespace ts { scheduleBuildInvalidatedProject(); } - function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { - return getUpToDateStatus(parseConfigFile(configFileName)); - } - function getBuildOrder() { - return buildOrder || (buildOrder = createBuildOrder(resolveProjectNames(rootNames))); + return buildOrder || (buildOrder = createBuildOrder(rootNames.map(resolveProjectName))); } function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { @@ -1004,7 +1002,7 @@ namespace ts { } } - function createBuildOrder(roots: ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { + function createBuildOrder(roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { const temporaryMarks = createFileMap(toPath); const permanentMarks = createFileMap(toPath); const circularityReportStack: string[] = []; @@ -1344,10 +1342,6 @@ namespace ts { return resolveConfigFileProjectName(resolvePath(host.getCurrentDirectory(), name)); } - function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] { - return configFileNames.map(resolveProjectName); - } - function buildAllProjects(): ExitStatus { if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } // TODO:: In watch mode as well to use caches for incremental build once we can invalidate caches correctly and have right api diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 6e31ada9ee575..2621bd303ead7 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -337,61 +337,6 @@ namespace ts { }); }); - describe("project invalidation", () => { - it("invalidates projects correctly", () => { - const fs = projFs.shadow(); - const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - - builder.buildAllProjects(); - host.assertDiagnosticMessages(/*empty*/); - - // Update a timestamp in the middle project - tick(); - appendText(fs, "/src/logic/index.ts", "function foo() {}"); - const originalWriteFile = fs.writeFileSync; - const writtenFiles = createMap(); - fs.writeFileSync = (path, data, encoding) => { - writtenFiles.set(path, true); - originalWriteFile.call(fs, path, data, encoding); - }; - // Because we haven't reset the build context, the builder should assume there's nothing to do right now - const status = builder.getUpToDateStatusOfFile(builder.resolveProjectName("/src/logic")); - assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - verifyInvalidation(/*expectedToWriteTests*/ false); - - // Rebuild this project - fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} -export class cNew {}`); - verifyInvalidation(/*expectedToWriteTests*/ true); - - function verifyInvalidation(expectedToWriteTests: boolean) { - // Rebuild this project - tick(); - builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProject(); - // The file should be updated - assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); - assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); - assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); - assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - writtenFiles.clear(); - - // Build downstream projects should update 'tests', but not 'core' - tick(); - builder.buildInvalidatedProject(); - if (expectedToWriteTests) { - assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); - } - else { - assert.equal(writtenFiles.size, 0, "Should not write any new files"); - } - assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); - assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); - } - }); - }); - describe("lists files", () => { it("listFiles", () => { const fs = projFs.shadow(); From b42e76b1e5e1712856190b5d2f5ae6022b344d5f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 16:35:08 -0700 Subject: [PATCH 08/41] Remove usage of fileMap for output unchanged timestamp modification and simplify to use configFileMap --- src/compiler/tsbuild.ts | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 18eaeed188d5f..f119481ac0871 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -194,27 +194,22 @@ namespace ts { } } - interface FileMap { - setValue(fileName: U, value: T): void; - getValue(fileName: U): T | undefined; - hasKey(fileName: U): boolean; - removeKey(fileName: U): void; - forEach(action: (value: T, key: V) => void): void; + interface ConfigFileMap { + setValue(fileName: ResolvedConfigFileName, value: T): void; + getValue(fileName: ResolvedConfigFileName): T | undefined; + hasKey(fileName: ResolvedConfigFileName): boolean; + removeKey(fileName: ResolvedConfigFileName): void; + forEach(action: (value: T, key: ResolvedConfigFilePath) => void): void; getSize(): number; clear(): void; } type ResolvedConfigFilePath = ResolvedConfigFileName & Path; - type ConfigFileMap = FileMap; - type ToResolvedConfigFilePath = (fileName: ResolvedConfigFileName) => ResolvedConfigFilePath; - type ToPath = (fileName: string) => Path; /** * A FileMap maintains a normalized-key to value relationship */ - function createFileMap(toPath: ToResolvedConfigFilePath): ConfigFileMap; - function createFileMap(toPath: ToPath): FileMap; - function createFileMap(toPath: (fileName: U) => V): FileMap { + function createFileMap(toPath: (fileName: ResolvedConfigFileName) => ResolvedConfigFilePath): ConfigFileMap { // tslint:disable-next-line:no-null-keyword const lookup = createMap(); @@ -228,23 +223,23 @@ namespace ts { clear }; - function forEach(action: (value: T, key: V) => void) { + function forEach(action: (value: T, key: ResolvedConfigFilePath) => void) { lookup.forEach(action); } - function hasKey(fileName: U) { + function hasKey(fileName: ResolvedConfigFileName) { return lookup.has(toPath(fileName)); } - function removeKey(fileName: U) { + function removeKey(fileName: ResolvedConfigFileName) { lookup.delete(toPath(fileName)); } - function setValue(fileName: U, value: T) { + function setValue(fileName: ResolvedConfigFileName, value: T) { lookup.set(toPath(fileName), value); } - function getValue(fileName: U): T | undefined { + function getValue(fileName: ResolvedConfigFileName): T | undefined { return lookup.get(toPath(fileName)); } @@ -1132,7 +1127,7 @@ namespace ts { // Actual Emit const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createFileMap(toPath as ToPath); + const emittedOutputs = createMap(); outputFiles.forEach(({ name, text, writeByteOrderMark }) => { let priorChangeTime: Date | undefined; if (!anyDtsChanged && isDeclarationFile(name)) { @@ -1146,7 +1141,7 @@ namespace ts { } } - emittedOutputs.setValue(name, name); + emittedOutputs.set(toPath(name), name); writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); if (priorChangeTime !== undefined) { newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); @@ -1235,9 +1230,9 @@ namespace ts { // Actual Emit Debug.assert(!!outputFiles.length); const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createFileMap(toPath as ToPath); + const emittedOutputs = createMap(); outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - emittedOutputs.setValue(name, name); + emittedOutputs.set(toPath(name), name); writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); }); const emitDiagnostics = emitterDiagnostics.getDiagnostics(); @@ -1280,15 +1275,15 @@ namespace ts { projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, status); } - function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { + function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: Map) { const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames()); - if (!skipOutputs || outputs.length !== skipOutputs.getSize()) { + if (!skipOutputs || outputs.length !== skipOutputs.size) { if (options.verbose) { reportStatus(verboseMessage, proj.options.configFilePath!); } const now = host.now ? host.now() : new Date(); for (const file of outputs) { - if (skipOutputs && skipOutputs.hasKey(file)) { + if (skipOutputs && skipOutputs.has(toPath(file))) { continue; } From d4474a5dfca8ee80cd343696e6c3e42cef94fae5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 16:42:56 -0700 Subject: [PATCH 09/41] More refactoring to move file map creation inside solution builder --- src/compiler/tsbuild.ts | 126 +++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index f119481ac0871..76a93afc4787a 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -206,51 +206,6 @@ namespace ts { type ResolvedConfigFilePath = ResolvedConfigFileName & Path; - /** - * A FileMap maintains a normalized-key to value relationship - */ - function createFileMap(toPath: (fileName: ResolvedConfigFileName) => ResolvedConfigFilePath): ConfigFileMap { - // tslint:disable-next-line:no-null-keyword - const lookup = createMap(); - - return { - setValue, - getValue, - removeKey, - forEach, - hasKey, - getSize, - clear - }; - - function forEach(action: (value: T, key: ResolvedConfigFilePath) => void) { - lookup.forEach(action); - } - - function hasKey(fileName: ResolvedConfigFileName) { - return lookup.has(toPath(fileName)); - } - - function removeKey(fileName: ResolvedConfigFileName) { - lookup.delete(toPath(fileName)); - } - - function setValue(fileName: ResolvedConfigFileName, value: T) { - lookup.set(toPath(fileName), value); - } - - function getValue(fileName: ResolvedConfigFileName): T | undefined { - return lookup.get(toPath(fileName)); - } - - function getSize() { - return lookup.size; - } - - function clear() { - lookup.clear(); - } - } function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFileName, createT: () => T): T { const existingValue = configFileMap.getValue(resolved); @@ -378,10 +333,11 @@ namespace ts { // State of the solution let options = defaultOptions; let baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); + const resolvedConfigFilePaths = createMap(); type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; - const configFileCache = createFileMap(toPath); + const configFileCache = createFileMap(); /** Map from config file name to up-to-date status */ - const projectStatus = createFileMap(toPath); + const projectStatus = createFileMap(); let buildOrder: readonly ResolvedConfigFileName[] | undefined; const writeFileName = host.trace ? (s: string) => host.trace!(s) : undefined; let readFileWithCache = (f: string) => host.readFile(f); @@ -393,21 +349,21 @@ namespace ts { compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives); let moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined; - const buildInfoChecked = createFileMap(toPath); + const buildInfoChecked = createFileMap(); // Watch state - const builderPrograms = createFileMap(toPath); - const diagnostics = createFileMap>(toPath); - const projectPendingBuild = createFileMap(toPath); - const projectErrorsReported = createFileMap(toPath); + const builderPrograms = createFileMap(); + const diagnostics = createFileMap>(); + const projectPendingBuild = createFileMap(); + const projectErrorsReported = createFileMap(); let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); // Watches for the solution - const allWatchedWildcardDirectories = createFileMap>(toPath); - const allWatchedInputFiles = createFileMap>(toPath); - const allWatchedConfigFiles = createFileMap(toPath); + const allWatchedWildcardDirectories = createFileMap>(); + const allWatchedInputFiles = createFileMap>(); + const allWatchedConfigFiles = createFileMap(); return { buildAllProjects, @@ -423,15 +379,67 @@ namespace ts { startWatching }; - function toPath(fileName: ResolvedConfigFileName): ResolvedConfigFilePath; - function toPath(fileName: string): Path; function toPath(fileName: string) { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } + function toResolvedConfigFilePath(fileName: ResolvedConfigFileName): ResolvedConfigFilePath { + const path = resolvedConfigFilePaths.get(fileName); + if (path !== undefined) return path; + + const resolvedPath = toPath(fileName) as ResolvedConfigFilePath; + resolvedConfigFilePaths.set(fileName, resolvedPath); + return resolvedPath; + } + + + // TODO remove this and use normal map so we arent transforming paths constantly + function createFileMap(): ConfigFileMap { + const lookup = createMap(); + return { + setValue, + getValue, + removeKey, + forEach, + hasKey, + getSize, + clear + }; + + function forEach(action: (value: T, key: ResolvedConfigFilePath) => void) { + lookup.forEach(action); + } + + function hasKey(fileName: ResolvedConfigFileName) { + return lookup.has(toResolvedConfigFilePath(fileName)); + } + + function removeKey(fileName: ResolvedConfigFileName) { + lookup.delete(toResolvedConfigFilePath(fileName)); + } + + function setValue(fileName: ResolvedConfigFileName, value: T) { + lookup.set(toResolvedConfigFilePath(fileName), value); + } + + function getValue(fileName: ResolvedConfigFileName): T | undefined { + return lookup.get(toResolvedConfigFilePath(fileName)); + } + + function getSize() { + return lookup.size; + } + + function clear() { + lookup.clear(); + } + } + + function resetBuildContext(opts = defaultOptions) { options = opts; baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); + resolvedConfigFilePaths.clear(); configFileCache.clear(); projectStatus.clear(); buildOrder = undefined; @@ -998,8 +1006,8 @@ namespace ts { } function createBuildOrder(roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { - const temporaryMarks = createFileMap(toPath); - const permanentMarks = createFileMap(toPath); + const temporaryMarks = createFileMap(); + const permanentMarks = createFileMap(); const circularityReportStack: string[] = []; let buildOrder: ResolvedConfigFileName[] | undefined; for (const root of roots) { From c615d48727433d09912568285326956bba8d8748 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 16:49:06 -0700 Subject: [PATCH 10/41] Make use of maps in build order calculation --- src/compiler/tsbuild.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 76a93afc4787a..69da4bed77f3e 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1006,8 +1006,8 @@ namespace ts { } function createBuildOrder(roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { - const temporaryMarks = createFileMap(); - const permanentMarks = createFileMap(); + const temporaryMarks = createMap(); + const permanentMarks = createMap(); const circularityReportStack: string[] = []; let buildOrder: ResolvedConfigFileName[] | undefined; for (const root of roots) { @@ -1016,11 +1016,12 @@ namespace ts { return buildOrder || emptyArray; - function visit(projPath: ResolvedConfigFileName, inCircularContext?: boolean) { + function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) { + const projPath = toResolvedConfigFilePath(configFileName); // Already visited - if (permanentMarks.hasKey(projPath)) return; + if (permanentMarks.has(projPath)) return; // Circular - if (temporaryMarks.hasKey(projPath)) { + if (temporaryMarks.has(projPath)) { if (!inCircularContext) { // TODO:: Do we report this as error? reportStatus(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); @@ -1028,9 +1029,9 @@ namespace ts { return; } - temporaryMarks.setValue(projPath, true); - circularityReportStack.push(projPath); - const parsed = parseConfigFile(projPath); + temporaryMarks.set(projPath, true); + circularityReportStack.push(configFileName); + const parsed = parseConfigFile(configFileName); if (parsed && parsed.projectReferences) { for (const ref of parsed.projectReferences) { const resolvedRefPath = resolveProjectName(ref.path); @@ -1039,8 +1040,8 @@ namespace ts { } circularityReportStack.pop(); - permanentMarks.setValue(projPath, true); - (buildOrder || (buildOrder = [])).push(projPath); + permanentMarks.set(projPath, true); + (buildOrder || (buildOrder = [])).push(configFileName); } } From 50741e647be6cea5406b8fb900167d1ab016c66d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 16:59:07 -0700 Subject: [PATCH 11/41] Use maps for all the watch data structures --- src/compiler/tsbuild.ts | 58 +++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 69da4bed77f3e..cc6899163fca8 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -207,17 +207,17 @@ namespace ts { type ResolvedConfigFilePath = ResolvedConfigFileName & Path; - function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFileName, createT: () => T): T { - const existingValue = configFileMap.getValue(resolved); + function getOrCreateValueFromConfigFileMap(configFileMap: Map, resolved: ResolvedConfigFilePath, createT: () => T): T { + const existingValue = configFileMap.get(resolved); let newValue: T | undefined; if (!existingValue) { newValue = createT(); - configFileMap.setValue(resolved, newValue); + configFileMap.set(resolved, newValue); } return existingValue || newValue!; } - function getOrCreateValueMapFromConfigFileMap(configFileMap: ConfigFileMap>, resolved: ResolvedConfigFileName): Map { + function getOrCreateValueMapFromConfigFileMap(configFileMap: Map>, resolved: ResolvedConfigFilePath): Map { return getOrCreateValueFromConfigFileMap>(configFileMap, resolved, createMap); } @@ -361,9 +361,9 @@ namespace ts { const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); // Watches for the solution - const allWatchedWildcardDirectories = createFileMap>(); - const allWatchedInputFiles = createFileMap>(); - const allWatchedConfigFiles = createFileMap(); + const allWatchedWildcardDirectories = createMap>(); + const allWatchedInputFiles = createMap>(); + const allWatchedConfigFiles = createMap(); return { buildAllProjects, @@ -489,28 +489,29 @@ namespace ts { function startWatching() { for (const resolved of getBuildOrder()) { + const resolvedPath = toResolvedConfigFilePath(resolved); // Watch this file - watchConfigFile(resolved); + watchConfigFile(resolved, resolvedPath); const cfg = parseConfigFile(resolved); if (cfg) { // Update watchers for wildcard directories - watchWildCardDirectories(resolved, cfg); + watchWildCardDirectories(resolved, resolvedPath, cfg); // Watch input files - watchInputFiles(resolved, cfg); + watchInputFiles(resolved, resolvedPath, cfg); } } } - function watchConfigFile(resolved: ResolvedConfigFileName) { - if (options.watch && !allWatchedConfigFiles.hasKey(resolved)) { - allWatchedConfigFiles.setValue(resolved, watchFile( + function watchConfigFile(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { + if (options.watch && !allWatchedConfigFiles.has(resolvedPath)) { + allWatchedConfigFiles.set(resolvedPath, watchFile( hostWithWatch, resolved, () => { - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Full); + invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.Full); }, PollingInterval.High, WatchType.ConfigFile, @@ -519,10 +520,10 @@ namespace ts { } } - function watchWildCardDirectories(resolved: ResolvedConfigFileName, parsed: ParsedCommandLine) { + function watchWildCardDirectories(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { if (!options.watch) return; updateWatchingWildcardDirectories( - getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolved), + getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolvedPath), createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), (dir, flags) => { return watchDirectory( @@ -540,7 +541,7 @@ namespace ts { return; } - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Partial); + invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.Partial); }, flags, WatchType.WildcardDirectory, @@ -550,16 +551,16 @@ namespace ts { ); } - function watchInputFiles(resolved: ResolvedConfigFileName, parsed: ParsedCommandLine) { + function watchInputFiles(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { if (!options.watch) return; mutateMap( - getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolved), + getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolvedPath), arrayToMap(parsed.fileNames, toPath), { createNewValue: (path, input) => watchFilePath( hostWithWatch, input, - () => invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.None), + () => invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.None), PollingInterval.Low, path as Path, WatchType.SourceFile, @@ -602,9 +603,9 @@ namespace ts { return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; } - function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { + function invalidateProjectAndScheduleBuilds(resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { reportFileChangeDetected = true; - invalidateResolvedProject(resolved, reloadLevel); + invalidateResolvedProject(resolvedPath, reloadLevel); scheduleBuildInvalidatedProject(); } @@ -830,10 +831,10 @@ namespace ts { } function invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel) { - invalidateResolvedProject(resolveProjectName(configFileName), reloadLevel || ConfigFileProgramReloadLevel.None); + invalidateResolvedProject(toResolvedConfigFilePath(resolveProjectName(configFileName)), reloadLevel || ConfigFileProgramReloadLevel.None); } - function invalidateResolvedProject(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { + function invalidateResolvedProject(resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { if (reloadLevel === ConfigFileProgramReloadLevel.Full) { configFileCache.removeKey(resolved); buildOrder = undefined; @@ -928,17 +929,18 @@ namespace ts { return; } + const resolvedPath = toResolvedConfigFilePath(resolved); if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(resolved); - watchWildCardDirectories(resolved, proj); - watchInputFiles(resolved, proj); + watchConfigFile(resolved, resolvedPath); + watchWildCardDirectories(resolved, resolvedPath, proj); + watchInputFiles(resolved, resolvedPath, proj); } else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { // Update file names const result = getFileNamesFromConfigSpecs(proj.configFileSpecs!, getDirectoryPath(resolved), proj.options, parseConfigFileHost); updateErrorForNoInputFiles(result, resolved, proj.configFileSpecs!, proj.errors, canJsonReportNoInutFiles(proj.raw)); proj.fileNames = result.fileNames; - watchInputFiles(resolved, proj); + watchInputFiles(resolved, resolvedPath, proj); } const status = getUpToDateStatus(proj); From 579d2bfe1f550f4bd88ca7590f52ee0fe0012682 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 17:16:36 -0700 Subject: [PATCH 12/41] Make FileMap Apis to match map but just ensure that keys are always paths Turn projectPendingBuild to config file map --- src/compiler/tsbuild.ts | 75 ++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index cc6899163fca8..f1e32051310da 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -194,20 +194,24 @@ namespace ts { } } - interface ConfigFileMap { - setValue(fileName: ResolvedConfigFileName, value: T): void; - getValue(fileName: ResolvedConfigFileName): T | undefined; - hasKey(fileName: ResolvedConfigFileName): boolean; - removeKey(fileName: ResolvedConfigFileName): void; - forEach(action: (value: T, key: ResolvedConfigFilePath) => void): void; - getSize(): number; + type ResolvedConfigFilePath = ResolvedConfigFileName & Path; + + interface FileMap extends Map { + get(key: U): T | undefined; + has(key: U): boolean; + forEach(action: (value: T, key: U) => void): void; + readonly size: number; + keys(): Iterator; + values(): Iterator; + entries(): Iterator<[U, T]>; + set(key: U, value: T): this; + delete(key: U): boolean; clear(): void; } - type ResolvedConfigFilePath = ResolvedConfigFileName & Path; + type ConfigFileMap = FileMap; - - function getOrCreateValueFromConfigFileMap(configFileMap: Map, resolved: ResolvedConfigFilePath, createT: () => T): T { + function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFilePath, createT: () => T): T { const existingValue = configFileMap.get(resolved); let newValue: T | undefined; if (!existingValue) { @@ -217,7 +221,7 @@ namespace ts { return existingValue || newValue!; } - function getOrCreateValueMapFromConfigFileMap(configFileMap: Map>, resolved: ResolvedConfigFilePath): Map { + function getOrCreateValueMapFromConfigFileMap(configFileMap: ConfigFileMap>, resolved: ResolvedConfigFilePath): Map { return getOrCreateValueFromConfigFileMap>(configFileMap, resolved, createMap); } @@ -354,16 +358,16 @@ namespace ts { // Watch state const builderPrograms = createFileMap(); const diagnostics = createFileMap>(); - const projectPendingBuild = createFileMap(); + const projectPendingBuild = createMap() as ConfigFileMap; const projectErrorsReported = createFileMap(); let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); // Watches for the solution - const allWatchedWildcardDirectories = createMap>(); - const allWatchedInputFiles = createMap>(); - const allWatchedConfigFiles = createMap(); + const allWatchedWildcardDirectories = createMap() as ConfigFileMap>; + const allWatchedInputFiles = createMap() as ConfigFileMap>; + const allWatchedConfigFiles = createMap() as ConfigFileMap; return { buildAllProjects, @@ -394,7 +398,15 @@ namespace ts { // TODO remove this and use normal map so we arent transforming paths constantly - function createFileMap(): ConfigFileMap { + function createFileMap(): { + setValue(fileName: ResolvedConfigFileName, value: T): void; + getValue(fileName: ResolvedConfigFileName): T | undefined; + hasKey(fileName: ResolvedConfigFileName): boolean; + removeKey(fileName: ResolvedConfigFileName): void; + forEach(action: (value: T, key: ResolvedConfigFilePath) => void): void; + getSize(): number; + clear(): void; + } { const lookup = createMap(); return { setValue, @@ -435,7 +447,6 @@ namespace ts { } } - function resetBuildContext(opts = defaultOptions) { options = opts; baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); @@ -848,28 +859,29 @@ namespace ts { /** * return true if new addition */ - function addProjToQueue(proj: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { - const value = projectPendingBuild.getValue(proj); + function addProjToQueue(proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + const value = projectPendingBuild.get(proj); if (value === undefined) { - projectPendingBuild.setValue(proj, reloadLevel); + projectPendingBuild.set(proj, reloadLevel); } else if (value < reloadLevel) { - projectPendingBuild.setValue(proj, reloadLevel); + projectPendingBuild.set(proj, reloadLevel); } } function getNextInvalidatedProject() { for (const project of getBuildOrder()) { - const reloadLevel = projectPendingBuild.getValue(project); + const projectPath = toResolvedConfigFilePath(project); + const reloadLevel = projectPendingBuild.get(projectPath); if (reloadLevel !== undefined) { - projectPendingBuild.removeKey(project); + projectPendingBuild.delete(projectPath); return { project, reloadLevel }; } } } function hasPendingInvalidatedProjects() { - return !!projectPendingBuild.getSize(); + return !!projectPendingBuild.size; } function scheduleBuildInvalidatedProject() { @@ -969,7 +981,8 @@ namespace ts { // Always use build order to queue projects for (let index = buildOrder.indexOf(resolved) + 1; index < buildOrder.length; index++) { const project = buildOrder[index]; - if (projectPendingBuild.hasKey(project)) continue; + const projectPath = toResolvedConfigFilePath(project); + if (projectPendingBuild.has(projectPath)) continue; const config = parseConfigFile(project); if (!config || !config.projectReferences) continue; @@ -1001,15 +1014,15 @@ namespace ts { status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; } } - addProjToQueue(project, ConfigFileProgramReloadLevel.None); + addProjToQueue(projectPath, ConfigFileProgramReloadLevel.None); break; } } } function createBuildOrder(roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { - const temporaryMarks = createMap(); - const permanentMarks = createMap(); + const temporaryMarks = createMap() as ConfigFileMap; + const permanentMarks = createMap() as ConfigFileMap; const circularityReportStack: string[] = []; let buildOrder: ResolvedConfigFileName[] | undefined; for (const root of roots) { @@ -1138,7 +1151,7 @@ namespace ts { // Actual Emit const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap(); + const emittedOutputs = createMap() as FileMap; outputFiles.forEach(({ name, text, writeByteOrderMark }) => { let priorChangeTime: Date | undefined; if (!anyDtsChanged && isDeclarationFile(name)) { @@ -1241,7 +1254,7 @@ namespace ts { // Actual Emit Debug.assert(!!outputFiles.length); const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap(); + const emittedOutputs = createMap() as FileMap; outputFiles.forEach(({ name, text, writeByteOrderMark }) => { emittedOutputs.set(toPath(name), name); writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); @@ -1286,7 +1299,7 @@ namespace ts { projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, status); } - function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: Map) { + function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames()); if (!skipOutputs || outputs.length !== skipOutputs.size) { if (options.verbose) { From 05257e8696d4adc2e620c1e85e7d799190df0d98 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 17:31:20 -0700 Subject: [PATCH 13/41] configFileCache, projectStatus, buildInfoChecked is now ConfigFileMap --- src/compiler/tsbuild.ts | 149 ++++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 73 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index f1e32051310da..17c6d893764a5 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -339,9 +339,9 @@ namespace ts { let baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); const resolvedConfigFilePaths = createMap(); type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; - const configFileCache = createFileMap(); + const configFileCache = createMap() as ConfigFileMap; /** Map from config file name to up-to-date status */ - const projectStatus = createFileMap(); + const projectStatus = createMap() as ConfigFileMap; let buildOrder: readonly ResolvedConfigFileName[] | undefined; const writeFileName = host.trace ? (s: string) => host.trace!(s) : undefined; let readFileWithCache = (f: string) => host.readFile(f); @@ -353,7 +353,7 @@ namespace ts { compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives); let moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined; - const buildInfoChecked = createFileMap(); + const buildInfoChecked = createMap() as ConfigFileMap; // Watch state const builderPrograms = createFileMap(); @@ -474,17 +474,17 @@ namespace ts { return !!(entry as ParsedCommandLine).options; } - function parseConfigFile(configFilePath: ResolvedConfigFileName): ParsedCommandLine | undefined { - const value = configFileCache.getValue(configFilePath); + function parseConfigFile(configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { + const value = configFileCache.get(configFilePath); if (value) { return isParsedCommandLine(value) ? value : undefined; } let diagnostic: Diagnostic | undefined; parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - const parsed = getParsedCommandLineOfConfigFile(configFilePath, baseCompilerOptions, parseConfigFileHost); + const parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost); parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; - configFileCache.setValue(configFilePath, parsed || diagnostic!); + configFileCache.set(configFilePath, parsed || diagnostic!); return parsed; } @@ -504,7 +504,7 @@ namespace ts { // Watch this file watchConfigFile(resolved, resolvedPath); - const cfg = parseConfigFile(resolved); + const cfg = parseConfigFile(resolved, resolvedPath); if (cfg) { // Update watchers for wildcard directories watchWildCardDirectories(resolved, resolvedPath, cfg); @@ -624,22 +624,22 @@ namespace ts { return buildOrder || (buildOrder = createBuildOrder(rootNames.map(resolveProjectName))); } - function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { + function getUpToDateStatus(project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { if (project === undefined) { return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; } - const prior = projectStatus.getValue(project.options.configFilePath as ResolvedConfigFilePath); + const prior = projectStatus.get(resolvedPath); if (prior !== undefined) { return prior; } - const actual = getUpToDateStatusWorker(project); - projectStatus.setValue(project.options.configFilePath as ResolvedConfigFilePath, actual); + const actual = getUpToDateStatusWorker(project, resolvedPath); + projectStatus.set(resolvedPath, actual); return actual; } - function getUpToDateStatusWorker(project: ParsedCommandLine): UpToDateStatus { + function getUpToDateStatusWorker(project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { let newestInputFileName: string = undefined!; let newestInputFileTime = minimumDate; // Get timestamps of input files @@ -716,11 +716,12 @@ namespace ts { let usesPrepend = false; let upstreamChangedProject: string | undefined; if (project.projectReferences) { - projectStatus.setValue(project.options.configFilePath as ResolvedConfigFileName, { type: UpToDateStatusType.ComputingUpstream }); + projectStatus.set(resolvedPath, { type: UpToDateStatusType.ComputingUpstream }); for (const ref of project.projectReferences) { usesPrepend = usesPrepend || !!(ref.prepend); const resolvedRef = resolveProjectReferencePath(ref); - const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef)); + const resolvedRefPath = toResolvedConfigFilePath(resolvedRef); + const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef, resolvedRefPath), resolvedRefPath); // Its a circular reference ignore the status of this project if (refStatus.type === UpToDateStatusType.ComputingUpstream) { @@ -794,8 +795,8 @@ namespace ts { if (extendedConfigStatus) return extendedConfigStatus; } - if (!buildInfoChecked.hasKey(project.options.configFilePath as ResolvedConfigFileName)) { - buildInfoChecked.setValue(project.options.configFilePath as ResolvedConfigFileName, true); + if (!buildInfoChecked.has(resolvedPath)) { + buildInfoChecked.set(resolvedPath, true); const buildInfoPath = getOutputPathForBuildInfo(project.options); if (buildInfoPath) { const value = readFileWithCache(buildInfoPath); @@ -847,10 +848,10 @@ namespace ts { function invalidateResolvedProject(resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - configFileCache.removeKey(resolved); + configFileCache.delete(resolved); buildOrder = undefined; } - projectStatus.removeKey(resolved); + projectStatus.delete(resolved); diagnostics.removeKey(resolved); addProjToQueue(resolved, reloadLevel); @@ -935,13 +936,13 @@ namespace ts { } function buildSingleInvalidatedProject(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { - const proj = parseConfigFile(resolved); + const resolvedPath = toResolvedConfigFilePath(resolved); + const proj = parseConfigFile(resolved, resolvedPath); if (!proj) { - reportParseConfigFileDiagnostic(resolved); + reportParseConfigFileDiagnostic(resolvedPath); return; } - const resolvedPath = toResolvedConfigFilePath(resolved); if (reloadLevel === ConfigFileProgramReloadLevel.Full) { watchConfigFile(resolved, resolvedPath); watchWildCardDirectories(resolved, resolvedPath, proj); @@ -955,7 +956,7 @@ namespace ts { watchInputFiles(resolved, resolvedPath, proj); } - const status = getUpToDateStatus(proj); + const status = getUpToDateStatus(proj, resolvedPath); verboseReportProjectStatus(resolved, status); if (status.type === UpToDateStatusType.UpstreamBlocked) { @@ -965,13 +966,13 @@ namespace ts { if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { // Fake that files have been built by updating output file stamps - updateOutputTimestamps(proj); + updateOutputTimestamps(proj, resolvedPath); return; } - const buildResult = needsBuild(status, resolved) ? - buildSingleProject(resolved) : // Actual build - updateBundle(resolved); // Fake that files have been built by manipulating prepend and existing output + const buildResult = needsBuild(status, proj) ? + buildSingleProject(resolved, resolvedPath) : // Actual build + updateBundle(resolved, resolvedPath); // Fake that files have been built by manipulating prepend and existing output if (buildResult & BuildResultFlags.AnyErrors) return; // Only composite projects can be referenced by other projects @@ -984,18 +985,18 @@ namespace ts { const projectPath = toResolvedConfigFilePath(project); if (projectPendingBuild.has(projectPath)) continue; - const config = parseConfigFile(project); + const config = parseConfigFile(project, projectPath); if (!config || !config.projectReferences) continue; for (const ref of config.projectReferences) { const resolvedRefPath = resolveProjectName(ref.path); - if (resolvedRefPath !== resolved) continue; + if (toResolvedConfigFilePath(resolvedRefPath) !== resolvedPath) continue; // If the project is referenced with prepend, always build downstream projects, // If declaration output is changed, build the project // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps - const status = projectStatus.getValue(project); + const status = projectStatus.get(projectPath); if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { if (status && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes || status.type === UpToDateStatusType.OutOfDateWithPrepend)) { - projectStatus.setValue(project, { + projectStatus.set(projectPath, { type: UpToDateStatusType.OutOfDateWithUpstream, outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, newerProjectName: resolved @@ -1004,7 +1005,7 @@ namespace ts { } else if (status && status.type === UpToDateStatusType.UpToDate) { if (ref.prepend) { - projectStatus.setValue(project, { + projectStatus.set(projectPath, { type: UpToDateStatusType.OutOfDateWithPrepend, outOfDateOutputFileName: status.oldestOutputFileName, newerProjectName: resolved @@ -1046,7 +1047,7 @@ namespace ts { temporaryMarks.set(projPath, true); circularityReportStack.push(configFileName); - const parsed = parseConfigFile(configFileName); + const parsed = parseConfigFile(configFileName, projPath); if (parsed && parsed.projectReferences) { for (const ref of parsed.projectReferences) { const resolvedRefPath = resolveProjectName(ref.path); @@ -1060,7 +1061,7 @@ namespace ts { } } - function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { + function buildSingleProject(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath): BuildResultFlags { if (options.dry) { reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj); return BuildResultFlags.Success; @@ -1070,16 +1071,16 @@ namespace ts { let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; - const configFile = parseConfigFile(proj); + const configFile = parseConfigFile(proj, resolvedPath); if (!configFile) { // Failed to read the config file resultFlags |= BuildResultFlags.ConfigFileErrors; - reportParseConfigFileDiagnostic(proj); - projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); + reportParseConfigFileDiagnostic(resolvedPath); + projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); return resultFlags; } if (configFile.fileNames.length === 0) { - reportAndStoreErrors(proj, configFile.errors); + reportAndStoreErrors(resolvedPath, configFile.errors); // Nothing to build - must be a solution file, basically return BuildResultFlags.None; } @@ -1191,17 +1192,17 @@ namespace ts { oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) }; diagnostics.removeKey(proj); - projectStatus.setValue(proj, status); + projectStatus.set(resolvedPath, status); afterProgramCreate(proj, program); projectCompilerOptions = baseCompilerOptions; return resultFlags; function buildErrors(diagnostics: ReadonlyArray, errorFlags: BuildResultFlags, errorType: string) { resultFlags |= errorFlags; - reportAndStoreErrors(proj, diagnostics); + reportAndStoreErrors(resolvedPath, diagnostics); // List files if any other build error using program (emit errors already report files) if (writeFileName) listFiles(program, writeFileName); - projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); + projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); afterProgramCreate(proj, program); projectCompilerOptions = baseCompilerOptions; return resultFlags; @@ -1231,7 +1232,7 @@ namespace ts { return readBuilderProgram(parsed.options, readFileWithCache) as any as T; } - function updateBundle(proj: ResolvedConfigFileName): BuildResultFlags { + function updateBundle(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath): BuildResultFlags { if (options.dry) { reportStatus(Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); return BuildResultFlags.Success; @@ -1240,15 +1241,18 @@ namespace ts { if (options.verbose) reportStatus(Diagnostics.Updating_output_of_project_0, proj); // Update js, and source map - const config = Debug.assertDefined(parseConfigFile(proj)); + const config = Debug.assertDefined(parseConfigFile(proj, resolvedPath)); projectCompilerOptions = config.options; const outputFiles = emitUsingBuildInfo( config, compilerHost, - ref => parseConfigFile(resolveProjectName(ref.path))); + ref => { + const refName = resolveProjectName(ref.path); + return parseConfigFile(refName, toResolvedConfigFilePath(refName)); + }); if (isString(outputFiles)) { reportStatus(Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(outputFiles)); - return buildSingleProject(proj); + return buildSingleProject(proj, resolvedPath); } // Actual Emit @@ -1261,8 +1265,8 @@ namespace ts { }); const emitDiagnostics = emitterDiagnostics.getDiagnostics(); if (emitDiagnostics.length) { - reportAndStoreErrors(proj, emitDiagnostics); - projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Emit errors" }); + reportAndStoreErrors(resolvedPath, emitDiagnostics); + projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Emit errors" }); projectCompilerOptions = baseCompilerOptions; return BuildResultFlags.DeclarationOutputUnchanged | BuildResultFlags.EmitErrors; } @@ -1281,12 +1285,12 @@ namespace ts { }; diagnostics.removeKey(proj); - projectStatus.setValue(proj, status); + projectStatus.set(resolvedPath, status); projectCompilerOptions = baseCompilerOptions; return BuildResultFlags.DeclarationOutputUnchanged; } - function updateOutputTimestamps(proj: ParsedCommandLine) { + function updateOutputTimestamps(proj: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath) { if (options.dry) { return reportStatus(Diagnostics.A_non_dry_build_would_update_timestamps_for_output_of_project_0, proj.options.configFilePath!); } @@ -1296,7 +1300,7 @@ namespace ts { newestDeclarationFileContentChangedTime: priorNewestUpdateTime, oldestOutputFileName: getFirstProjectOutput(proj, !host.useCaseSensitiveFileNames()) }; - projectStatus.setValue(proj.options.configFilePath as ResolvedConfigFilePath, status); + projectStatus.set(resolvedPath, status); } function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { @@ -1327,10 +1331,11 @@ namespace ts { // Get the same graph for cleaning we'd use for building const filesToDelete: string[] = []; for (const proj of getBuildOrder()) { - const parsed = parseConfigFile(proj); + const resolvedPath = toResolvedConfigFilePath(proj); + const parsed = parseConfigFile(proj, resolvedPath); if (parsed === undefined) { // File has gone missing; fine to ignore here - reportParseConfigFileDiagnostic(proj); + reportParseConfigFileDiagnostic(resolvedPath); continue; } const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); @@ -1386,51 +1391,51 @@ namespace ts { reportBuildQueue(buildOrder); let anyFailed = false; for (const next of buildOrder) { - const proj = parseConfigFile(next); + const resolvedPath = toResolvedConfigFilePath(next); + const proj = parseConfigFile(next, resolvedPath); if (proj === undefined) { - reportParseConfigFileDiagnostic(next); + reportParseConfigFileDiagnostic(resolvedPath); anyFailed = true; break; } // report errors early when using continue or break statements const errors = proj.errors; - const status = getUpToDateStatus(proj); + const status = getUpToDateStatus(proj, resolvedPath); verboseReportProjectStatus(next, status); - const projName = proj.options.configFilePath!; if (status.type === UpToDateStatusType.UpToDate && !options.force) { - reportAndStoreErrors(next, errors); + reportAndStoreErrors(resolvedPath, errors); // Up to date, skip if (defaultOptions.dry) { // In a dry build, inform the user of this fact - reportStatus(Diagnostics.Project_0_is_up_to_date, projName); + reportStatus(Diagnostics.Project_0_is_up_to_date, next); } continue; } if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !options.force) { - reportAndStoreErrors(next, errors); + reportAndStoreErrors(resolvedPath, errors); // Fake build - updateOutputTimestamps(proj); + updateOutputTimestamps(proj, resolvedPath); continue; } if (status.type === UpToDateStatusType.UpstreamBlocked) { - reportAndStoreErrors(next, errors); - if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); + reportAndStoreErrors(resolvedPath, errors); + if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, next, status.upstreamProjectName); continue; } if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(next, errors); + reportAndStoreErrors(resolvedPath, errors); // Do nothing continue; } - const buildResult = needsBuild(status, next) ? - buildSingleProject(next) : // Actual build - updateBundle(next); // Fake that files have been built by manipulating prepend and existing output + const buildResult = needsBuild(status, proj) ? + buildSingleProject(next, resolvedPath) : // Actual build + updateBundle(next, resolvedPath); // Fake that files have been built by manipulating prepend and existing output anyFailed = anyFailed || !!(buildResult & BuildResultFlags.AnyErrors); } @@ -1447,20 +1452,18 @@ namespace ts { return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; } - function needsBuild(status: UpToDateStatus, configFile: ResolvedConfigFileName) { + function needsBuild(status: UpToDateStatus, config: ParsedCommandLine) { if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; - const config = parseConfigFile(configFile); - return !config || - config.fileNames.length === 0 || + return config.fileNames.length === 0 || !!config.errors.length || !isIncrementalCompilation(config.options); } - function reportParseConfigFileDiagnostic(proj: ResolvedConfigFileName) { - reportAndStoreErrors(proj, [configFileCache.getValue(proj) as Diagnostic]); + function reportParseConfigFileDiagnostic(proj: ResolvedConfigFilePath) { + reportAndStoreErrors(proj, [configFileCache.get(proj) as Diagnostic]); } - function reportAndStoreErrors(proj: ResolvedConfigFileName, errors: ReadonlyArray) { + function reportAndStoreErrors(proj: ResolvedConfigFilePath, errors: ReadonlyArray) { reportErrors(errors); projectErrorsReported.setValue(proj, true); diagnostics.setValue(proj, errors); From fd6773f94416008149b9c92b1db22179f5b9d747 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 17:55:15 -0700 Subject: [PATCH 14/41] Diagnostics as ConfigFileMap --- src/compiler/tsbuild.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 17c6d893764a5..d8d55c346f961 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -357,9 +357,9 @@ namespace ts { // Watch state const builderPrograms = createFileMap(); - const diagnostics = createFileMap>(); + const diagnostics = createMap() as ConfigFileMap>; const projectPendingBuild = createMap() as ConfigFileMap; - const projectErrorsReported = createFileMap(); + const projectErrorsReported = createMap() as ConfigFileMap; let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); @@ -852,7 +852,7 @@ namespace ts { buildOrder = undefined; } projectStatus.delete(resolved); - diagnostics.removeKey(resolved); + diagnostics.delete(resolved); addProjToQueue(resolved, reloadLevel); } @@ -920,8 +920,9 @@ namespace ts { if (options.watch || (host as SolutionBuilderHost).reportErrorSummary) { // Report errors from the other projects getBuildOrder().forEach(project => { - if (!projectErrorsReported.hasKey(project)) { - reportErrors(diagnostics.getValue(project) || emptyArray); + const projectPath = toResolvedConfigFilePath(project); + if (!projectErrorsReported.has(projectPath)) { + reportErrors(diagnostics.get(projectPath) || emptyArray); } }); let totalErrors = 0; @@ -1191,7 +1192,7 @@ namespace ts { newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) }; - diagnostics.removeKey(proj); + diagnostics.delete(resolvedPath); projectStatus.set(resolvedPath, status); afterProgramCreate(proj, program); projectCompilerOptions = baseCompilerOptions; @@ -1284,7 +1285,7 @@ namespace ts { oldestOutputFileName: outputFiles[0].name }; - diagnostics.removeKey(proj); + diagnostics.delete(resolvedPath); projectStatus.set(resolvedPath, status); projectCompilerOptions = baseCompilerOptions; return BuildResultFlags.DeclarationOutputUnchanged; @@ -1465,8 +1466,8 @@ namespace ts { function reportAndStoreErrors(proj: ResolvedConfigFilePath, errors: ReadonlyArray) { reportErrors(errors); - projectErrorsReported.setValue(proj, true); - diagnostics.setValue(proj, errors); + projectErrorsReported.set(proj, true); + diagnostics.set(proj, errors); } function reportErrors(errors: ReadonlyArray) { From 11b21fbba6aa383c04917c0379b0f7c04756a899 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 17:56:50 -0700 Subject: [PATCH 15/41] builderPrograms as ConfigFileMap --- src/compiler/tsbuild.ts | 67 +++++------------------------------------ 1 file changed, 8 insertions(+), 59 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index d8d55c346f961..54094fb62de9c 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -356,7 +356,7 @@ namespace ts { const buildInfoChecked = createMap() as ConfigFileMap; // Watch state - const builderPrograms = createFileMap(); + const builderPrograms = createMap() as ConfigFileMap; const diagnostics = createMap() as ConfigFileMap>; const projectPendingBuild = createMap() as ConfigFileMap; const projectErrorsReported = createMap() as ConfigFileMap; @@ -396,57 +396,6 @@ namespace ts { return resolvedPath; } - - // TODO remove this and use normal map so we arent transforming paths constantly - function createFileMap(): { - setValue(fileName: ResolvedConfigFileName, value: T): void; - getValue(fileName: ResolvedConfigFileName): T | undefined; - hasKey(fileName: ResolvedConfigFileName): boolean; - removeKey(fileName: ResolvedConfigFileName): void; - forEach(action: (value: T, key: ResolvedConfigFilePath) => void): void; - getSize(): number; - clear(): void; - } { - const lookup = createMap(); - return { - setValue, - getValue, - removeKey, - forEach, - hasKey, - getSize, - clear - }; - - function forEach(action: (value: T, key: ResolvedConfigFilePath) => void) { - lookup.forEach(action); - } - - function hasKey(fileName: ResolvedConfigFileName) { - return lookup.has(toResolvedConfigFilePath(fileName)); - } - - function removeKey(fileName: ResolvedConfigFileName) { - lookup.delete(toResolvedConfigFilePath(fileName)); - } - - function setValue(fileName: ResolvedConfigFileName, value: T) { - lookup.set(toResolvedConfigFilePath(fileName), value); - } - - function getValue(fileName: ResolvedConfigFileName): T | undefined { - return lookup.get(toResolvedConfigFilePath(fileName)); - } - - function getSize() { - return lookup.size; - } - - function clear() { - lookup.clear(); - } - } - function resetBuildContext(opts = defaultOptions) { options = opts; baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); @@ -1116,7 +1065,7 @@ namespace ts { configFile.fileNames, configFile.options, compilerHost, - getOldProgram(proj, configFile), + getOldProgram(resolvedPath, configFile), configFile.errors, configFile.projectReferences ); @@ -1194,7 +1143,7 @@ namespace ts { }; diagnostics.delete(resolvedPath); projectStatus.set(resolvedPath, status); - afterProgramCreate(proj, program); + afterProgramCreate(resolvedPath, program); projectCompilerOptions = baseCompilerOptions; return resultFlags; @@ -1204,7 +1153,7 @@ namespace ts { // List files if any other build error using program (emit errors already report files) if (writeFileName) listFiles(program, writeFileName); projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); - afterProgramCreate(proj, program); + afterProgramCreate(resolvedPath, program); projectCompilerOptions = baseCompilerOptions; return resultFlags; } @@ -1216,19 +1165,19 @@ namespace ts { } } - function afterProgramCreate(proj: ResolvedConfigFileName, program: T) { + function afterProgramCreate(proj: ResolvedConfigFilePath, program: T) { if (host.afterProgramEmitAndDiagnostics) { host.afterProgramEmitAndDiagnostics(program); } if (options.watch) { program.releaseProgram(); - builderPrograms.setValue(proj, program); + builderPrograms.set(proj, program); } } - function getOldProgram(proj: ResolvedConfigFileName, parsed: ParsedCommandLine) { + function getOldProgram(proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { if (options.force) return undefined; - const value = builderPrograms.getValue(proj); + const value = builderPrograms.get(proj); if (value) return value; return readBuilderProgram(parsed.options, readFileWithCache) as any as T; } From 65ed413d8ddf80d418bb9df059f2e2201adccbfb Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 18 Apr 2019 18:01:03 -0700 Subject: [PATCH 16/41] Remove resolveProjectName as api --- src/compiler/tsbuild.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 54094fb62de9c..0042abfcb9f34 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -266,8 +266,6 @@ namespace ts { // Testing only // TODO:: All the below ones should technically only be in watch mode. but thats for later time - /*@internal*/ resolveProjectName(name: string): ResolvedConfigFileName; - /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildInvalidatedProject(): void; @@ -378,8 +376,6 @@ namespace ts { invalidateProject, buildInvalidatedProject, - resolveProjectName, - startWatching }; From ddee617e845c33cbb45fddb914c8914b54a131d7 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 19 Apr 2019 10:11:44 -0700 Subject: [PATCH 17/41] Make SolutionBuilder and SolutionBuilderWithWatch separate --- src/compiler/tsbuild.ts | 95 ++++++++++++-------- src/testRunner/unittests/tsbuild/sample.ts | 55 ++++++++++++ src/testRunner/unittests/tsbuildWatchMode.ts | 13 ++- src/tsc/tsc.ts | 32 ++++--- 4 files changed, 133 insertions(+), 62 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 0042abfcb9f34..9ec1b3f99c45c 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -236,7 +236,6 @@ namespace ts { export interface SolutionBuilderHostBase extends ProgramHost { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; - deleteFile(fileName: string): void; reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here reportSolutionBuilderStatus: DiagnosticReporter; @@ -246,10 +245,11 @@ namespace ts { afterProgramEmitAndDiagnostics?(program: T): void; // For testing - now?(): Date; + /*@internal*/ now?(): Date; } export interface SolutionBuilderHost extends SolutionBuilderHostBase { + deleteFile(fileName: string): void; reportErrorSummary?: ReportEmitErrorSummary; } @@ -262,17 +262,16 @@ namespace ts { // Currently used for testing but can be made public if needed: /*@internal*/ getBuildOrder(): ReadonlyArray; + /*@internal*/ resetBuildContext(opts?: BuildOptions): void; // Testing only - - // TODO:: All the below ones should technically only be in watch mode. but thats for later time + /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildInvalidatedProject(): void; - - /*@internal*/ resetBuildContext(opts?: BuildOptions): void; } - export interface SolutionBuilderWithWatch extends SolutionBuilder { + export interface SolutionBuilderWithWatch { + buildAllProjects(): ExitStatus; /*@internal*/ startWatching(): void; } @@ -291,7 +290,6 @@ namespace ts { const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase; host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined; host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop; - host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system); return host; @@ -299,6 +297,7 @@ namespace ts { export function createSolutionBuilderHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary) { const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderHost; + host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; host.reportErrorSummary = reportErrorSummary; return host; } @@ -318,16 +317,23 @@ namespace ts { return result; } + 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): SolutionBuilderWithWatch { + return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); + } + /** * 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 - * TODO: use SolutionBuilderWithWatchHost => watchedSolution - * use SolutionBuilderHost => Solution */ - export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - export function createSolutionBuilder(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch; - export function createSolutionBuilder(host: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch { - const hostWithWatch = host as SolutionBuilderWithWatchHost; + function createSolutionBuilderWorker(watch: false, host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch; + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder | SolutionBuilderWithWatch { + const host = hostOrHostWithWatch as SolutionBuilderHost; + const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); const parseConfigFileHost = parseConfigHostFromCompilerHostLike(host); @@ -360,24 +366,27 @@ namespace ts { const projectErrorsReported = createMap() as ConfigFileMap; let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; - const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(host, options); + const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); // Watches for the solution const allWatchedWildcardDirectories = createMap() as ConfigFileMap>; const allWatchedInputFiles = createMap() as ConfigFileMap>; const allWatchedConfigFiles = createMap() as ConfigFileMap; - return { - buildAllProjects, - cleanAllProjects, - resetBuildContext, - getBuildOrder, - - invalidateProject, - buildInvalidatedProject, - - startWatching - }; + return watch ? + { + buildAllProjects, + startWatching + } : + { + buildAllProjects, + cleanAllProjects, + getBuildOrder, + resetBuildContext, + getUpToDateStatusOfProject, + invalidateProject, + buildInvalidatedProject, + }; function toPath(fileName: string) { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); @@ -392,8 +401,8 @@ namespace ts { return resolvedPath; } - function resetBuildContext(opts = defaultOptions) { - options = opts; + function resetBuildContext(opts?: BuildOptions) { + options = opts || defaultOptions; baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); resolvedConfigFilePaths.clear(); configFileCache.clear(); @@ -462,7 +471,7 @@ namespace ts { } function watchConfigFile(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { - if (options.watch && !allWatchedConfigFiles.has(resolvedPath)) { + if (watch && !allWatchedConfigFiles.has(resolvedPath)) { allWatchedConfigFiles.set(resolvedPath, watchFile( hostWithWatch, resolved, @@ -477,7 +486,7 @@ namespace ts { } function watchWildCardDirectories(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!options.watch) return; + if (!watch) return; updateWatchingWildcardDirectories( getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolvedPath), createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), @@ -508,7 +517,7 @@ namespace ts { } function watchInputFiles(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!options.watch) return; + if (!watch) return; mutateMap( getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolvedPath), arrayToMap(parsed.fileNames, toPath), @@ -565,8 +574,14 @@ namespace ts { scheduleBuildInvalidatedProject(); } + function getUpToDateStatusOfProject(project: string): UpToDateStatus { + const configFileName = resolveProjectName(project); + const configFilePath = toResolvedConfigFilePath(configFileName); + return getUpToDateStatus(parseConfigFile(configFileName, configFilePath), configFilePath); + } + function getBuildOrder() { - return buildOrder || (buildOrder = createBuildOrder(rootNames.map(resolveProjectName))); + return buildOrder || (buildOrder = createBuildOrder(resolveProjectNames(rootNames))); } function getUpToDateStatus(project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { @@ -851,7 +866,7 @@ namespace ts { if (buildProject) { buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); if (hasPendingInvalidatedProjects()) { - if (options.watch && !timerToBuildInvalidatedProject) { + if (watch && !timerToBuildInvalidatedProject) { scheduleBuildInvalidatedProject(); } } @@ -862,7 +877,7 @@ namespace ts { } function reportErrorSummary() { - if (options.watch || (host as SolutionBuilderHost).reportErrorSummary) { + if (watch || host.reportErrorSummary) { // Report errors from the other projects getBuildOrder().forEach(project => { const projectPath = toResolvedConfigFilePath(project); @@ -872,11 +887,11 @@ namespace ts { }); let totalErrors = 0; diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); - if (options.watch) { + if (watch) { reportWatchStatus(getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); } else { - (host as SolutionBuilderHost).reportErrorSummary!(totalErrors); + host.reportErrorSummary!(totalErrors); } } } @@ -1165,7 +1180,7 @@ namespace ts { if (host.afterProgramEmitAndDiagnostics) { host.afterProgramEmitAndDiagnostics(program); } - if (options.watch) { + if (watch) { program.releaseProgram(); builderPrograms.set(proj, program); } @@ -1312,8 +1327,12 @@ namespace ts { return resolveConfigFileProjectName(resolvePath(host.getCurrentDirectory(), name)); } + function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] { + return configFileNames.map(resolveProjectName); + } + function buildAllProjects(): ExitStatus { - if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } + if (watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } // TODO:: In watch mode as well to use caches for incremental build once we can invalidate caches correctly and have right api // Override readFile for json files and output .d.ts to cache the text const savedReadFileWithCache = readFileWithCache; diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 2621bd303ead7..c004f53d38e70 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -337,6 +337,61 @@ namespace ts { }); }); + describe("project invalidation", () => { + it("invalidates projects correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + + // Update a timestamp in the middle project + tick(); + appendText(fs, "/src/logic/index.ts", "function foo() {}"); + const originalWriteFile = fs.writeFileSync; + const writtenFiles = createMap(); + fs.writeFileSync = (path, data, encoding) => { + writtenFiles.set(path, true); + originalWriteFile.call(fs, path, data, encoding); + }; + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfProject("/src/logic"); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + verifyInvalidation(/*expectedToWriteTests*/ false); + + // Rebuild this project + fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} +export class cNew {}`); + verifyInvalidation(/*expectedToWriteTests*/ true); + + function verifyInvalidation(expectedToWriteTests: boolean) { + // Rebuild this project + tick(); + builder.invalidateProject("/src/logic"); + builder.buildInvalidatedProject(); + // The file should be updated + assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + writtenFiles.clear(); + + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildInvalidatedProject(); + if (expectedToWriteTests) { + assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); + } + else { + assert.equal(writtenFiles.size, 0, "Should not write any new files"); + } + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + } + }); + }); + describe("lists files", () => { it("listFiles", () => { const fs = projFs.shadow(); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 005956a98ad1d..ef87aa1f9cb76 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -17,12 +17,13 @@ namespace ts.tscWatch { } export function createSolutionBuilder(system: WatchedSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { - const host = createSolutionBuilderWithWatchHost(system); - return ts.createSolutionBuilder(host, rootNames, defaultOptions || { watch: true }); + const host = createSolutionBuilderHost(system); + return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); } - function createSolutionBuilderWithWatch(host: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { - const solutionBuilder = createSolutionBuilder(host, rootNames, defaultOptions); + function createSolutionBuilderWithWatch(system: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true }); solutionBuilder.buildAllProjects(); solutionBuilder.startWatching(); return solutionBuilder; @@ -1140,9 +1141,7 @@ export function gfoo() { it("incremental updates in verbose mode", () => { const host = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); - const solutionBuilder = createSolutionBuilder(host, [`${project}/${SubProject.tests}`], { verbose: true, watch: true }); - solutionBuilder.buildAllProjects(); - solutionBuilder.startWatching(); + createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`], { verbose: true, watch: true }); checkOutputErrorsInitial(host, emptyArray, /*disableConsoleClears*/ undefined, [ `Projects in this build: \r\n * sample1/core/tsconfig.json\r\n * sample1/logic/tsconfig.json\r\n * sample1/tests/tsconfig.json\n\n`, `Project 'sample1/core/tsconfig.json' is out of date because output file 'sample1/core/anotherModule.js' does not exist\n\n`, diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 500480463bdbb..35bd29ea77438 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -183,6 +183,9 @@ namespace ts { function performBuild(args: string[]) { const { buildOptions, projects, errors } = parseBuildCommand(args); + // Update to pretty if host supports it + updateReportDiagnostic(buildOptions); + if (errors.length > 0) { errors.forEach(reportDiagnostic); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); @@ -194,8 +197,6 @@ namespace ts { return sys.exit(ExitStatus.Success); } - // Update to pretty if host supports it - updateReportDiagnostic(buildOptions); if (projects.length === 0) { printVersion(); printHelp(buildOpts, "--build "); @@ -210,24 +211,21 @@ namespace ts { reportWatchModeWithoutSysSupport(); } - // Use default createProgram - const buildHost = buildOptions.watch ? - createSolutionBuilderWithWatchHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createWatchStatusReporter(buildOptions)) : - createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); - updateCreateProgram(buildHost); - buildHost.afterProgramEmitAndDiagnostics = (program: BuilderProgram) => reportStatistics(program.getProgram()); - - const builder = createSolutionBuilder(buildHost, projects, buildOptions); - if (buildOptions.clean) { - return sys.exit(builder.cleanAllProjects()); - } - if (buildOptions.watch) { + const buildHost = createSolutionBuilderWithWatchHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createWatchStatusReporter(buildOptions)); + updateCreateProgram(buildHost); + buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); + const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions); builder.buildAllProjects(); - return (builder as SolutionBuilderWithWatch).startWatching(); + return builder.startWatching(); + } + else { + const buildHost = createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); + updateCreateProgram(buildHost); + buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); + const builder = createSolutionBuilder(buildHost, projects, buildOptions); + return sys.exit(buildOptions.clean ? builder.cleanAllProjects() : builder.buildAllProjects()); } - - return sys.exit(builder.buildAllProjects()); } function createReportErrorSummary(options: CompilerOptions | BuildOptions): ReportEmitErrorSummary | undefined { From 4b572bea3703cc12b9086a6f4bbfcda4fca58897 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 19 Apr 2019 14:16:29 -0700 Subject: [PATCH 18/41] Instead of having two separate paths for building projects, (build all or invalidated use single path) --- src/compiler/tsbuild.ts | 212 +++++++++++++++++++--------------------- 1 file changed, 103 insertions(+), 109 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 9ec1b3f99c45c..5690851e0b8c6 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -195,7 +195,6 @@ namespace ts { } type ResolvedConfigFilePath = ResolvedConfigFileName & Path; - interface FileMap extends Map { get(key: U): T | undefined; has(key: U): boolean; @@ -208,7 +207,6 @@ namespace ts { delete(key: U): boolean; clear(): void; } - type ConfigFileMap = FileMap; function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFilePath, createT: () => T): T { @@ -811,10 +809,13 @@ namespace ts { configFileCache.delete(resolved); buildOrder = undefined; } + clearProjectStatus(resolved); + addProjToQueue(resolved, reloadLevel); + } + + function clearProjectStatus(resolved: ResolvedConfigFilePath) { projectStatus.delete(resolved); diagnostics.delete(resolved); - - addProjToQueue(resolved, reloadLevel); } /** @@ -896,87 +897,121 @@ namespace ts { } } - function buildSingleInvalidatedProject(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { - const resolvedPath = toResolvedConfigFilePath(resolved); - const proj = parseConfigFile(resolved, resolvedPath); - if (!proj) { - reportParseConfigFileDiagnostic(resolvedPath); + function buildSingleInvalidatedProject(project: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { + const projectPath = toResolvedConfigFilePath(project); + const config = parseConfigFile(project, projectPath); + if (!config) { + reportParseConfigFileDiagnostic(projectPath); return; } if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(resolved, resolvedPath); - watchWildCardDirectories(resolved, resolvedPath, proj); - watchInputFiles(resolved, resolvedPath, proj); + watchConfigFile(project, projectPath); + watchWildCardDirectories(project, projectPath, config); + watchInputFiles(project, projectPath, config); } else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { // Update file names - const result = getFileNamesFromConfigSpecs(proj.configFileSpecs!, getDirectoryPath(resolved), proj.options, parseConfigFileHost); - updateErrorForNoInputFiles(result, resolved, proj.configFileSpecs!, proj.errors, canJsonReportNoInutFiles(proj.raw)); - proj.fileNames = result.fileNames; - watchInputFiles(resolved, resolvedPath, proj); + const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, parseConfigFileHost); + updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); + config.fileNames = result.fileNames; + watchInputFiles(project, projectPath, config); } - const status = getUpToDateStatus(proj, resolvedPath); - verboseReportProjectStatus(resolved, status); - - if (status.type === UpToDateStatusType.UpstreamBlocked) { - if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); + const status = getUpToDateStatus(config, projectPath); + verboseReportProjectStatus(project, status); + if (status.type === UpToDateStatusType.UpToDate && !options.force) { + reportAndStoreErrors(projectPath, config.errors); + // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportStatus(Diagnostics.Project_0_is_up_to_date, project); + } return; } - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !options.force) { + reportAndStoreErrors(projectPath, config.errors); // Fake that files have been built by updating output file stamps - updateOutputTimestamps(proj, resolvedPath); + updateOutputTimestamps(config, projectPath); return; } - const buildResult = needsBuild(status, proj) ? - buildSingleProject(resolved, resolvedPath) : // Actual build - updateBundle(resolved, resolvedPath); // Fake that files have been built by manipulating prepend and existing output - if (buildResult & BuildResultFlags.AnyErrors) return; + if (status.type === UpToDateStatusType.UpstreamBlocked) { + reportAndStoreErrors(projectPath, config.errors); + if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); + return; + } + + if (status.type === UpToDateStatusType.ContainerOnly) { + reportAndStoreErrors(projectPath, config.errors); + // Do nothing + return; + } + const buildResult = needsBuild(status, config) ? + buildSingleProject(project, projectPath) : // Actual build + updateBundle(project, projectPath); // Fake that files have been built by manipulating prepend and existing output // Only composite projects can be referenced by other projects - if (!proj.options.composite) return; - const buildOrder = getBuildOrder(); + if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { + queueReferencingProjects(project, projectPath, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); + } + } + function queueReferencingProjects(project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, declarationOutputChanged: boolean) { // Always use build order to queue projects - for (let index = buildOrder.indexOf(resolved) + 1; index < buildOrder.length; index++) { - const project = buildOrder[index]; - const projectPath = toResolvedConfigFilePath(project); - if (projectPendingBuild.has(projectPath)) continue; - - const config = parseConfigFile(project, projectPath); - if (!config || !config.projectReferences) continue; - for (const ref of config.projectReferences) { + const buildOrder = getBuildOrder(); + for (let index = buildOrder.indexOf(project) + 1; index < buildOrder.length; index++) { + const nextProject = buildOrder[index]; + const nextProjectPath = toResolvedConfigFilePath(nextProject); + if (projectPendingBuild.has(nextProjectPath)) continue; + + const nextProjectConfig = parseConfigFile(nextProject, nextProjectPath); + if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; + for (const ref of nextProjectConfig.projectReferences) { const resolvedRefPath = resolveProjectName(ref.path); - if (toResolvedConfigFilePath(resolvedRefPath) !== resolvedPath) continue; + if (toResolvedConfigFilePath(resolvedRefPath) !== projectPath) continue; // If the project is referenced with prepend, always build downstream projects, // If declaration output is changed, build the project // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps - const status = projectStatus.get(projectPath); - if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { - if (status && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes || status.type === UpToDateStatusType.OutOfDateWithPrepend)) { - projectStatus.set(projectPath, { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, - newerProjectName: resolved - }); - } - } - else if (status && status.type === UpToDateStatusType.UpToDate) { - if (ref.prepend) { - projectStatus.set(projectPath, { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: status.oldestOutputFileName, - newerProjectName: resolved - }); - } - else { - status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; + const status = projectStatus.get(nextProjectPath); + if (status) { + switch (status.type) { + case UpToDateStatusType.UpToDate: + if (!declarationOutputChanged) { + if (ref.prepend) { + projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: status.oldestOutputFileName, + newerProjectName: project + }); + } + else { + status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; + } + break; + } + + // falls through + case UpToDateStatusType.UpToDateWithUpstreamTypes: + case UpToDateStatusType.OutOfDateWithPrepend: + if (declarationOutputChanged) { + projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, + newerProjectName: project + }); + } + break; + + case UpToDateStatusType.UpstreamBlocked: + if (toResolvedConfigFilePath(resolveProjectName(status.upstreamProjectName)) === projectPath) { + clearProjectStatus(nextProjectPath); + } + break; } } - addProjToQueue(projectPath, ConfigFileProgramReloadLevel.None); + addProjToQueue(nextProjectPath, ConfigFileProgramReloadLevel.None); break; } } @@ -1354,55 +1389,12 @@ namespace ts { const buildOrder = getBuildOrder(); reportBuildQueue(buildOrder); - let anyFailed = false; - for (const next of buildOrder) { - const resolvedPath = toResolvedConfigFilePath(next); - const proj = parseConfigFile(next, resolvedPath); - if (proj === undefined) { - reportParseConfigFileDiagnostic(resolvedPath); - anyFailed = true; - break; - } - - // report errors early when using continue or break statements - const errors = proj.errors; - const status = getUpToDateStatus(proj, resolvedPath); - verboseReportProjectStatus(next, status); - - if (status.type === UpToDateStatusType.UpToDate && !options.force) { - reportAndStoreErrors(resolvedPath, errors); - // Up to date, skip - if (defaultOptions.dry) { - // In a dry build, inform the user of this fact - reportStatus(Diagnostics.Project_0_is_up_to_date, next); - } - continue; - } + buildOrder.forEach(configFileName => + projectPendingBuild.set(toResolvedConfigFilePath(configFileName), ConfigFileProgramReloadLevel.None)); - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !options.force) { - reportAndStoreErrors(resolvedPath, errors); - // Fake build - updateOutputTimestamps(proj, resolvedPath); - continue; - } - - if (status.type === UpToDateStatusType.UpstreamBlocked) { - reportAndStoreErrors(resolvedPath, errors); - if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, next, status.upstreamProjectName); - continue; - } - - if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(resolvedPath, errors); - // Do nothing - continue; - } - - const buildResult = needsBuild(status, proj) ? - buildSingleProject(next, resolvedPath) : // Actual build - updateBundle(next, resolvedPath); // Fake that files have been built by manipulating prepend and existing output - - anyFailed = anyFailed || !!(buildResult & BuildResultFlags.AnyErrors); + while (hasPendingInvalidatedProjects()) { + const buildProject = getNextInvalidatedProject()!; + buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); } reportErrorSummary(); host.readFile = originalReadFile; @@ -1414,7 +1406,7 @@ namespace ts { readFileWithCache = savedReadFileWithCache; compilerHost.resolveModuleNames = originalResolveModuleNames; moduleResolutionCache = undefined; - return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; + return diagnostics.size ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; } function needsBuild(status: UpToDateStatus, config: ParsedCommandLine) { @@ -1431,7 +1423,9 @@ namespace ts { function reportAndStoreErrors(proj: ResolvedConfigFilePath, errors: ReadonlyArray) { reportErrors(errors); projectErrorsReported.set(proj, true); - diagnostics.set(proj, errors); + if (errors.length) { + diagnostics.set(proj, errors); + } } function reportErrors(errors: ReadonlyArray) { From 04a972b9f384738769e90e133e88340d64cbc043 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 19 Apr 2019 15:13:55 -0700 Subject: [PATCH 19/41] More refactoring --- src/compiler/tsbuild.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 5690851e0b8c6..1b298f248f205 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -832,14 +832,15 @@ namespace ts { } function getNextInvalidatedProject() { - for (const project of getBuildOrder()) { + Debug.assert(hasPendingInvalidatedProjects()); + return forEach(getBuildOrder(), (project, projectIndex) => { const projectPath = toResolvedConfigFilePath(project); const reloadLevel = projectPendingBuild.get(projectPath); if (reloadLevel !== undefined) { projectPendingBuild.delete(projectPath); - return { project, reloadLevel }; + return { project, projectPath, reloadLevel, projectIndex }; } - } + }); } function hasPendingInvalidatedProjects() { @@ -863,9 +864,8 @@ namespace ts { projectErrorsReported.clear(); reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); } - const buildProject = getNextInvalidatedProject(); - if (buildProject) { - buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); + if (hasPendingInvalidatedProjects()) { + buildNextInvalidatedProject(); if (hasPendingInvalidatedProjects()) { if (watch && !timerToBuildInvalidatedProject) { scheduleBuildInvalidatedProject(); @@ -897,8 +897,8 @@ namespace ts { } } - function buildSingleInvalidatedProject(project: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { - const projectPath = toResolvedConfigFilePath(project); + function buildNextInvalidatedProject() { + const { project, projectPath, reloadLevel, projectIndex } = getNextInvalidatedProject()!; const config = parseConfigFile(project, projectPath); if (!config) { reportParseConfigFileDiagnostic(projectPath); @@ -954,14 +954,14 @@ namespace ts { updateBundle(project, projectPath); // Fake that files have been built by manipulating prepend and existing output // Only composite projects can be referenced by other projects if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { - queueReferencingProjects(project, projectPath, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); + queueReferencingProjects(project, projectPath, projectIndex, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); } } - function queueReferencingProjects(project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, declarationOutputChanged: boolean) { + function queueReferencingProjects(project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, declarationOutputChanged: boolean) { // Always use build order to queue projects const buildOrder = getBuildOrder(); - for (let index = buildOrder.indexOf(project) + 1; index < buildOrder.length; index++) { + for (let index = projectIndex + 1; index < buildOrder.length; index++) { const nextProject = buildOrder[index]; const nextProjectPath = toResolvedConfigFilePath(nextProject); if (projectPendingBuild.has(nextProjectPath)) continue; @@ -1393,8 +1393,7 @@ namespace ts { projectPendingBuild.set(toResolvedConfigFilePath(configFileName), ConfigFileProgramReloadLevel.None)); while (hasPendingInvalidatedProjects()) { - const buildProject = getNextInvalidatedProject()!; - buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); + buildNextInvalidatedProject(); } reportErrorSummary(); host.readFile = originalReadFile; From 1a75c62ceb077454b3d2e5bd652690da136f54e6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 19 Apr 2019 15:37:23 -0700 Subject: [PATCH 20/41] Remove resetBuildContext --- src/compiler/tsbuild.ts | 29 ++----------------- src/testRunner/unittests/tsbuild/outFile.ts | 14 ++++----- .../unittests/tsbuild/resolveJsonModule.ts | 12 ++++---- src/testRunner/unittests/tsbuild/sample.ts | 9 +++--- 4 files changed, 19 insertions(+), 45 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 1b298f248f205..5d36be723145d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -260,7 +260,6 @@ namespace ts { // Currently used for testing but can be made public if needed: /*@internal*/ getBuildOrder(): ReadonlyArray; - /*@internal*/ resetBuildContext(opts?: BuildOptions): void; // Testing only /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; @@ -337,8 +336,8 @@ namespace ts { const parseConfigFileHost = parseConfigHostFromCompilerHostLike(host); // State of the solution - let options = defaultOptions; - let baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); + const options = defaultOptions; + const baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); const resolvedConfigFilePaths = createMap(); type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; const configFileCache = createMap() as ConfigFileMap; @@ -380,7 +379,6 @@ namespace ts { buildAllProjects, cleanAllProjects, getBuildOrder, - resetBuildContext, getUpToDateStatusOfProject, invalidateProject, buildInvalidatedProject, @@ -399,29 +397,6 @@ namespace ts { return resolvedPath; } - function resetBuildContext(opts?: BuildOptions) { - options = opts || defaultOptions; - baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); - resolvedConfigFilePaths.clear(); - configFileCache.clear(); - projectStatus.clear(); - buildOrder = undefined; - buildInfoChecked.clear(); - - diagnostics.clear(); - projectPendingBuild.clear(); - projectErrorsReported.clear(); - if (timerToBuildInvalidatedProject) { - clearTimeout(timerToBuildInvalidatedProject); - timerToBuildInvalidatedProject = undefined; - } - reportFileChangeDetected = false; - clearMap(allWatchedWildcardDirectories, wildCardWatches => clearMap(wildCardWatches, closeFileWatcherOf)); - clearMap(allWatchedInputFiles, inputFileWatches => clearMap(inputFileWatches, closeFileWatcher)); - clearMap(allWatchedConfigFiles, closeFileWatcher); - builderPrograms.clear(); - } - function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { return !!(entry as ParsedCommandLine).options; } diff --git a/src/testRunner/unittests/tsbuild/outFile.ts b/src/testRunner/unittests/tsbuild/outFile.ts index 48beeef4f61da..20061c86a7b3f 100644 --- a/src/testRunner/unittests/tsbuild/outFile.ts +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -388,7 +388,7 @@ namespace ts { ...outputFiles[project.third] ]; const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); + let builder = createSolutionBuilder(host); builder.buildAllProjects(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist @@ -398,7 +398,7 @@ namespace ts { // Delete bundle info host.clearDiagnostics(); host.deleteFile(outputFiles[project.first][ext.buildinfo]); - builder.resetBuildContext(); + builder = createSolutionBuilder(host); builder.buildAllProjects(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), @@ -428,11 +428,11 @@ namespace ts { it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { const fs = outFileFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); + let builder = createSolutionBuilder(host); builder.buildAllProjects(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); host.clearDiagnostics(); - builder.resetBuildContext(); + builder = createSolutionBuilder(host); changeCompilerVersion(host); builder.buildAllProjects(); host.assertDiagnosticMessages( @@ -453,7 +453,7 @@ namespace ts { // Build with command line incremental const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, { incremental: true }); + let builder = createSolutionBuilder(host, { incremental: true }); builder.buildAllProjects(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); host.clearDiagnostics(); @@ -461,7 +461,7 @@ namespace ts { // Make non incremental build with change in file that doesnt affect dts appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); - builder.resetBuildContext({ verbose: true }); + builder = createSolutionBuilder(host, { verbose: true }); builder.buildAllProjects(); host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.first][source.config], relOutputFiles[project.first][ext.js], relSources[project.first][source.ts][part.one]], @@ -475,7 +475,7 @@ namespace ts { // Make incremental build with change in file that doesnt affect dts appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); - builder.resetBuildContext({ verbose: true, incremental: true }); + builder = createSolutionBuilder(host, { verbose: true, incremental: true }); builder.buildAllProjects(); // Builds completely because tsbuildinfo is old. host.assertDiagnosticMessages( diff --git a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts index 3fc50d72ea5ee..d2555a730a2ec 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -64,7 +64,7 @@ export default hello.hello`); const configFile = "src/tsconfig_withFiles.json"; replaceText(fs, configFile, `"composite": true,`, `"composite": true, "sourceMap": true,`); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, [configFile], { verbose: true }); + let builder = createSolutionBuilder(host, [configFile], { verbose: true }); builder.buildAllProjects(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(configFile), @@ -75,7 +75,7 @@ export default hello.hello`); assert(fs.existsSync(output), `Expect file ${output} to exist`); } host.clearDiagnostics(); - builder.resetBuildContext(); + builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); builder.buildAllProjects(); host.assertDiagnosticMessages( @@ -89,7 +89,7 @@ export default hello.hello`); const configFile = "src/tsconfig_withFiles.json"; replaceText(fs, configFile, `"outDir": "dist",`, ""); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, [configFile], { verbose: true }); + let builder = createSolutionBuilder(host, [configFile], { verbose: true }); builder.buildAllProjects(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(configFile), @@ -100,7 +100,7 @@ export default hello.hello`); assert(fs.existsSync(output), `Expect file ${output} to exist`); } host.clearDiagnostics(); - builder.resetBuildContext(); + builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); builder.buildAllProjects(); host.assertDiagnosticMessages( @@ -128,7 +128,7 @@ export default hello.hello`); const stringsConfigFile = "src/strings/tsconfig.json"; const mainConfigFile = "src/main/tsconfig.json"; const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, [configFile], { verbose: true }); + let builder = createSolutionBuilder(host, [configFile], { verbose: true }); builder.buildAllProjects(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(stringsConfigFile, mainConfigFile, configFile), @@ -139,7 +139,7 @@ export default hello.hello`); ); assert(fs.existsSync(expectedOutput), `Expect file ${expectedOutput} to exist`); host.clearDiagnostics(); - builder.resetBuildContext(); + builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); builder.buildAllProjects(); host.assertDiagnosticMessages( diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index c004f53d38e70..0682284e1d7f9 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -171,11 +171,11 @@ namespace ts { function initializeWithBuild(opts?: BuildOptions) { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); builder.buildAllProjects(); host.clearDiagnostics(); tick(); - builder.resetBuildContext(opts ? { ...opts, verbose: true } : undefined); + builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true }); return { fs, host, builder }; } @@ -183,7 +183,6 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.resetBuildContext(); builder.buildAllProjects(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), @@ -288,7 +287,7 @@ namespace ts { fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: { target: "es3" } })); replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); builder.buildAllProjects(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), @@ -301,7 +300,7 @@ namespace ts { ); host.clearDiagnostics(); tick(); - builder.resetBuildContext(); + builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })); builder.buildAllProjects(); host.assertDiagnosticMessages( From 5b361c8497d2cc25d0e25f97c11e5c2394429a2a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 2 May 2019 13:27:36 -0700 Subject: [PATCH 21/41] Make API to build project and wire cancellation token --- src/compiler/tsbuild.ts | 151 +++++++++++++----- src/compiler/types.ts | 3 + src/compiler/watch.ts | 29 ++-- .../unittests/tsbuild/emptyFiles.ts | 4 +- src/testRunner/unittests/tsbuild/helpers.ts | 2 +- .../unittests/tsbuild/missingExtendedFile.ts | 2 +- src/testRunner/unittests/tsbuild/outFile.ts | 37 +++-- .../tsbuild/referencesWithRootDirInParent.ts | 8 +- .../unittests/tsbuild/resolveJsonModule.ts | 14 +- src/testRunner/unittests/tsbuild/sample.ts | 80 ++++++---- .../unittests/tsbuild/transitiveReferences.ts | 2 +- src/testRunner/unittests/tsbuildWatchMode.ts | 12 +- .../unittests/tsserver/projectReferences.ts | 2 +- src/tsc/tsc.ts | 4 +- 14 files changed, 233 insertions(+), 117 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index dd9b62edd7a7b..4fb2edf68016f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -255,7 +255,7 @@ namespace ts { } export interface SolutionBuilder { - buildAllProjects(): ExitStatus; + build(project?: string, cancellationToken?: CancellationToken): ExitStatus; cleanAllProjects(): ExitStatus; // Currently used for testing but can be made public if needed: @@ -264,14 +264,21 @@ namespace ts { // Testing only /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; - /*@internal*/ buildInvalidatedProject(): void; + /*@internal*/ buildNextInvalidatedProject(): void; } export interface SolutionBuilderWithWatch { - buildAllProjects(): ExitStatus; + build(project?: string, cancellationToken?: CancellationToken): ExitStatus; /*@internal*/ startWatching(): void; } + interface InvalidatedProject { + project: ResolvedConfigFileName; + projectPath: ResolvedConfigFilePath; + reloadLevel: ConfigFileProgramReloadLevel; + projectIndex: number; + } + /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic */ @@ -386,18 +393,22 @@ namespace ts { const allWatchedInputFiles = createMap() as ConfigFileMap>; const allWatchedConfigFiles = createMap() as ConfigFileMap; + let allProjectBuildPending = true; + let needsSummary = true; + // let watchAllProjectsPending = watch; + return watch ? { - buildAllProjects, + build, startWatching } : { - buildAllProjects, + build, cleanAllProjects, getBuildOrder, getUpToDateStatusOfProject, invalidateProject, - buildInvalidatedProject, + buildNextInvalidatedProject, }; function toPath(fileName: string) { @@ -800,6 +811,7 @@ namespace ts { configFileCache.delete(resolved); buildOrder = undefined; } + needsSummary = true; clearProjectStatus(resolved); addProjToQueue(resolved, reloadLevel); enableCache(); @@ -823,16 +835,16 @@ namespace ts { } } - function getNextInvalidatedProject() { - Debug.assert(hasPendingInvalidatedProjects()); - return forEach(getBuildOrder(), (project, projectIndex) => { - const projectPath = toResolvedConfigFilePath(project); - const reloadLevel = projectPendingBuild.get(projectPath); - if (reloadLevel !== undefined) { - projectPendingBuild.delete(projectPath); - return { project, projectPath, reloadLevel, projectIndex }; - } - }); + function getNextInvalidatedProject(buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { + return hasPendingInvalidatedProjects() ? + forEach(buildOrder, (project, projectIndex) => { + const projectPath = toResolvedConfigFilePath(project); + const reloadLevel = projectPendingBuild.get(projectPath); + if (reloadLevel !== undefined) { + return { project, projectPath, reloadLevel, projectIndex }; + } + }) : + undefined; } function hasPendingInvalidatedProjects() { @@ -846,18 +858,19 @@ namespace ts { if (timerToBuildInvalidatedProject) { hostWithWatch.clearTimeout(timerToBuildInvalidatedProject); } - timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildInvalidatedProject, 250); + timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250); } - function buildInvalidatedProject() { + function buildNextInvalidatedProject() { timerToBuildInvalidatedProject = undefined; if (reportFileChangeDetected) { reportFileChangeDetected = false; projectErrorsReported.clear(); reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); } - if (hasPendingInvalidatedProjects()) { - buildNextInvalidatedProject(); + const invalidatedProject = getNextInvalidatedProject(getBuildOrder()); + if (invalidatedProject) { + buildInvalidatedProject(invalidatedProject); if (hasPendingInvalidatedProjects()) { if (watch && !timerToBuildInvalidatedProject) { scheduleBuildInvalidatedProject(); @@ -872,6 +885,7 @@ namespace ts { function reportErrorSummary() { if (watch || host.reportErrorSummary) { + needsSummary = false; // Report errors from the other projects getBuildOrder().forEach(project => { const projectPath = toResolvedConfigFilePath(project); @@ -890,11 +904,11 @@ namespace ts { } } - function buildNextInvalidatedProject() { - const { project, projectPath, reloadLevel, projectIndex } = getNextInvalidatedProject()!; + function buildInvalidatedProject({ project, projectPath, reloadLevel, projectIndex }: InvalidatedProject, cancellationToken?: CancellationToken) { const config = parseConfigFile(project, projectPath); if (!config) { reportParseConfigFileDiagnostic(projectPath); + projectPendingBuild.delete(projectPath); return; } @@ -920,6 +934,7 @@ namespace ts { // In a dry build, inform the user of this fact reportStatus(Diagnostics.Project_0_is_up_to_date, project); } + projectPendingBuild.delete(projectPath); return; } @@ -927,24 +942,28 @@ namespace ts { reportAndStoreErrors(projectPath, config.errors); // Fake that files have been built by updating output file stamps updateOutputTimestamps(config, projectPath); + projectPendingBuild.delete(projectPath); return; } if (status.type === UpToDateStatusType.UpstreamBlocked) { reportAndStoreErrors(projectPath, config.errors); if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); + projectPendingBuild.delete(projectPath); return; } if (status.type === UpToDateStatusType.ContainerOnly) { reportAndStoreErrors(projectPath, config.errors); // Do nothing + projectPendingBuild.delete(projectPath); return; } const buildResult = needsBuild(status, config) ? - buildSingleProject(project, projectPath) : // Actual build - updateBundle(project, projectPath); // Fake that files have been built by manipulating prepend and existing output + buildSingleProject(project, projectPath, cancellationToken) : // Actual build + updateBundle(project, projectPath, cancellationToken); // Fake that files have been built by manipulating prepend and existing output + projectPendingBuild.delete(projectPath); // Only composite projects can be referenced by other projects if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { queueReferencingProjects(project, projectPath, projectIndex, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); @@ -1050,7 +1069,7 @@ namespace ts { } } - function buildSingleProject(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath): BuildResultFlags { + function buildSingleProject(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { if (options.dry) { reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj); return BuildResultFlags.Success; @@ -1112,15 +1131,15 @@ namespace ts { // Don't emit anything in the presence of syntactic errors or options diagnostics const syntaxDiagnostics = [ ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(), - ...program.getGlobalDiagnostics(), - ...program.getSyntacticDiagnostics()]; + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; if (syntaxDiagnostics.length) { return buildErrors(syntaxDiagnostics, BuildResultFlags.SyntaxErrors, "Syntactic"); } // Same as above but now for semantic diagnostics - const semanticDiagnostics = program.getSemanticDiagnostics(); + const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); if (semanticDiagnostics.length) { return buildErrors(semanticDiagnostics, BuildResultFlags.TypeErrors, "Semantic"); } @@ -1132,7 +1151,14 @@ namespace ts { let declDiagnostics: Diagnostic[] | undefined; const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); const outputFiles: OutputFile[] = []; - emitFilesAndReportErrors(program, reportDeclarationDiagnostics, /*writeFileName*/ undefined, /*reportSummary*/ undefined, (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark })); + emitFilesAndReportErrors( + program, + reportDeclarationDiagnostics, + /*writeFileName*/ undefined, + /*reportSummary*/ undefined, + (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), + cancellationToken + ); // Don't emit .d.ts if there are decl file errors if (declDiagnostics) { program.restoreState(); @@ -1221,7 +1247,7 @@ namespace ts { return readBuilderProgram(parsed.options, readFileWithCache) as any as T; } - function updateBundle(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath): BuildResultFlags { + function updateBundle(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { if (options.dry) { reportStatus(Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); return BuildResultFlags.Success; @@ -1241,7 +1267,7 @@ namespace ts { }); if (isString(outputFiles)) { reportStatus(Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(outputFiles)); - return buildSingleProject(proj, resolvedPath); + return buildSingleProject(proj, resolvedPath, cancellationToken); } // Actual Emit @@ -1403,21 +1429,58 @@ namespace ts { cacheState = undefined; } - function buildAllProjects(): ExitStatus { - if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } - enableCache(); + function build(project?: string, cancellationToken?: CancellationToken): ExitStatus { + // Set initial build if not already built + if (allProjectBuildPending) { + allProjectBuildPending = false; + if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } + enableCache(); + const buildOrder = getBuildOrder(); + reportBuildQueue(buildOrder); + buildOrder.forEach(configFileName => + projectPendingBuild.set(toResolvedConfigFilePath(configFileName), ConfigFileProgramReloadLevel.None)); - const buildOrder = getBuildOrder(); - reportBuildQueue(buildOrder); - buildOrder.forEach(configFileName => - projectPendingBuild.set(toResolvedConfigFilePath(configFileName), ConfigFileProgramReloadLevel.None)); + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); + } + } - while (hasPendingInvalidatedProjects()) { - buildNextInvalidatedProject(); + let successfulProjects = 0; + let errorProjects = 0; + const resolvedProject = project && resolveProjectName(project); + if (resolvedProject) { + const projectPath = toResolvedConfigFilePath(resolvedProject); + const projectIndex = findIndex( + getBuildOrder(), + configFileName => toResolvedConfigFilePath(configFileName) === projectPath + ); + if (projectIndex === -1) return ExitStatus.InvalidProject_OutputsSkipped; + } + const buildOrder = resolvedProject ? createBuildOrder([resolvedProject]) : getBuildOrder(); + while (true) { + const invalidatedProject = getNextInvalidatedProject(buildOrder); + if (!invalidatedProject) { + if (needsSummary) { + disableCache(); + reportErrorSummary(); + } + break; + } + + buildInvalidatedProject(invalidatedProject, cancellationToken); + if (diagnostics.has(invalidatedProject.projectPath)) { + errorProjects++; + } + else { + successfulProjects++; + } } - reportErrorSummary(); - disableCache(); - return diagnostics.size ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; + + return errorProjects ? + successfulProjects ? + ExitStatus.DiagnosticsPresent_OutputsGenerated : + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; } function needsBuild(status: UpToDateStatus, config: ParsedCommandLine) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5e2d6aba474ee..791b26ab572f9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3053,6 +3053,9 @@ namespace ts { // Diagnostics were produced and outputs were generated in spite of them. DiagnosticsPresent_OutputsGenerated = 2, + + // When build skipped because passed in project is invalid + InvalidProject_OutputsSkipped = 3, } export interface EmitResult { diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index f9bf2a8468db6..d6a856cfe9f53 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -113,12 +113,12 @@ namespace ts { getCurrentDirectory(): string; getCompilerOptions(): CompilerOptions; getSourceFiles(): ReadonlyArray; - getSyntacticDiagnostics(): ReadonlyArray; - getOptionsDiagnostics(): ReadonlyArray; - getGlobalDiagnostics(): ReadonlyArray; - getSemanticDiagnostics(): ReadonlyArray; + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; getConfigFileParsingDiagnostics(): ReadonlyArray; - emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback): EmitResult; + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult; } export function listFiles(program: ProgramToEmitFilesAndReportErrors, writeFileName: (s: string) => void) { @@ -132,25 +132,32 @@ namespace ts { /** * Helper that emit files, report diagnostics and lists emitted and/or source files depending on compiler options */ - export function emitFilesAndReportErrors(program: ProgramToEmitFilesAndReportErrors, reportDiagnostic: DiagnosticReporter, writeFileName?: (s: string) => void, reportSummary?: ReportEmitErrorSummary, writeFile?: WriteFileCallback) { + export function emitFilesAndReportErrors( + program: ProgramToEmitFilesAndReportErrors, + reportDiagnostic: DiagnosticReporter, + writeFileName?: (s: string) => void, + reportSummary?: ReportEmitErrorSummary, + writeFile?: WriteFileCallback, + cancellationToken?: CancellationToken + ) { // First get and report any syntactic errors. const diagnostics = program.getConfigFileParsingDiagnostics().slice(); const configFileParsingDiagnosticsLength = diagnostics.length; - addRange(diagnostics, program.getSyntacticDiagnostics()); + addRange(diagnostics, program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)); // If we didn't have any syntactic errors, then also try getting the global and // semantic errors. if (diagnostics.length === configFileParsingDiagnosticsLength) { - addRange(diagnostics, program.getOptionsDiagnostics()); - addRange(diagnostics, program.getGlobalDiagnostics()); + addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken)); + addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken)); if (diagnostics.length === configFileParsingDiagnosticsLength) { - addRange(diagnostics, program.getSemanticDiagnostics()); + addRange(diagnostics, program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken)); } } // Emit and report any errors we ran into. - const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(/*targetSourceFile*/ undefined, writeFile); + const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken); addRange(diagnostics, emitDiagnostics); sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic); diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts index 17c6644ccc66c..badad8f3377e3 100644 --- a/src/testRunner/unittests/tsbuild/emptyFiles.ts +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -14,7 +14,7 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/no-references"], { dry: false, force: false, verbose: false }); host.clearDiagnostics(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages([Diagnostics.The_files_list_in_config_file_0_is_empty, "/src/no-references/tsconfig.json"]); // Check for outputs to not be written. @@ -29,7 +29,7 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/with-references"], { dry: false, force: false, verbose: false }); host.clearDiagnostics(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(/*empty*/); // Check for outputs to be written. diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts index 579573a876e6e..33a7afb181ab3 100644 --- a/src/testRunner/unittests/tsbuild/helpers.ts +++ b/src/testRunner/unittests/tsbuild/helpers.ts @@ -172,7 +172,7 @@ declare const console: { log(msg: any): void; };`; } return originalReadFile.call(host, path); }; - builder.buildAllProjects(); + builder.build(); generateSourceMapBaselineFiles(fs, expectedMapFileNames); generateBuildInfoSectionBaselineFiles(fs, expectedBuildInfoFilesForSectionBaselines || emptyArray); fs.makeReadonly(); diff --git a/src/testRunner/unittests/tsbuild/missingExtendedFile.ts b/src/testRunner/unittests/tsbuild/missingExtendedFile.ts index 00e8bfec1e3e4..881f3fbc030ea 100644 --- a/src/testRunner/unittests/tsbuild/missingExtendedFile.ts +++ b/src/testRunner/unittests/tsbuild/missingExtendedFile.ts @@ -5,7 +5,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tsconfig.json"], {}); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( [Diagnostics.The_specified_path_does_not_exist_Colon_0, "/src/foobar.json"], [Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, "/src/tsconfig.first.json", "[\"**/*\"]", "[]"], diff --git a/src/testRunner/unittests/tsbuild/outFile.ts b/src/testRunner/unittests/tsbuild/outFile.ts index 20061c86a7b3f..ea56c7f511de0 100644 --- a/src/testRunner/unittests/tsbuild/outFile.ts +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -363,7 +363,7 @@ namespace ts { ]; const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist for (const output of expectedOutputs) { @@ -389,7 +389,7 @@ namespace ts { ]; const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist for (const output of expectedOutputs) { @@ -399,7 +399,7 @@ namespace ts { host.clearDiagnostics(); host.deleteFile(outputFiles[project.first][ext.buildinfo]); builder = createSolutionBuilder(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.first][source.config], relOutputFiles[project.first][ext.buildinfo]], @@ -416,7 +416,7 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""); const builder = createSolutionBuilder(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist - without tsbuildinfo for third project for (const output of expectedOutputFiles.slice(0, expectedOutputFiles.length - 2)) { @@ -429,12 +429,12 @@ namespace ts { const fs = outFileFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); host.clearDiagnostics(); builder = createSolutionBuilder(host); changeCompilerVersion(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.first][source.config], fakes.version, version], @@ -454,7 +454,7 @@ namespace ts { // Build with command line incremental const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, { incremental: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); host.clearDiagnostics(); tick(); @@ -462,7 +462,7 @@ namespace ts { // Make non incremental build with change in file that doesnt affect dts appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); builder = createSolutionBuilder(host, { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.first][source.config], relOutputFiles[project.first][ext.js], relSources[project.first][source.ts][part.one]], [Diagnostics.Building_project_0, sources[project.first][source.config]], @@ -476,7 +476,7 @@ namespace ts { // Make incremental build with change in file that doesnt affect dts appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); builder = createSolutionBuilder(host, { verbose: true, incremental: true }); - builder.buildAllProjects(); + builder.build(); // Builds completely because tsbuildinfo is old. host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), @@ -489,6 +489,23 @@ namespace ts { host.clearDiagnostics(); }); + it("builds till project specified", () => { + const fs = outFileFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, { verbose: false }); + const result = builder.build(sources[project.second][source.config]); + host.assertDiagnosticMessages(/*empty*/); + // First and Third is not built + for (const output of [...outputFiles[project.first], ...outputFiles[project.third]]) { + assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); + } + // second is built + for (const output of outputFiles[project.second]) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + assert.equal(result, ExitStatus.Success); + }); + describe("Prepend output with .tsbuildinfo", () => { // Prologues describe("Prologues", () => { @@ -904,7 +921,7 @@ ${internal} enum internalEnum { a, b, c }`); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.first][source.config], "src/first/first_PART1.js"], diff --git a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts index 630cd8575eccf..c965eb73067d4 100644 --- a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -19,7 +19,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/src/main", "/src/src/other"], {}); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(/*empty*/); for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); @@ -39,7 +39,7 @@ namespace ts { replaceText(fs, "/src/tsconfig.base.json", `"rootDir": "./src/",`, ""); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/src/main"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/src/other/tsconfig.json", "src/src/main/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/src/other/tsconfig.json", "src/dist/other.js"], @@ -75,7 +75,7 @@ namespace ts { })); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/src/main"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/src/other/tsconfig.json", "src/src/main/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/src/other/tsconfig.json", "src/dist/other.js"], @@ -112,7 +112,7 @@ namespace ts { })); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/src/main/tsconfig.main.json"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/src/other/tsconfig.other.json", "src/src/main/tsconfig.main.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/src/other/tsconfig.other.json", "src/dist/other.js"], diff --git a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts index d2555a730a2ec..f1a40c050f94f 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -19,7 +19,7 @@ namespace ts { function verifyProjectWithResolveJsonModuleWithFs(fs: vfs.FileSystem, configFile: string, allExpectedOutputs: ReadonlyArray, ...expectedDiagnosticMessages: fakes.ExpectedDiagnostic[]) { const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, [configFile], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...expectedDiagnosticMessages); if (!expectedDiagnosticMessages.length) { // Check for outputs. Not an exhaustive list @@ -65,7 +65,7 @@ export default hello.hello`); replaceText(fs, configFile, `"composite": true,`, `"composite": true, "sourceMap": true,`); const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, [configFile], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(configFile), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFile, "src/dist/src/index.js"], @@ -77,7 +77,7 @@ export default hello.hello`); host.clearDiagnostics(); builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(configFile), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFile, "src/src/index.ts", "src/dist/src/index.js"] @@ -90,7 +90,7 @@ export default hello.hello`); replaceText(fs, configFile, `"outDir": "dist",`, ""); const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, [configFile], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(configFile), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFile, "src/src/index.js"], @@ -102,7 +102,7 @@ export default hello.hello`); host.clearDiagnostics(); builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(configFile), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFile, "src/src/index.ts", "src/src/index.js"] @@ -129,7 +129,7 @@ export default hello.hello`); const mainConfigFile = "src/main/tsconfig.json"; const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, [configFile], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(stringsConfigFile, mainConfigFile, configFile), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, stringsConfigFile, "src/strings/tsconfig.tsbuildinfo"], @@ -141,7 +141,7 @@ export default hello.hello`); host.clearDiagnostics(); builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild(stringsConfigFile, mainConfigFile, configFile), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, stringsConfigFile, "src/strings/foo.json", "src/strings/tsconfig.tsbuildinfo"], diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 9119fd2fb96ee..3ad2b2e3eec64 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -21,7 +21,7 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); host.clearDiagnostics(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list @@ -39,7 +39,7 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], {}); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(/*empty*/); const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/", "/logic/outDir/")); // Check for outputs. Not an exhaustive list @@ -57,7 +57,7 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], {}); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(/*empty*/); const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/index.d.ts", "/logic/out/decls/index.d.ts")); // Check for outputs. Not an exhaustive list @@ -71,7 +71,7 @@ namespace ts { replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/core"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], @@ -88,7 +88,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( [Diagnostics.A_non_dry_build_would_build_project_0, "/src/core/tsconfig.json"], [Diagnostics.A_non_dry_build_would_build_project_0, "/src/logic/tsconfig.json"], @@ -106,12 +106,12 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); + builder.build(); tick(); host.clearDiagnostics(); builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( [Diagnostics.Project_0_is_up_to_date, "/src/core/tsconfig.json"], [Diagnostics.Project_0_is_up_to_date, "/src/logic/tsconfig.json"], @@ -126,7 +126,7 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); + builder.build(); // Verify they exist for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); @@ -146,15 +146,16 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: true, verbose: false }); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: true, verbose: false }); + builder.build(); let currentTime = time(); checkOutputTimestamps(currentTime); tick(); Debug.assert(time() !== currentTime, "Time moves on"); currentTime = time(); - builder.buildAllProjects(); + builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: true, verbose: false }); + builder.build(); checkOutputTimestamps(currentTime); function checkOutputTimestamps(expected: number) { @@ -172,7 +173,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.clearDiagnostics(); tick(); builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true }); @@ -183,7 +184,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], @@ -198,7 +199,7 @@ namespace ts { // All three projects are up to date it("Detects that all projects are up to date", () => { const { host, builder } = initializeWithBuild(); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], @@ -211,7 +212,7 @@ namespace ts { it("Only builds the leaf node project", () => { const { fs, host, builder } = initializeWithBuild(); fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], @@ -225,7 +226,7 @@ namespace ts { it("Detects type-only changes in upstream projects", () => { const { fs, host, builder } = initializeWithBuild(); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), @@ -242,7 +243,7 @@ namespace ts { it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { const { host, builder } = initializeWithBuild(); changeCompilerVersion(host); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/core/tsconfig.json", fakes.version, version], @@ -256,7 +257,7 @@ namespace ts { it("rebuilds from start if --f is passed", () => { const { host, builder } = initializeWithBuild({ force: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], @@ -271,7 +272,7 @@ namespace ts { it("rebuilds when tsconfig changes", () => { const { fs, host, builder } = initializeWithBuild(); replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], @@ -287,7 +288,7 @@ namespace ts { replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); const host = new fakes.SolutionBuilderHost(fs); let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], @@ -301,7 +302,7 @@ namespace ts { tick(); builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], @@ -310,6 +311,31 @@ namespace ts { [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] ); }); + + it("builds till project specified", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + const result = builder.build("/src/logic"); + host.assertDiagnosticMessages(/*empty*/); + assert.isFalse(fs.existsSync(allExpectedOutputs[0]), `Expect file ${allExpectedOutputs[0]} to not exist`); + for (const output of allExpectedOutputs.slice(1)) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + assert.equal(result, ExitStatus.Success); + }); + + it("building project in not build order doesnt throw error", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + const result = builder.build("/src/logic2"); + host.assertDiagnosticMessages(/*empty*/); + for (const output of allExpectedOutputs) { + assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); + } + assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); + }); }); describe("downstream-blocked compilations", () => { @@ -320,7 +346,7 @@ namespace ts { // Induce an error in the middle project replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages( getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], @@ -340,7 +366,7 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(/*empty*/); // Update a timestamp in the middle project @@ -366,7 +392,7 @@ export class cNew {}`); // Rebuild this project tick(); builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProject(); + builder.buildNextInvalidatedProject(); // The file should be updated assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); @@ -376,7 +402,7 @@ export class cNew {}`); // Build downstream projects should update 'tests', but not 'core' tick(); - builder.buildInvalidatedProject(); + builder.buildNextInvalidatedProject(); if (expectedToWriteTests) { assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); } @@ -394,7 +420,7 @@ export class cNew {}`); const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { listFiles: true }); - builder.buildAllProjects(); + builder.build(); assert.deepEqual(host.traces, [ "/lib/lib.d.ts", "/src/core/anotherModule.ts", @@ -421,7 +447,7 @@ export class cNew {}`); const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { listEmittedFiles: true }); - builder.buildAllProjects(); + builder.build(); assert.deepEqual(host.traces, [ "TSFILE: /src/core/anotherModule.js", "TSFILE: /src/core/anotherModule.d.ts.map", diff --git a/src/testRunner/unittests/tsbuild/transitiveReferences.ts b/src/testRunner/unittests/tsbuild/transitiveReferences.ts index 30e28de8cdec2..94f364ddec032 100644 --- a/src/testRunner/unittests/tsbuild/transitiveReferences.ts +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -30,7 +30,7 @@ namespace ts { const host = new fakes.SolutionBuilderHost(fs); modifyDiskLayout(fs); const builder = createSolutionBuilder(host, ["/src/tsconfig.c.json"], { listFiles: true }); - builder.buildAllProjects(); + builder.build(); host.assertDiagnosticMessages(...expectedDiagnostics); for (const output of allExpectedOutputs) { assert(fs.existsSync(output), `Expect file ${output} to exist`); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index ef87aa1f9cb76..ad00145db52d9 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -24,7 +24,7 @@ namespace ts.tscWatch { function createSolutionBuilderWithWatch(system: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { const host = createSolutionBuilderWithWatchHost(system); const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true }); - solutionBuilder.buildAllProjects(); + solutionBuilder.build(); solutionBuilder.startWatching(); return solutionBuilder; } @@ -608,7 +608,7 @@ let x: string = 10;`); // Build the composite project const host = createTsBuildWatchSystem(allFiles, { currentDirectory }); const solutionBuilder = createSolutionBuilder(host, [solutionBuilderconfig], {}); - solutionBuilder.buildAllProjects(); + solutionBuilder.build(); const outputFileStamps = getOutputFileStamps(host); for (const stamp of outputFileStamps) { assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); @@ -723,7 +723,7 @@ let x: string = 10;`); function foo() { }`); solutionBuilder.invalidateProject(`${project}/${SubProject.logic}`); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.buildNextInvalidatedProject(); // not ideal, but currently because of d.ts but no new file is written // There will be timeout queued even though file contents are same @@ -736,7 +736,7 @@ function foo() { export function gfoo() { }`); solutionBuilder.invalidateProject(logic[0].path); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.buildNextInvalidatedProject(); }, expectedProgramFiles); }); @@ -747,7 +747,7 @@ export function gfoo() { references: [{ path: "../core" }] })); solutionBuilder.invalidateProject(logic[0].path, ConfigFileProgramReloadLevel.Full); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.buildNextInvalidatedProject(); }, [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]); }); }); @@ -967,7 +967,7 @@ export function gfoo() { export function gfoo() { }`); solutionBuilder.invalidateProject(bTsconfig.path); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.buildNextInvalidatedProject(); }, emptyArray, expectedProgramFiles, diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index e39c510216d76..f07774cd43c1d 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -5,7 +5,7 @@ namespace ts.projectSystem { // ts build should succeed const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {}); - solutionBuilder.buildAllProjects(); + solutionBuilder.build(); assert.equal(host.getOutput().length, 0); return host; diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 35bd29ea77438..fc5559be02387 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -216,7 +216,7 @@ namespace ts { updateCreateProgram(buildHost); buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions); - builder.buildAllProjects(); + builder.build(); return builder.startWatching(); } else { @@ -224,7 +224,7 @@ namespace ts { updateCreateProgram(buildHost); buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); const builder = createSolutionBuilder(buildHost, projects, buildOptions); - return sys.exit(buildOptions.clean ? builder.cleanAllProjects() : builder.buildAllProjects()); + return sys.exit(buildOptions.clean ? builder.cleanAllProjects() : builder.build()); } } From e8074f7fdc3aac4fd8b6e23f0067800685420d1f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 2 May 2019 14:30:59 -0700 Subject: [PATCH 22/41] Rename cleanAll to clean and take optional project as input --- src/compiler/tsbuild.ts | 59 ++++++++------- .../unittests/tsbuild/emptyFiles.ts | 8 +-- src/testRunner/unittests/tsbuild/helpers.ts | 12 ++++ src/testRunner/unittests/tsbuild/outFile.ts | 48 ++++++------- .../tsbuild/referencesWithRootDirInParent.ts | 24 ++----- .../unittests/tsbuild/resolveJsonModule.ts | 14 ++-- src/testRunner/unittests/tsbuild/sample.ts | 72 +++++++++++-------- .../unittests/tsbuild/transitiveReferences.ts | 4 +- src/tsc/tsc.ts | 2 +- 9 files changed, 124 insertions(+), 119 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 4fb2edf68016f..c075e1f799c0a 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -256,7 +256,7 @@ namespace ts { export interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; - cleanAllProjects(): ExitStatus; + clean(project?: string): ExitStatus; // Currently used for testing but can be made public if needed: /*@internal*/ getBuildOrder(): ReadonlyArray; @@ -404,7 +404,7 @@ namespace ts { } : { build, - cleanAllProjects, + clean, getBuildOrder, getUpToDateStatusOfProject, invalidateProject, @@ -1342,10 +1342,12 @@ namespace ts { return priorNewestUpdateTime; } - function getFilesToClean(): string[] { - // Get the same graph for cleaning we'd use for building - const filesToDelete: string[] = []; - for (const proj of getBuildOrder()) { + function clean(project?: string) { + const buildOrder = getBuildOrderFor(project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + const filesToDelete = options.dry ? [] as string[] : undefined; + for (const proj of buildOrder) { const resolvedPath = toResolvedConfigFilePath(proj); const parsed = parseConfigFile(proj, resolvedPath); if (parsed === undefined) { @@ -1356,22 +1358,19 @@ namespace ts { const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); for (const output of outputs) { if (host.fileExists(output)) { - filesToDelete.push(output); + if (filesToDelete) { + filesToDelete.push(output); + } + else { + host.deleteFile(output); + invalidateResolvedProject(resolvedPath, ConfigFileProgramReloadLevel.None); + } } } } - return filesToDelete; - } - function cleanAllProjects() { - const filesToDelete = getFilesToClean(); - if (options.dry) { + if (filesToDelete) { reportStatus(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); - return ExitStatus.Success; - } - - for (const output of filesToDelete) { - host.deleteFile(output); } return ExitStatus.Success; @@ -1429,7 +1428,23 @@ namespace ts { cacheState = undefined; } + function getBuildOrderFor(project: string | undefined) { + const resolvedProject = project && resolveProjectName(project); + if (resolvedProject) { + const projectPath = toResolvedConfigFilePath(resolvedProject); + const projectIndex = findIndex( + getBuildOrder(), + configFileName => toResolvedConfigFilePath(configFileName) === projectPath + ); + if (projectIndex === -1) return undefined; + } + return resolvedProject ? createBuildOrder([resolvedProject]) : getBuildOrder(); + } + function build(project?: string, cancellationToken?: CancellationToken): ExitStatus { + const buildOrder = getBuildOrderFor(project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + // Set initial build if not already built if (allProjectBuildPending) { allProjectBuildPending = false; @@ -1447,16 +1462,6 @@ namespace ts { let successfulProjects = 0; let errorProjects = 0; - const resolvedProject = project && resolveProjectName(project); - if (resolvedProject) { - const projectPath = toResolvedConfigFilePath(resolvedProject); - const projectIndex = findIndex( - getBuildOrder(), - configFileName => toResolvedConfigFilePath(configFileName) === projectPath - ); - if (projectIndex === -1) return ExitStatus.InvalidProject_OutputsSkipped; - } - const buildOrder = resolvedProject ? createBuildOrder([resolvedProject]) : getBuildOrder(); while (true) { const invalidatedProject = getNextInvalidatedProject(buildOrder); if (!invalidatedProject) { diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts index badad8f3377e3..38b8b4b537915 100644 --- a/src/testRunner/unittests/tsbuild/emptyFiles.ts +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -18,9 +18,7 @@ namespace ts { host.assertDiagnosticMessages([Diagnostics.The_files_list_in_config_file_0_is_empty, "/src/no-references/tsconfig.json"]); // Check for outputs to not be written. - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsAbsent(fs, allExpectedOutputs); }); it("does not have empty files diagnostic when files is empty and references are provided", () => { @@ -33,9 +31,7 @@ namespace ts { host.assertDiagnosticMessages(/*empty*/); // Check for outputs to be written. - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); }); } diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts index 33a7afb181ab3..7aeaba36199e6 100644 --- a/src/testRunner/unittests/tsbuild/helpers.ts +++ b/src/testRunner/unittests/tsbuild/helpers.ts @@ -82,6 +82,18 @@ declare const console: { log(msg: any): void; };`; return fs; } + export function verifyOutputsPresent(fs: vfs.FileSystem, outputs: readonly string[]) { + for (const output of outputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + } + + export function verifyOutputsAbsent(fs: vfs.FileSystem, outputs: readonly string[]) { + for (const output of outputs) { + assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); + } + } + function generateSourceMapBaselineFiles(fs: vfs.FileSystem, mapFileNames: ReadonlyArray) { for (const mapFile of mapFileNames) { if (!fs.existsSync(mapFile)) continue; diff --git a/src/testRunner/unittests/tsbuild/outFile.ts b/src/testRunner/unittests/tsbuild/outFile.ts index ea56c7f511de0..f562d4dc26b77 100644 --- a/src/testRunner/unittests/tsbuild/outFile.ts +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -366,18 +366,14 @@ namespace ts { builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, expectedOutputs); host.clearDiagnostics(); - builder.cleanAllProjects(); + builder.clean(); host.assertDiagnosticMessages(/*none*/); // Verify they are gone - for (const output of expectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsAbsent(fs, expectedOutputs); // Subsequent clean shouldn't throw / etc - builder.cleanAllProjects(); + builder.clean(); }); it("verify buildInfo absence results in new build", () => { @@ -392,9 +388,7 @@ namespace ts { builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, expectedOutputs); // Delete bundle info host.clearDiagnostics(); host.deleteFile(outputFiles[project.first][ext.buildinfo]); @@ -419,10 +413,8 @@ namespace ts { builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); // Verify they exist - without tsbuildinfo for third project - for (const output of expectedOutputFiles.slice(0, expectedOutputFiles.length - 2)) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - assert.isFalse(fs.existsSync(outputFiles[project.third][ext.buildinfo]), `Expect file ${outputFiles[project.third][ext.buildinfo]} to not exist`); + verifyOutputsPresent(fs, expectedOutputFiles.slice(0, expectedOutputFiles.length - 2)); + verifyOutputsAbsent(fs, [outputFiles[project.third][ext.buildinfo]]); }); it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { @@ -496,13 +488,23 @@ namespace ts { const result = builder.build(sources[project.second][source.config]); host.assertDiagnosticMessages(/*empty*/); // First and Third is not built - for (const output of [...outputFiles[project.first], ...outputFiles[project.third]]) { - assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsAbsent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); // second is built - for (const output of outputFiles[project.second]) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, outputFiles[project.second]); + assert.equal(result, ExitStatus.Success); + }); + + it("cleans till project specified", () => { + const fs = outFileFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, { verbose: false }); + builder.build(); + const result = builder.clean(sources[project.second][source.config]); + host.assertDiagnosticMessages(/*empty*/); + // First and Third output for present + verifyOutputsPresent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); + // second is cleaned + verifyOutputsAbsent(fs, outputFiles[project.second]); assert.equal(result, ExitStatus.Success); }); @@ -940,9 +942,7 @@ ${internal} enum internalEnum { a, b, c }`); removeFileExtension(f) + Extension.Dts + ".map", ]) ]); - for (const output of expectedOutputFiles) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, expectedOutputFiles); }); }); } diff --git a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts index c965eb73067d4..186a43b3c3e9d 100644 --- a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -21,9 +21,7 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/src/main", "/src/src/other"], {}); builder.build(); host.assertDiagnosticMessages(/*empty*/); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); it("verify that it reports error for same .tsbuildinfo file because no rootDir in the base", () => { @@ -48,12 +46,8 @@ namespace ts { [Diagnostics.Building_project_0, "/src/src/main/tsconfig.json"], [Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, "/src/dist/tsconfig.tsbuildinfo", "/src/src/other"] ); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - for (const output of missingOutputs) { - assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); + verifyOutputsAbsent(fs, missingOutputs); }); it("verify that it reports error for same .tsbuildinfo file", () => { @@ -84,12 +78,8 @@ namespace ts { [Diagnostics.Building_project_0, "/src/src/main/tsconfig.json"], [Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, "/src/dist/tsconfig.tsbuildinfo", "/src/src/other"] ); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - for (const output of missingOutputs) { - assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); + verifyOutputsAbsent(fs, missingOutputs); }); it("verify that it reports no error when .tsbuildinfo differ", () => { @@ -120,9 +110,7 @@ namespace ts { [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/src/main/tsconfig.main.json", "src/dist/a.js"], [Diagnostics.Building_project_0, "/src/src/main/tsconfig.main.json"] ); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); }); } diff --git a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts index f1a40c050f94f..66bb0dc6e9533 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -23,9 +23,7 @@ namespace ts { host.assertDiagnosticMessages(...expectedDiagnosticMessages); if (!expectedDiagnosticMessages.length) { // Check for outputs. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); } } @@ -71,9 +69,7 @@ export default hello.hello`); [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFile, "src/dist/src/index.js"], [Diagnostics.Building_project_0, `/${configFile}`] ); - for (const output of [...allExpectedOutputs, "/src/dist/src/index.js.map"]) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, [...allExpectedOutputs, "/src/dist/src/index.js.map"]); host.clearDiagnostics(); builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); @@ -96,9 +92,7 @@ export default hello.hello`); [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFile, "src/src/index.js"], [Diagnostics.Building_project_0, `/${configFile}`] ); - for (const output of ["/src/src/index.js", "/src/src/index.d.ts"]) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, ["/src/src/index.js", "/src/src/index.d.ts"]); host.clearDiagnostics(); builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); @@ -137,7 +131,7 @@ export default hello.hello`); [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, mainConfigFile, "src/main/index.js"], [Diagnostics.Building_project_0, `/${mainConfigFile}`], ); - assert(fs.existsSync(expectedOutput), `Expect file ${expectedOutput} to exist`); + verifyOutputsPresent(fs, [expectedOutput]); host.clearDiagnostics(); builder = createSolutionBuilder(host, [configFile], { verbose: true }); tick(); diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 3ad2b2e3eec64..65530e5970551 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -25,9 +25,7 @@ namespace ts { host.assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); it("builds correctly when outDir is specified", () => { @@ -43,9 +41,7 @@ namespace ts { host.assertDiagnosticMessages(/*empty*/); const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/", "/logic/outDir/")); // Check for outputs. Not an exhaustive list - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, expectedOutputs); }); it("builds correctly when declarationDir is specified", () => { @@ -61,9 +57,7 @@ namespace ts { host.assertDiagnosticMessages(/*empty*/); const expectedOutputs = allExpectedOutputs.map(f => f.replace("/logic/index.d.ts", "/logic/out/decls/index.d.ts")); // Check for outputs. Not an exhaustive list - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, expectedOutputs); }); it("builds correctly when project is not composite or doesnt have any references", () => { @@ -77,9 +71,7 @@ namespace ts { [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], [Diagnostics.Building_project_0, "/src/core/tsconfig.json"] ); - for (const output of ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map"]) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map"]); }); }); @@ -96,9 +88,7 @@ namespace ts { ); // Check for outputs to not be written. Not an exhaustive list - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsAbsent(fs, allExpectedOutputs); }); it("indicates that it would skip builds during a dry build", () => { @@ -128,16 +118,42 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.build(); // Verify they exist - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } - builder.cleanAllProjects(); + verifyOutputsPresent(fs, allExpectedOutputs); + + builder.clean(); // Verify they are gone - for (const output of allExpectedOutputs) { - assert(!fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsAbsent(fs, allExpectedOutputs); + // Subsequent clean shouldn't throw / etc - builder.cleanAllProjects(); + builder.clean(); + verifyOutputsAbsent(fs, allExpectedOutputs); + + builder.build(); + // Verify they exist + verifyOutputsPresent(fs, allExpectedOutputs); + }); + + it("cleans till project specified", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + builder.build(); + const result = builder.clean("/src/logic"); + host.assertDiagnosticMessages(/*empty*/); + verifyOutputsPresent(fs, [allExpectedOutputs[0]]); + verifyOutputsAbsent(fs, allExpectedOutputs.slice(1)); + assert.equal(result, ExitStatus.Success); + }); + + it("cleaning project in not build order doesnt throw error", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + builder.build(); + const result = builder.clean("/src/logic2"); + host.assertDiagnosticMessages(/*empty*/); + verifyOutputsPresent(fs, allExpectedOutputs); + assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); }); }); @@ -318,10 +334,8 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tests"], {}); const result = builder.build("/src/logic"); host.assertDiagnosticMessages(/*empty*/); - assert.isFalse(fs.existsSync(allExpectedOutputs[0]), `Expect file ${allExpectedOutputs[0]} to not exist`); - for (const output of allExpectedOutputs.slice(1)) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsAbsent(fs, [allExpectedOutputs[0]]); + verifyOutputsPresent(fs, allExpectedOutputs.slice(1)); assert.equal(result, ExitStatus.Success); }); @@ -331,9 +345,7 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tests"], {}); const result = builder.build("/src/logic2"); host.assertDiagnosticMessages(/*empty*/); - for (const output of allExpectedOutputs) { - assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); - } + verifyOutputsAbsent(fs, allExpectedOutputs); assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); }); }); diff --git a/src/testRunner/unittests/tsbuild/transitiveReferences.ts b/src/testRunner/unittests/tsbuild/transitiveReferences.ts index 94f364ddec032..614796b2a8592 100644 --- a/src/testRunner/unittests/tsbuild/transitiveReferences.ts +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -32,9 +32,7 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tsconfig.c.json"], { listFiles: true }); builder.build(); host.assertDiagnosticMessages(...expectedDiagnostics); - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); assert.deepEqual(host.traces, expectedFileTraces); } diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index fc5559be02387..3514236bedaa6 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -224,7 +224,7 @@ namespace ts { updateCreateProgram(buildHost); buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); const builder = createSolutionBuilder(buildHost, projects, buildOptions); - return sys.exit(buildOptions.clean ? builder.cleanAllProjects() : builder.build()); + return sys.exit(buildOptions.clean ? builder.clean() : builder.build()); } } From 3da47963d51d85a1af3c482344d56c947b71c552 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 2 May 2019 15:17:53 -0700 Subject: [PATCH 23/41] Remove startWatching as explicit method from api --- src/compiler/tsbuild.ts | 44 +++++++++----------- src/testRunner/unittests/tsbuildWatchMode.ts | 1 - src/tsc/tsc.ts | 19 ++++----- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index c075e1f799c0a..1f7a90cabd650 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -234,6 +234,7 @@ namespace ts { export interface SolutionBuilderHostBase extends ProgramHost { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; + deleteFile(fileName: string): void; reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here reportSolutionBuilderStatus: DiagnosticReporter; @@ -247,7 +248,6 @@ namespace ts { } export interface SolutionBuilderHost extends SolutionBuilderHostBase { - deleteFile(fileName: string): void; reportErrorSummary?: ReportEmitErrorSummary; } @@ -267,11 +267,6 @@ namespace ts { /*@internal*/ buildNextInvalidatedProject(): void; } - export interface SolutionBuilderWithWatch { - build(project?: string, cancellationToken?: CancellationToken): ExitStatus; - /*@internal*/ startWatching(): void; - } - interface InvalidatedProject { project: ResolvedConfigFileName; projectPath: ResolvedConfigFilePath; @@ -294,6 +289,7 @@ namespace ts { const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase; host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined; host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop; + host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system); return host; @@ -301,7 +297,6 @@ namespace ts { export function createSolutionBuilderHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary) { const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderHost; - host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; host.reportErrorSummary = reportErrorSummary; return host; } @@ -325,7 +320,7 @@ namespace ts { return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions); } - export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilderWithWatch { + export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); } @@ -334,8 +329,8 @@ namespace ts { * 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): SolutionBuilderWithWatch; - function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder | SolutionBuilderWithWatch { + function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { const host = hostOrHostWithWatch as SolutionBuilderHost; const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; const currentDirectory = host.getCurrentDirectory(); @@ -395,21 +390,16 @@ namespace ts { let allProjectBuildPending = true; let needsSummary = true; - // let watchAllProjectsPending = watch; - - return watch ? - { - build, - startWatching - } : - { - build, - clean, - getBuildOrder, - getUpToDateStatusOfProject, - invalidateProject, - buildNextInvalidatedProject, - }; + let watchAllProjectsPending = watch; + + return { + build, + clean, + getBuildOrder, + getUpToDateStatusOfProject, + invalidateProject, + buildNextInvalidatedProject, + }; function toPath(fileName: string) { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); @@ -1469,6 +1459,10 @@ namespace ts { disableCache(); reportErrorSummary(); } + if (watchAllProjectsPending) { + watchAllProjectsPending = false; + startWatching(); + } break; } diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index ad00145db52d9..70fe602c8a2f8 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -25,7 +25,6 @@ namespace ts.tscWatch { const host = createSolutionBuilderWithWatchHost(system); const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true }); solutionBuilder.build(); - solutionBuilder.startWatching(); return solutionBuilder; } diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 3514236bedaa6..47814a1f68b88 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -207,25 +207,22 @@ namespace ts { reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--build")); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } - if (buildOptions.watch) { - reportWatchModeWithoutSysSupport(); - } if (buildOptions.watch) { + reportWatchModeWithoutSysSupport(); const buildHost = createSolutionBuilderWithWatchHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createWatchStatusReporter(buildOptions)); updateCreateProgram(buildHost); buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions); builder.build(); - return builder.startWatching(); - } - else { - const buildHost = createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); - updateCreateProgram(buildHost); - buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); - const builder = createSolutionBuilder(buildHost, projects, buildOptions); - return sys.exit(buildOptions.clean ? builder.clean() : builder.build()); + return; } + + const buildHost = createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); + updateCreateProgram(buildHost); + buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); + const builder = createSolutionBuilder(buildHost, projects, buildOptions); + return sys.exit(buildOptions.clean ? builder.clean() : builder.build()); } function createReportErrorSummary(options: CompilerOptions | BuildOptions): ReportEmitErrorSummary | undefined { From 5c18513e966dc9718358778cc53261aa2c845cc2 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 2 May 2019 15:34:13 -0700 Subject: [PATCH 24/41] Make SolutionBuilder as Public API --- src/compiler/commandLineParser.ts | 3 +- src/compiler/tsbuild.ts | 122 +++++++++--------- src/compiler/watch.ts | 2 - .../reference/api/tsserverlibrary.d.ts | 43 +++++- tests/baselines/reference/api/typescript.d.ts | 43 +++++- 5 files changed, 145 insertions(+), 68 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 104f3576e3a6d..b6ae404a2951a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1022,8 +1022,7 @@ namespace ts { } } - /* @internal */ - export interface OptionsBase { + interface OptionsBase { [option: string]: CompilerOptionsValue | undefined; } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 1f7a90cabd650..673f712689e69 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,58 +1,5 @@ -// Currently we do not want to expose API for build, we should work out the API, and then expose it just like we did for builder/watch /*@internal*/ namespace ts { - const minimumDate = new Date(-8640000000000000); - const maximumDate = new Date(8640000000000000); - - export interface BuildHost { - verbose(diag: DiagnosticMessage, ...args: string[]): void; - error(diag: DiagnosticMessage, ...args: string[]): void; - errorDiagnostic(diag: Diagnostic): void; - message(diag: DiagnosticMessage, ...args: string[]): void; - } - - export interface BuildOptions extends OptionsBase { - dry?: boolean; - force?: boolean; - verbose?: boolean; - - /*@internal*/ clean?: boolean; - /*@internal*/ watch?: boolean; - /*@internal*/ help?: boolean; - - preserveWatchOutput?: boolean; - listEmittedFiles?: boolean; - listFiles?: boolean; - pretty?: boolean; - incremental?: boolean; - - traceResolution?: boolean; - /* @internal */ diagnostics?: boolean; - /* @internal */ extendedDiagnostics?: boolean; - } - - enum BuildResultFlags { - None = 0, - - /** - * No errors of any kind occurred during build - */ - Success = 1 << 0, - /** - * None of the .d.ts files emitted by this build were - * different from the existing files on disk - */ - DeclarationOutputUnchanged = 1 << 1, - - ConfigFileErrors = 1 << 2, - SyntaxErrors = 1 << 3, - TypeErrors = 1 << 4, - DeclarationEmitErrors = 1 << 5, - EmitErrors = 1 << 6, - - AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors - } - export enum UpToDateStatusType { Unbuildable, UpToDate, @@ -194,6 +141,63 @@ namespace ts { } } + export function resolveConfigFileProjectName(project: string): ResolvedConfigFileName { + if (fileExtensionIs(project, Extension.Json)) { + return project as ResolvedConfigFileName; + } + + return combinePaths(project, "tsconfig.json") as ResolvedConfigFileName; + } +} + +namespace ts { + const minimumDate = new Date(-8640000000000000); + const maximumDate = new Date(8640000000000000); + + export interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + + /*@internal*/ clean?: boolean; + /*@internal*/ watch?: boolean; + /*@internal*/ help?: boolean; + + preserveWatchOutput?: boolean; + listEmittedFiles?: boolean; + listFiles?: boolean; + pretty?: boolean; + incremental?: boolean; + + traceResolution?: boolean; + /* @internal */ diagnostics?: boolean; + /* @internal */ extendedDiagnostics?: boolean; + + [option: string]: CompilerOptionsValue | undefined; + } + + enum BuildResultFlags { + None = 0, + + /** + * No errors of any kind occurred during build + */ + Success = 1 << 0, + /** + * None of the .d.ts files emitted by this build were + * different from the existing files on disk + */ + DeclarationOutputUnchanged = 1 << 1, + + ConfigFileErrors = 1 << 2, + SyntaxErrors = 1 << 3, + TypeErrors = 1 << 4, + DeclarationEmitErrors = 1 << 5, + EmitErrors = 1 << 6, + + AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors + } + type ResolvedConfigFilePath = ResolvedConfigFileName & Path; interface FileMap extends Map { get(key: U): T | undefined; @@ -231,6 +235,8 @@ namespace ts { return fileExtensionIs(fileName, Extension.Dts); } + export type ReportEmitErrorSummary = (errorCount: number) => void; + export interface SolutionBuilderHostBase extends ProgramHost { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; @@ -1527,15 +1533,7 @@ namespace ts { } } - export function resolveConfigFileProjectName(project: string): ResolvedConfigFileName { - if (fileExtensionIs(project, Extension.Json)) { - return project as ResolvedConfigFileName; - } - - return combinePaths(project, "tsconfig.json") as ResolvedConfigFileName; - } - - export function formatUpToDateStatus(configFileName: string, status: UpToDateStatus, relName: (fileName: string) => string, formatMessage: (message: DiagnosticMessage, ...args: string[]) => T) { + function formatUpToDateStatus(configFileName: string, status: UpToDateStatus, relName: (fileName: string) => string, formatMessage: (message: DiagnosticMessage, ...args: string[]) => T) { switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: return formatMessage(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index d6a856cfe9f53..50853534bb021 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -88,8 +88,6 @@ namespace ts { return result; } - export type ReportEmitErrorSummary = (errorCount: number) => void; - export function getErrorCountForSummary(diagnostics: ReadonlyArray) { return countWhere(diagnostics, diagnostic => diagnostic.category === DiagnosticCategory.Error); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index fb95102e79126..08a1309d792ca 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1905,7 +1905,8 @@ declare namespace ts { enum ExitStatus { Success = 0, DiagnosticsPresent_OutputsSkipped = 1, - DiagnosticsPresent_OutputsGenerated = 2 + DiagnosticsPresent_OutputsGenerated = 2, + InvalidProject_OutputsSkipped = 3 } interface EmitResult { emitSkipped: boolean; @@ -4555,6 +4556,46 @@ declare namespace ts { */ function createWatchProgram(host: WatchCompilerHostOfConfigFile): WatchOfConfigFile; } +declare namespace ts { + interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + preserveWatchOutput?: boolean; + listEmittedFiles?: boolean; + listFiles?: boolean; + pretty?: boolean; + incremental?: boolean; + traceResolution?: boolean; + [option: string]: CompilerOptionsValue | undefined; + } + type ReportEmitErrorSummary = (errorCount: number) => void; + interface SolutionBuilderHostBase extends ProgramHost { + getModifiedTime(fileName: string): Date | undefined; + setModifiedTime(fileName: string, date: Date): void; + deleteFile(fileName: string): void; + reportDiagnostic: DiagnosticReporter; + reportSolutionBuilderStatus: DiagnosticReporter; + afterProgramEmitAndDiagnostics?(program: T): void; + } + interface SolutionBuilderHost extends SolutionBuilderHostBase { + reportErrorSummary?: ReportEmitErrorSummary; + } + interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { + } + interface SolutionBuilder { + build(project?: string, cancellationToken?: CancellationToken): ExitStatus; + clean(project?: string): ExitStatus; + } + /** + * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic + */ + 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; +} declare namespace ts.server { type ActionSet = "action::set"; type ActionInvalidate = "action::invalidate"; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 170cb00b9e879..1c8129c20b02c 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1905,7 +1905,8 @@ declare namespace ts { enum ExitStatus { Success = 0, DiagnosticsPresent_OutputsSkipped = 1, - DiagnosticsPresent_OutputsGenerated = 2 + DiagnosticsPresent_OutputsGenerated = 2, + InvalidProject_OutputsSkipped = 3 } interface EmitResult { emitSkipped: boolean; @@ -4555,6 +4556,46 @@ declare namespace ts { */ function createWatchProgram(host: WatchCompilerHostOfConfigFile): WatchOfConfigFile; } +declare namespace ts { + interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + preserveWatchOutput?: boolean; + listEmittedFiles?: boolean; + listFiles?: boolean; + pretty?: boolean; + incremental?: boolean; + traceResolution?: boolean; + [option: string]: CompilerOptionsValue | undefined; + } + type ReportEmitErrorSummary = (errorCount: number) => void; + interface SolutionBuilderHostBase extends ProgramHost { + getModifiedTime(fileName: string): Date | undefined; + setModifiedTime(fileName: string, date: Date): void; + deleteFile(fileName: string): void; + reportDiagnostic: DiagnosticReporter; + reportSolutionBuilderStatus: DiagnosticReporter; + afterProgramEmitAndDiagnostics?(program: T): void; + } + interface SolutionBuilderHost extends SolutionBuilderHostBase { + reportErrorSummary?: ReportEmitErrorSummary; + } + interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { + } + interface SolutionBuilder { + build(project?: string, cancellationToken?: CancellationToken): ExitStatus; + clean(project?: string): ExitStatus; + } + /** + * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic + */ + 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; +} declare namespace ts.server { type ActionSet = "action::set"; type ActionInvalidate = "action::invalidate"; From 0a255249f6bf04c3c15e21fadb393c31fb9a3b15 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 2 May 2019 15:42:08 -0700 Subject: [PATCH 25/41] Enable apis to create incremental program --- src/compiler/watch.ts | 59 ++++++++++--------- .../reference/api/tsserverlibrary.d.ts | 11 ++++ tests/baselines/reference/api/typescript.d.ts | 11 ++++ 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 50853534bb021..4f974add57d80 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -380,6 +380,33 @@ namespace ts { return host; } + export interface IncrementalCompilationOptions { + rootNames: ReadonlyArray; + options: CompilerOptions; + configFileParsingDiagnostics?: ReadonlyArray; + projectReferences?: ReadonlyArray; + host?: CompilerHost; + reportDiagnostic?: DiagnosticReporter; + reportErrorSummary?: ReportEmitErrorSummary; + afterProgramEmitAndDiagnostics?(program: EmitAndSemanticDiagnosticsBuilderProgram): void; + system?: System; + } + export function performIncrementalCompilation(input: IncrementalCompilationOptions) { + const system = input.system || sys; + const host = input.host || (input.host = createIncrementalCompilerHost(input.options, system)); + const builderProgram = createIncrementalProgram(input); + const exitStatus = emitFilesAndReportErrors( + builderProgram, + input.reportDiagnostic || createDiagnosticReporter(system), + s => host.trace && host.trace(s), + input.reportErrorSummary || input.options.pretty ? errorCount => system.write(getErrorSummaryText(errorCount, system.newLine)) : undefined + ); + if (input.afterProgramEmitAndDiagnostics) input.afterProgramEmitAndDiagnostics(builderProgram); + return exitStatus; + } +} + +namespace ts { export function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined) { if (compilerOptions.out || compilerOptions.outFile) return undefined; const buildInfoPath = getOutputPathForBuildInfo(compilerOptions); @@ -400,7 +427,7 @@ namespace ts { return host; } - interface IncrementalProgramOptions { + export interface IncrementalProgramOptions { rootNames: ReadonlyArray; options: CompilerOptions; configFileParsingDiagnostics?: ReadonlyArray; @@ -408,7 +435,8 @@ namespace ts { host?: CompilerHost; createProgram?: CreateProgram; } - function createIncrementalProgram({ + + export function createIncrementalProgram({ rootNames, options, configFileParsingDiagnostics, projectReferences, host, createProgram }: IncrementalProgramOptions): T { host = host || createIncrementalCompilerHost(options); @@ -417,33 +445,6 @@ namespace ts { return createProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences); } - export interface IncrementalCompilationOptions { - rootNames: ReadonlyArray; - options: CompilerOptions; - configFileParsingDiagnostics?: ReadonlyArray; - projectReferences?: ReadonlyArray; - host?: CompilerHost; - reportDiagnostic?: DiagnosticReporter; - reportErrorSummary?: ReportEmitErrorSummary; - afterProgramEmitAndDiagnostics?(program: EmitAndSemanticDiagnosticsBuilderProgram): void; - system?: System; - } - export function performIncrementalCompilation(input: IncrementalCompilationOptions) { - const system = input.system || sys; - const host = input.host || (input.host = createIncrementalCompilerHost(input.options, system)); - const builderProgram = createIncrementalProgram(input); - const exitStatus = emitFilesAndReportErrors( - builderProgram, - input.reportDiagnostic || createDiagnosticReporter(system), - s => host.trace && host.trace(s), - input.reportErrorSummary || input.options.pretty ? errorCount => system.write(getErrorSummaryText(errorCount, system.newLine)) : undefined - ); - if (input.afterProgramEmitAndDiagnostics) input.afterProgramEmitAndDiagnostics(builderProgram); - return exitStatus; - } -} - -namespace ts { export type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void; /** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */ export type CreateProgram = (rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray | undefined) => T; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 08a1309d792ca..7b45703dcfdd9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4443,6 +4443,17 @@ declare namespace ts { function createAbstractBuilder(rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray): BuilderProgram; } declare namespace ts { + function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined): (EmitAndSemanticDiagnosticsBuilderProgram & SemanticDiagnosticsBuilderProgram) | undefined; + function createIncrementalCompilerHost(options: CompilerOptions, system?: System): CompilerHost; + interface IncrementalProgramOptions { + rootNames: ReadonlyArray; + options: CompilerOptions; + configFileParsingDiagnostics?: ReadonlyArray; + projectReferences?: ReadonlyArray; + host?: CompilerHost; + createProgram?: CreateProgram; + } + function createIncrementalProgram({ rootNames, options, configFileParsingDiagnostics, projectReferences, host, createProgram }: IncrementalProgramOptions): T; type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void; /** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */ type CreateProgram = (rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray | undefined) => T; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 1c8129c20b02c..acbe1b1ac0d49 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4443,6 +4443,17 @@ declare namespace ts { function createAbstractBuilder(rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray): BuilderProgram; } declare namespace ts { + function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined): (EmitAndSemanticDiagnosticsBuilderProgram & SemanticDiagnosticsBuilderProgram) | undefined; + function createIncrementalCompilerHost(options: CompilerOptions, system?: System): CompilerHost; + interface IncrementalProgramOptions { + rootNames: ReadonlyArray; + options: CompilerOptions; + configFileParsingDiagnostics?: ReadonlyArray; + projectReferences?: ReadonlyArray; + host?: CompilerHost; + createProgram?: CreateProgram; + } + function createIncrementalProgram({ rootNames, options, configFileParsingDiagnostics, projectReferences, host, createProgram }: IncrementalProgramOptions): T; type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void; /** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */ type CreateProgram = (rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray | undefined) => T; From 71b190af61ba2b074c54bb85d2a8c817a7df7bf9 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 2 May 2019 16:12:29 -0700 Subject: [PATCH 26/41] Create api for buildNextProject --- src/compiler/tsbuild.ts | 33 +++++++++++-- src/testRunner/unittests/tsbuild/sample.ts | 48 ++++++++++++++++--- .../reference/api/tsserverlibrary.d.ts | 5 ++ tests/baselines/reference/api/typescript.d.ts | 5 ++ 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 673f712689e69..9e62504128754 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -260,9 +260,15 @@ namespace ts { export interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } + export interface SolutionBuilderResult { + project: ResolvedConfigFileName; + result: T; + } + export interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; + buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined; // Currently used for testing but can be made public if needed: /*@internal*/ getBuildOrder(): ReadonlyArray; @@ -401,6 +407,7 @@ namespace ts { return { build, clean, + buildNextProject, getBuildOrder, getUpToDateStatusOfProject, invalidateProject, @@ -1437,10 +1444,7 @@ namespace ts { return resolvedProject ? createBuildOrder([resolvedProject]) : getBuildOrder(); } - function build(project?: string, cancellationToken?: CancellationToken): ExitStatus { - const buildOrder = getBuildOrderFor(project); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - + function setupInitialBuild(cancellationToken: CancellationToken | undefined) { // Set initial build if not already built if (allProjectBuildPending) { allProjectBuildPending = false; @@ -1455,6 +1459,27 @@ namespace ts { cancellationToken.throwIfCancellationRequested(); } } + } + + function buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined { + setupInitialBuild(cancellationToken); + const invalidatedProject = getNextInvalidatedProject(getBuildOrder()); + if (!invalidatedProject) return undefined; + + buildInvalidatedProject(invalidatedProject, cancellationToken); + return { + project: invalidatedProject.project, + result: diagnostics.has(invalidatedProject.projectPath) ? + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success + }; + } + + function build(project?: string, cancellationToken?: CancellationToken): ExitStatus { + const buildOrder = getBuildOrderFor(project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + setupInitialBuild(cancellationToken); let successfulProjects = 0; let errorProjects = 0; diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 65530e5970551..7f9f2260d6de2 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -2,9 +2,10 @@ namespace ts { describe("unittests:: tsbuild:: on 'sample1' project", () => { let projFs: vfs.FileSystem; const { time, tick } = getTime(); - const allExpectedOutputs = ["/src/tests/index.js", - "/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", - "/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; + const testsOutputs = ["/src/tests/index.js"]; + const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; + const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map"]; + const allExpectedOutputs = [...testsOutputs, ...logicOutputs, ...coreOutputs]; before(() => { projFs = loadProjectFromDisk("tests/projects/sample1", time); @@ -140,8 +141,8 @@ namespace ts { builder.build(); const result = builder.clean("/src/logic"); host.assertDiagnosticMessages(/*empty*/); - verifyOutputsPresent(fs, [allExpectedOutputs[0]]); - verifyOutputsAbsent(fs, allExpectedOutputs.slice(1)); + verifyOutputsPresent(fs, testsOutputs); + verifyOutputsAbsent(fs, [...logicOutputs, ...coreOutputs]); assert.equal(result, ExitStatus.Success); }); @@ -334,8 +335,8 @@ namespace ts { const builder = createSolutionBuilder(host, ["/src/tests"], {}); const result = builder.build("/src/logic"); host.assertDiagnosticMessages(/*empty*/); - verifyOutputsAbsent(fs, [allExpectedOutputs[0]]); - verifyOutputsPresent(fs, allExpectedOutputs.slice(1)); + verifyOutputsAbsent(fs, testsOutputs); + verifyOutputsPresent(fs, [...logicOutputs, ...coreOutputs]); assert.equal(result, ExitStatus.Success); }); @@ -348,6 +349,39 @@ namespace ts { verifyOutputsAbsent(fs, allExpectedOutputs); assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); }); + + it("building using buildNextProject", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + verifyBuildNextResult({ + project: "/src/core/tsconfig.json" as ResolvedConfigFileName, + result: ExitStatus.Success + }, coreOutputs, [...logicOutputs, ...testsOutputs]); + + verifyBuildNextResult({ + project: "/src/logic/tsconfig.json" as ResolvedConfigFileName, + result: ExitStatus.Success + }, [...coreOutputs, ...logicOutputs], testsOutputs); + + verifyBuildNextResult({ + project: "/src/tests/tsconfig.json" as ResolvedConfigFileName, + result: ExitStatus.Success + }, allExpectedOutputs, emptyArray); + + verifyBuildNextResult(/*expected*/ undefined, allExpectedOutputs, emptyArray); + + function verifyBuildNextResult( + expected: SolutionBuilderResult | undefined, + presentOutputs: readonly string[], + absentOutputs: readonly string[] + ) { + const result = builder.buildNextProject(); + assert.deepEqual(result, expected); + verifyOutputsPresent(fs, presentOutputs); + verifyOutputsAbsent(fs, absentOutputs); + } + }); }); describe("downstream-blocked compilations", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 7b45703dcfdd9..f13d0ab3f5121 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4594,9 +4594,14 @@ declare namespace ts { } interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } + interface SolutionBuilderResult { + project: ResolvedConfigFileName; + result: T; + } interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; + buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined; } /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index acbe1b1ac0d49..e5e5046794aaa 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4594,9 +4594,14 @@ declare namespace ts { } interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } + interface SolutionBuilderResult { + project: ResolvedConfigFileName; + result: T; + } interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; + buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined; } /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic From f01743385796a2d8a07d1dc5d3feaad526166d7e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 3 May 2019 13:32:47 -0700 Subject: [PATCH 27/41] Move everything into state so we can pass it around --- src/compiler/tsbuild.ts | 2251 ++++++++++-------- src/testRunner/unittests/tsbuild/sample.ts | 2 +- src/testRunner/unittests/tsbuildWatchMode.ts | 8 +- 3 files changed, 1207 insertions(+), 1054 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 9e62504128754..22bc8ba823896 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -198,7 +198,8 @@ namespace ts { AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors } - type ResolvedConfigFilePath = ResolvedConfigFileName & Path; + /*@internal*/ + export type ResolvedConfigFilePath = ResolvedConfigFileName & Path; interface FileMap extends Map { get(key: U): T | undefined; has(key: U): boolean; @@ -212,6 +213,9 @@ namespace ts { clear(): void; } type ConfigFileMap = FileMap; + function createConfigFileMap(): ConfigFileMap { + return createMap() as ConfigFileMap; + } function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFilePath, createT: () => T): T { const existingValue = configFileMap.get(resolved); @@ -275,15 +279,16 @@ namespace ts { // Testing only /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; - /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; + /*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildNextInvalidatedProject(): void; } interface InvalidatedProject { - project: ResolvedConfigFileName; - projectPath: ResolvedConfigFilePath; - reloadLevel: ConfigFileProgramReloadLevel; - projectIndex: number; + readonly project: ResolvedConfigFileName; + readonly projectPath: ResolvedConfigFilePath; + readonly reloadLevel: ConfigFileProgramReloadLevel; + readonly projectIndex: number; + readonly buildOrder: readonly ResolvedConfigFileName[]; } /** @@ -336,1279 +341,1418 @@ namespace ts { return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); } - /** - * 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, defaultOptions: BuildOptions): SolutionBuilder { + type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; + interface SolutionBuilderStateCache { + originalReadFile: CompilerHost["readFile"]; + originalFileExists: CompilerHost["fileExists"]; + originalDirectoryExists: CompilerHost["directoryExists"]; + originalCreateDirectory: CompilerHost["createDirectory"]; + originalWriteFile: CompilerHost["writeFile"] | undefined; + originalReadFileWithCache: CompilerHost["readFile"]; + originalGetSourceFile: CompilerHost["getSourceFile"]; + } + + interface SolutionBuilderState { + readonly host: SolutionBuilderHost; + readonly hostWithWatch: SolutionBuilderWithWatchHost; + readonly currentDirectory: string; + readonly getCanonicalFileName: GetCanonicalFileName; + readonly parseConfigFileHost: ParseConfigFileHost; + readonly writeFileName: ((s: string) => void) | undefined; + + // State of solution + readonly options: BuildOptions; + readonly baseCompilerOptions: CompilerOptions; + readonly rootNames: ReadonlyArray; + + readonly resolvedConfigFilePaths: Map; + readonly configFileCache: ConfigFileMap; + /** Map from config file name to up-to-date status */ + readonly projectStatus: ConfigFileMap; + readonly buildInfoChecked: ConfigFileMap; + readonly extendedConfigCache: Map; + + readonly builderPrograms: ConfigFileMap; + readonly diagnostics: ConfigFileMap; + readonly projectPendingBuild: ConfigFileMap; + readonly projectErrorsReported: ConfigFileMap; + + readonly compilerHost: CompilerHost; + readonly moduleResolutionCache: ModuleResolutionCache | undefined; + + // Mutable state + buildOrder: readonly ResolvedConfigFileName[] | undefined; + readFileWithCache: (f: string) => string | undefined; + projectCompilerOptions: CompilerOptions; + cache: SolutionBuilderStateCache | undefined; + allProjectBuildPending: boolean; + needsSummary: boolean; + watchAllProjectsPending: boolean; + + // Watch state + readonly watch: boolean; + readonly allWatchedWildcardDirectories: ConfigFileMap>; + readonly allWatchedInputFiles: ConfigFileMap>; + readonly allWatchedConfigFiles: ConfigFileMap; + + timerToBuildInvalidatedProject: any; + reportFileChangeDetected: boolean; + watchFile: WatchFile; + watchFilePath: WatchFilePath; + watchDirectory: WatchDirectory; + writeLog: (s: string) => void; + } + + function createSolutionBuilderState(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, options: BuildOptions): SolutionBuilderState { const host = hostOrHostWithWatch as SolutionBuilderHost; const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - const parseConfigFileHost = parseConfigHostFromCompilerHostLike(host); // State of the solution - const options = defaultOptions; const baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); - const resolvedConfigFilePaths = createMap(); - type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; - const configFileCache = createMap() as ConfigFileMap; - /** Map from config file name to up-to-date status */ - const projectStatus = createMap() as ConfigFileMap; - let buildOrder: readonly ResolvedConfigFileName[] | undefined; - const writeFileName = host.trace ? (s: string) => host.trace!(s) : undefined; - let readFileWithCache = (f: string) => host.readFile(f); - let projectCompilerOptions = baseCompilerOptions; - const compilerHost = createCompilerHostFromProgramHost(host, () => projectCompilerOptions); + const compilerHost = createCompilerHostFromProgramHost(host, () => state.projectCompilerOptions); setGetSourceFileAsHashVersioned(compilerHost, host); - compilerHost.getParsedCommandLine = fileName => parseConfigFile(fileName as ResolvedConfigFileName, toResolvedConfigFilePath(fileName as ResolvedConfigFileName)); - + compilerHost.getParsedCommandLine = fileName => parseConfigFile(state, fileName as ResolvedConfigFileName, toResolvedConfigFilePath(state, fileName as ResolvedConfigFileName)); compilerHost.resolveModuleNames = maybeBind(host, host.resolveModuleNames); compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives); const moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined; if (!compilerHost.resolveModuleNames) { - const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference).resolvedModule!; + const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, state.projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference).resolvedModule!; compilerHost.resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference) => loadWithLocalCache(Debug.assertEachDefined(moduleNames), containingFile, redirectedReference, loader); } - let cacheState: { - originalReadFile: CompilerHost["readFile"]; - originalFileExists: CompilerHost["fileExists"]; - originalDirectoryExists: CompilerHost["directoryExists"]; - originalCreateDirectory: CompilerHost["createDirectory"]; - originalWriteFile: CompilerHost["writeFile"] | undefined; - originalReadFileWithCache: CompilerHost["readFile"]; - originalGetSourceFile: CompilerHost["getSourceFile"]; - } | undefined; - - const buildInfoChecked = createMap() as ConfigFileMap; - const extendedConfigCache = createMap(); - // Watch state - const builderPrograms = createMap() as ConfigFileMap; - const diagnostics = createMap() as ConfigFileMap>; - const projectPendingBuild = createMap() as ConfigFileMap; - const projectErrorsReported = createMap() as ConfigFileMap; - let timerToBuildInvalidatedProject: any; - let reportFileChangeDetected = false; const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); - // Watches for the solution - const allWatchedWildcardDirectories = createMap() as ConfigFileMap>; - const allWatchedInputFiles = createMap() as ConfigFileMap>; - const allWatchedConfigFiles = createMap() as ConfigFileMap; + const state: SolutionBuilderState = { + host, + hostWithWatch, + currentDirectory, + getCanonicalFileName, + parseConfigFileHost: parseConfigHostFromCompilerHostLike(host), + writeFileName: host.trace ? (s: string) => host.trace!(s) : undefined, + + // State of solution + options, + baseCompilerOptions, + rootNames, + + resolvedConfigFilePaths: createMap(), + configFileCache: createConfigFileMap(), + projectStatus: createConfigFileMap(), + buildInfoChecked: createConfigFileMap(), + extendedConfigCache: createMap(), + + builderPrograms: createConfigFileMap(), + diagnostics: createConfigFileMap(), + projectPendingBuild: createConfigFileMap(), + projectErrorsReported: createConfigFileMap(), + + compilerHost, + moduleResolutionCache, + + // Mutable state + buildOrder: undefined, + readFileWithCache: f => host.readFile(f), + projectCompilerOptions: baseCompilerOptions, + cache: undefined, + allProjectBuildPending: true, + needsSummary: true, + watchAllProjectsPending: watch, + + // Watch state + watch, + allWatchedWildcardDirectories: createConfigFileMap(), + allWatchedInputFiles: createConfigFileMap(), + allWatchedConfigFiles: createConfigFileMap(), + + timerToBuildInvalidatedProject: undefined, + reportFileChangeDetected: false, + watchFile, + watchFilePath, + watchDirectory, + writeLog, + }; - let allProjectBuildPending = true; - let needsSummary = true; - let watchAllProjectsPending = watch; + return state; + } - return { - build, - clean, - buildNextProject, - getBuildOrder, - getUpToDateStatusOfProject, - invalidateProject, - buildNextInvalidatedProject, - }; + function toPath(state: SolutionBuilderState, fileName: string) { + return ts.toPath(fileName, state.currentDirectory, state.getCanonicalFileName); + } - function toPath(fileName: string) { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); - } + function toResolvedConfigFilePath(state: SolutionBuilderState, fileName: ResolvedConfigFileName): ResolvedConfigFilePath { + const { resolvedConfigFilePaths } = state; + const path = resolvedConfigFilePaths.get(fileName); + if (path !== undefined) return path; - function toResolvedConfigFilePath(fileName: ResolvedConfigFileName): ResolvedConfigFilePath { - const path = resolvedConfigFilePaths.get(fileName); - if (path !== undefined) return path; + const resolvedPath = toPath(state, fileName) as ResolvedConfigFilePath; + resolvedConfigFilePaths.set(fileName, resolvedPath); + return resolvedPath; + } - const resolvedPath = toPath(fileName) as ResolvedConfigFilePath; - resolvedConfigFilePaths.set(fileName, resolvedPath); - return resolvedPath; - } + function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { + return !!(entry as ParsedCommandLine).options; + } - function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { - return !!(entry as ParsedCommandLine).options; + function parseConfigFile(state: SolutionBuilderState, configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { + const { configFileCache } = state; + const value = configFileCache.get(configFilePath); + if (value) { + return isParsedCommandLine(value) ? value : undefined; } - function parseConfigFile(configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { - const value = configFileCache.get(configFilePath); - if (value) { - return isParsedCommandLine(value) ? value : undefined; + let diagnostic: Diagnostic | undefined; + const { parseConfigFileHost, baseCompilerOptions, extendedConfigCache } = state; + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; + const parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; + configFileCache.set(configFilePath, parsed || diagnostic!); + return parsed; + } + + function resolveProjectName(state: SolutionBuilderState, name: string): ResolvedConfigFileName { + return resolveConfigFileProjectName(resolvePath(state.currentDirectory, name)); + } + + function createBuildOrder(state: SolutionBuilderState, roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { + const temporaryMarks = createMap() as ConfigFileMap; + const permanentMarks = createMap() as ConfigFileMap; + const circularityReportStack: string[] = []; + let buildOrder: ResolvedConfigFileName[] | undefined; + for (const root of roots) { + visit(root); + } + + return buildOrder || emptyArray; + + function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) { + const projPath = toResolvedConfigFilePath(state, configFileName); + // Already visited + if (permanentMarks.has(projPath)) return; + // Circular + if (temporaryMarks.has(projPath)) { + if (!inCircularContext) { + // TODO:: Do we report this as error? + reportStatus(state, Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); + } + return; + } + + temporaryMarks.set(projPath, true); + circularityReportStack.push(configFileName); + const parsed = parseConfigFile(state, configFileName, projPath); + if (parsed && parsed.projectReferences) { + for (const ref of parsed.projectReferences) { + const resolvedRefPath = resolveProjectName(state, ref.path); + visit(resolvedRefPath, inCircularContext || ref.circular); + } } - let diagnostic: Diagnostic | undefined; - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - const parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; - configFileCache.set(configFilePath, parsed || diagnostic!); - return parsed; + circularityReportStack.pop(); + permanentMarks.set(projPath, true); + (buildOrder || (buildOrder = [])).push(configFileName); } + } - function reportStatus(message: DiagnosticMessage, ...args: string[]) { - host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); + function getBuildOrder(state: SolutionBuilderState) { + return state.buildOrder || + (state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)))); + } + + function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined) { + const resolvedProject = project && resolveProjectName(state, project); + if (resolvedProject) { + const projectPath = toResolvedConfigFilePath(state, resolvedProject); + const projectIndex = findIndex( + getBuildOrder(state), + configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath + ); + if (projectIndex === -1) return undefined; } + return resolvedProject ? createBuildOrder(state, [resolvedProject]) : getBuildOrder(state); + } - function reportWatchStatus(message: DiagnosticMessage, ...args: (string | number | undefined)[]) { - if (hostWithWatch.onWatchStatusChange) { - hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), host.getNewLine(), baseCompilerOptions); - } + function enableCache(state: SolutionBuilderState) { + if (state.cache) { + disableCache(state); + } + + const { compilerHost, host } = state; + + const originalReadFileWithCache = state.readFileWithCache; + const originalGetSourceFile = compilerHost.getSourceFile; + + const { + originalReadFile, originalFileExists, originalDirectoryExists, + originalCreateDirectory, originalWriteFile, + getSourceFileWithCache, readFileWithCache + } = changeCompilerHostLikeToUseCache( + host, + fileName => toPath(state, fileName), + (...args) => originalGetSourceFile.call(compilerHost, ...args) + ); + state.readFileWithCache = readFileWithCache; + compilerHost.getSourceFile = getSourceFileWithCache!; + + state.cache = { + originalReadFile, + originalFileExists, + originalDirectoryExists, + originalCreateDirectory, + originalWriteFile, + originalReadFileWithCache, + originalGetSourceFile, + }; + } + + function disableCache(state: SolutionBuilderState) { + if (!state.cache) return; + + const { cache, host, compilerHost, extendedConfigCache, moduleResolutionCache } = state; + + host.readFile = cache.originalReadFile; + host.fileExists = cache.originalFileExists; + host.directoryExists = cache.originalDirectoryExists; + host.createDirectory = cache.originalCreateDirectory; + host.writeFile = cache.originalWriteFile; + compilerHost.getSourceFile = cache.originalGetSourceFile; + state.readFileWithCache = cache.originalReadFileWithCache; + extendedConfigCache.clear(); + if (moduleResolutionCache) { + moduleResolutionCache.directoryToModuleNameMap.clear(); + moduleResolutionCache.moduleNameToDirectoryMap.clear(); } + state.cache = undefined; + } + + function clearProjectStatus(state: SolutionBuilderState, resolved: ResolvedConfigFilePath) { + state.projectStatus.delete(resolved); + state.diagnostics.delete(resolved); + } - function startWatching() { - for (const resolved of getBuildOrder()) { - const resolvedPath = toResolvedConfigFilePath(resolved); - // Watch this file - watchConfigFile(resolved, resolvedPath); + function addProjToQueue({ projectPendingBuild }: SolutionBuilderState, proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + const value = projectPendingBuild.get(proj); + if (value === undefined) { + projectPendingBuild.set(proj, reloadLevel); + } + else if (value < reloadLevel) { + projectPendingBuild.set(proj, reloadLevel); + } + } - const cfg = parseConfigFile(resolved, resolvedPath); - if (cfg) { - // Update watchers for wildcard directories - watchWildCardDirectories(resolved, resolvedPath, cfg); + function setupInitialBuild(state: SolutionBuilderState, cancellationToken: CancellationToken | undefined) { + // Set initial build if not already built + if (!state.allProjectBuildPending) return; + state.allProjectBuildPending = false; + if (state.options.watch) { reportWatchStatus(state, Diagnostics.Starting_compilation_in_watch_mode); } + enableCache(state); + const buildOrder = getBuildOrder(state); + reportBuildQueue(state, buildOrder); + buildOrder.forEach(configFileName => + state.projectPendingBuild.set( + toResolvedConfigFilePath(state, configFileName), + ConfigFileProgramReloadLevel.None + ) + ); + + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); + } + } - // Watch input files - watchInputFiles(resolved, resolvedPath, cfg); + function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { + return state.projectPendingBuild.size ? + forEach(buildOrder, (project, projectIndex) => { + const projectPath = toResolvedConfigFilePath(state, project); + const reloadLevel = state.projectPendingBuild.get(projectPath); + if (reloadLevel !== undefined) { + return { project, projectPath, reloadLevel, projectIndex, buildOrder }; } - } + }) : + undefined; + } + function listEmittedFile({ writeFileName }: SolutionBuilderState, proj: ParsedCommandLine, file: string) { + if (writeFileName && proj.options.listEmittedFiles) { + writeFileName(`TSFILE: ${file}`); } + } - function watchConfigFile(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { - if (watch && !allWatchedConfigFiles.has(resolvedPath)) { - allWatchedConfigFiles.set(resolvedPath, watchFile( - hostWithWatch, - resolved, - () => { - invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.Full); - }, - PollingInterval.High, - WatchType.ConfigFile, - resolved - )); - } + function getOldProgram({ options, builderPrograms, readFileWithCache }: SolutionBuilderState, proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (options.force) return undefined; + const value = builderPrograms.get(proj); + if (value) return value; + return readBuilderProgram(parsed.options, readFileWithCache) as any as T; + } + + function afterProgramCreate({ host, watch, builderPrograms }: SolutionBuilderState, proj: ResolvedConfigFilePath, program: T) { + if (host.afterProgramEmitAndDiagnostics) { + host.afterProgramEmitAndDiagnostics(program); } + if (watch) { + program.releaseProgram(); + builderPrograms.set(proj, program); + } + } - function watchWildCardDirectories(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!watch) return; - updateWatchingWildcardDirectories( - getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolvedPath), - createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), - (dir, flags) => { - return watchDirectory( - hostWithWatch, - dir, - fileOrDirectory => { - const fileOrDirectoryPath = toPath(fileOrDirectory); - if (fileOrDirectoryPath !== toPath(dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, parsed.options)) { - writeLog(`Project: ${resolved} Detected file add/remove of non supported extension: ${fileOrDirectory}`); - return; - } + function buildErrors( + state: SolutionBuilderState, + resolvedPath: ResolvedConfigFilePath, + program: T | undefined, + diagnostics: ReadonlyArray, + errorFlags: BuildResultFlags, + errorType: string + ) { + reportAndStoreErrors(state, resolvedPath, diagnostics); + // List files if any other build error using program (emit errors already report files) + if (program && state.writeFileName) listFiles(program, state.writeFileName); + state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); + if (program) afterProgramCreate(state, resolvedPath, program); + state.projectCompilerOptions = state.baseCompilerOptions; + return errorFlags; + } - if (isOutputFile(fileOrDirectory, parsed)) { - writeLog(`${fileOrDirectory} is output file`); - return; - } + function buildSingleProject(state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, proj); + return BuildResultFlags.Success; + } - invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.Partial); - }, - flags, - WatchType.WildcardDirectory, - resolved - ); - } - ); + if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, proj); + + const { host, projectStatus, diagnostics, compilerHost, moduleResolutionCache, } = state; + const configFile = parseConfigFile(state, proj, resolvedPath); + if (!configFile) { + // Failed to read the config file + reportParseConfigFileDiagnostic(state, resolvedPath); + projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); + return BuildResultFlags.ConfigFileErrors; } - function watchInputFiles(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!watch) return; - mutateMap( - getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolvedPath), - arrayToMap(parsed.fileNames, toPath), - { - createNewValue: (path, input) => watchFilePath( - hostWithWatch, - input, - () => invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.None), - PollingInterval.Low, - path as Path, - WatchType.SourceFile, - resolved - ), - onDeleteValue: closeFileWatcher, - } + if (configFile.fileNames.length === 0) { + reportAndStoreErrors(state, resolvedPath, configFile.errors); + // Nothing to build - must be a solution file, basically + return BuildResultFlags.None; + } + + state.projectCompilerOptions = configFile.options; + // Update module resolution cache if needed + if (moduleResolutionCache) { + const projPath = toPath(state, proj); + if (moduleResolutionCache.directoryToModuleNameMap.redirectsMap.size === 0) { + // The own map will be for projectCompilerOptions + Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size === 0); + moduleResolutionCache.directoryToModuleNameMap.redirectsMap.set(projPath, moduleResolutionCache.directoryToModuleNameMap.ownMap); + moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.set(projPath, moduleResolutionCache.moduleNameToDirectoryMap.ownMap); + } + else { + // Set correct own map + Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size > 0); + + const ref: ResolvedProjectReference = { + sourceFile: configFile.options.configFile!, + commandLine: configFile + }; + moduleResolutionCache.directoryToModuleNameMap.setOwnMap(moduleResolutionCache.directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); + moduleResolutionCache.moduleNameToDirectoryMap.setOwnMap(moduleResolutionCache.moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); + } + moduleResolutionCache.directoryToModuleNameMap.setOwnOptions(configFile.options); + moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(configFile.options); + } + + // Create program + const program = host.createProgram( + configFile.fileNames, + configFile.options, + compilerHost, + getOldProgram(state, resolvedPath, configFile), + configFile.errors, + configFile.projectReferences + ); + + // Don't emit anything in the presence of syntactic errors or options diagnostics + const syntaxDiagnostics = [ + ...program.getConfigFileParsingDiagnostics(), + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; + if (syntaxDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + program, + syntaxDiagnostics, + BuildResultFlags.SyntaxErrors, + "Syntactic" ); } - function isOutputFile(fileName: string, configFile: ParsedCommandLine) { - if (configFile.options.noEmit) return false; + // Same as above but now for semantic diagnostics + const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); + if (semanticDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + program, + semanticDiagnostics, + BuildResultFlags.TypeErrors, + "Semantic" + ); + } - // ts or tsx files are not output - if (!fileExtensionIs(fileName, Extension.Dts) && - (fileExtensionIs(fileName, Extension.Ts) || fileExtensionIs(fileName, Extension.Tsx))) { - return false; - } + // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly + program.backupState(); + let declDiagnostics: Diagnostic[] | undefined; + const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); + const outputFiles: OutputFile[] = []; + emitFilesAndReportErrors( + program, + reportDeclarationDiagnostics, + /*writeFileName*/ undefined, + /*reportSummary*/ undefined, + (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), + cancellationToken + ); + // Don't emit .d.ts if there are decl file errors + if (declDiagnostics) { + program.restoreState(); + return buildErrors( + state, + resolvedPath, + program, + declDiagnostics, + BuildResultFlags.DeclarationEmitErrors, + "Declaration file" + ); + } - // If options have --outFile or --out, check if its that - const out = configFile.options.outFile || configFile.options.out; - if (out && (isSameFile(fileName, out) || isSameFile(fileName, removeFileExtension(out) + Extension.Dts))) { - return true; + // Actual Emit + let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; + let newestDeclarationFileContentChangedTime = minimumDate; + let anyDtsChanged = false; + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + let priorChangeTime: Date | undefined; + if (!anyDtsChanged && isDeclarationFile(name)) { + // Check for unchanged .d.ts files + if (host.fileExists(name) && state.readFileWithCache(name) === text) { + priorChangeTime = host.getModifiedTime(name); + } + else { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + anyDtsChanged = true; + } } - // If declarationDir is specified, return if its a file in that directory - if (configFile.options.declarationDir && containsPath(configFile.options.declarationDir, fileName, currentDirectory, !host.useCaseSensitiveFileNames())) { - return true; + emittedOutputs.set(toPath(state, name), name); + writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); } + }); - // If --outDir, check if file is in that directory - if (configFile.options.outDir && containsPath(configFile.options.outDir, fileName, currentDirectory, !host.useCaseSensitiveFileNames())) { - return true; - } + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + program, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); + } - return !forEach(configFile.fileNames, inputFile => isSameFile(fileName, inputFile)); + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, configFile, name)); + listFiles(program, state.writeFileName); } - function isSameFile(file1: string, file2: string) { - return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + // Update time stamps for rest of the outputs + newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + diagnostics.delete(resolvedPath); + projectStatus.set(resolvedPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, + oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) + }); + afterProgramCreate(state, resolvedPath, program); + state.projectCompilerOptions = state.baseCompilerOptions; + return resultFlags; + } + + function updateBundle(state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); + return BuildResultFlags.Success; } - function invalidateProjectAndScheduleBuilds(resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { - reportFileChangeDetected = true; - invalidateResolvedProject(resolvedPath, reloadLevel); - scheduleBuildInvalidatedProject(); + if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, proj); + + // Update js, and source map + const { projectStatus, diagnostics, compilerHost } = state; + const config = Debug.assertDefined(parseConfigFile(state, proj, resolvedPath)); + state.projectCompilerOptions = config.options; + const outputFiles = emitUsingBuildInfo( + config, + compilerHost, + ref => { + const refName = resolveProjectName(state, ref.path); + return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName)); + }); + if (isString(outputFiles)) { + reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(state, outputFiles)); + return buildSingleProject(state, proj, resolvedPath, cancellationToken); + } + + // Actual Emit + Debug.assert(!!outputFiles.length); + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + emittedOutputs.set(toPath(state, name), name); + writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + }); + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + /*program*/ undefined, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); } - function getUpToDateStatusOfProject(project: string): UpToDateStatus { - const configFileName = resolveProjectName(project); - const configFilePath = toResolvedConfigFilePath(configFileName); - return getUpToDateStatus(parseConfigFile(configFileName, configFilePath), configFilePath); + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); } - function getBuildOrder() { - return buildOrder || (buildOrder = createBuildOrder(resolveProjectNames(rootNames))); + // Update timestamps for dts + const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, minimumDate, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + diagnostics.delete(resolvedPath); + projectStatus.set(resolvedPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + oldestOutputFileName: outputFiles[0].name + }); + state.projectCompilerOptions = state.baseCompilerOptions; + return BuildResultFlags.DeclarationOutputUnchanged; + } + + function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { + // Check tsconfig time + const tsconfigTime = state.host.getModifiedTime(configFile) || missingFileModifiedTime; + if (oldestOutputFileTime < tsconfigTime) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: configFile + }; } + } - function getUpToDateStatus(project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { - if (project === undefined) { - return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; + const { host } = state; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!host.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; } - const prior = projectStatus.get(resolvedPath); - if (prior !== undefined) { - return prior; + const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; } + } - const actual = getUpToDateStatusWorker(project, resolvedPath); - projectStatus.set(resolvedPath, actual); - return actual; + // Container if no files are specified in the project + if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) { + return { + type: UpToDateStatusType.ContainerOnly + }; } - function getUpToDateStatusWorker(project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { - let newestInputFileName: string = undefined!; - let newestInputFileTime = minimumDate; - // Get timestamps of input files - for (const inputFile of project.fileNames) { - if (!host.fileExists(inputFile)) { - return { - type: UpToDateStatusType.Unbuildable, - reason: `${inputFile} does not exist` - }; - } + // Collect the expected outputs of this project + const outputs = getAllProjectOutputs(project, !host.useCaseSensitiveFileNames()); + + // Now see if all outputs are newer than the newest input + let oldestOutputFileName = "(none)"; + let oldestOutputFileTime = maximumDate; + let newestOutputFileName = "(none)"; + let newestOutputFileTime = minimumDate; + let missingOutputFileName: string | undefined; + let newestDeclarationFileContentChangedTime = minimumDate; + let isOutOfDateWithInputs = false; + for (const output of outputs) { + // Output is missing; can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (!host.fileExists(output)) { + missingOutputFileName = output; + break; + } - const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; - if (inputTime > newestInputFileTime) { - newestInputFileName = inputFile; - newestInputFileTime = inputTime; - } + const outputTime = host.getModifiedTime(output) || missingFileModifiedTime; + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; } - // Container if no files are specified in the project - if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) { - return { - type: UpToDateStatusType.ContainerOnly - }; + // If an output is older than the newest input, we can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (outputTime < newestInputFileTime) { + isOutOfDateWithInputs = true; + break; } - // Collect the expected outputs of this project - const outputs = getAllProjectOutputs(project, !host.useCaseSensitiveFileNames()); - - // Now see if all outputs are newer than the newest input - let oldestOutputFileName = "(none)"; - let oldestOutputFileTime = maximumDate; - let newestOutputFileName = "(none)"; - let newestOutputFileTime = minimumDate; - let missingOutputFileName: string | undefined; - let newestDeclarationFileContentChangedTime = minimumDate; - let isOutOfDateWithInputs = false; - for (const output of outputs) { - // Output is missing; can stop checking - // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status - if (!host.fileExists(output)) { - missingOutputFileName = output; - break; - } + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; + } - const outputTime = host.getModifiedTime(output) || missingFileModifiedTime; - if (outputTime < oldestOutputFileTime) { - oldestOutputFileTime = outputTime; - oldestOutputFileName = output; - } + // Keep track of when the most recent time a .d.ts file was changed. + // In addition to file timestamps, we also keep track of when a .d.ts file + // had its file touched but not had its contents changed - this allows us + // to skip a downstream typecheck + if (isDeclarationFile(output)) { + const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); + } + } + + let pseudoUpToDate = false; + let usesPrepend = false; + let upstreamChangedProject: string | undefined; + if (project.projectReferences) { + state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.ComputingUpstream }); + for (const ref of project.projectReferences) { + usesPrepend = usesPrepend || !!(ref.prepend); + const resolvedRef = resolveProjectReferencePath(ref); + const resolvedRefPath = toResolvedConfigFilePath(state, resolvedRef); + const refStatus = getUpToDateStatus(state, parseConfigFile(state, resolvedRef, resolvedRefPath), resolvedRefPath); - // If an output is older than the newest input, we can stop checking - // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status - if (outputTime < newestInputFileTime) { - isOutOfDateWithInputs = true; - break; + // Its a circular reference ignore the status of this project + if (refStatus.type === UpToDateStatusType.ComputingUpstream) { + continue; } - if (outputTime > newestOutputFileTime) { - newestOutputFileTime = outputTime; - newestOutputFileName = output; + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path + }; } - // Keep track of when the most recent time a .d.ts file was changed. - // In addition to file timestamps, we also keep track of when a .d.ts file - // had its file touched but not had its contents changed - this allows us - // to skip a downstream typecheck - if (isDeclarationFile(output)) { - const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) + if (refStatus.type !== UpToDateStatusType.UpToDate) { + return { + type: UpToDateStatusType.UpstreamOutOfDate, + upstreamProjectName: ref.path + }; } - } - let pseudoUpToDate = false; - let usesPrepend = false; - let upstreamChangedProject: string | undefined; - if (project.projectReferences) { - projectStatus.set(resolvedPath, { type: UpToDateStatusType.ComputingUpstream }); - for (const ref of project.projectReferences) { - usesPrepend = usesPrepend || !!(ref.prepend); - const resolvedRef = resolveProjectReferencePath(ref); - const resolvedRefPath = toResolvedConfigFilePath(resolvedRef); - const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef, resolvedRefPath), resolvedRefPath); - - // Its a circular reference ignore the status of this project - if (refStatus.type === UpToDateStatusType.ComputingUpstream) { + // Check oldest output file name only if there is no missing output file name + if (!missingOutputFileName) { + // If the upstream project's newest file is older than our oldest output, we + // can't be out of date because of it + if (refStatus.newestInputFileTime && refStatus.newestInputFileTime <= oldestOutputFileTime) { continue; } - // An upstream project is blocked - if (refStatus.type === UpToDateStatusType.Unbuildable) { - return { - type: UpToDateStatusType.UpstreamBlocked, - upstreamProjectName: ref.path - }; - } - - // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) - if (refStatus.type !== UpToDateStatusType.UpToDate) { - return { - type: UpToDateStatusType.UpstreamOutOfDate, - upstreamProjectName: ref.path - }; - } - - // Check oldest output file name only if there is no missing output file name - if (!missingOutputFileName) { - // If the upstream project's newest file is older than our oldest output, we - // can't be out of date because of it - if (refStatus.newestInputFileTime && refStatus.newestInputFileTime <= oldestOutputFileTime) { - continue; - } - - // If the upstream project has only change .d.ts files, and we've built - // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild - if (refStatus.newestDeclarationFileContentChangedTime && refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) { - pseudoUpToDate = true; - upstreamChangedProject = ref.path; - continue; - } - - // We have an output older than an upstream output - we are out of date - Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); - return { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: ref.path - }; + // If the upstream project has only change .d.ts files, and we've built + // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild + if (refStatus.newestDeclarationFileContentChangedTime && refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) { + pseudoUpToDate = true; + upstreamChangedProject = ref.path; + continue; } - } - } - - if (missingOutputFileName !== undefined) { - return { - type: UpToDateStatusType.OutputMissing, - missingOutputFileName - }; - } - if (isOutOfDateWithInputs) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: newestInputFileName - }; - } - else { - // Check tsconfig time - const configStatus = checkConfigFileUpToDateStatus(project.options.configFilePath!, oldestOutputFileTime, oldestOutputFileName); - if (configStatus) return configStatus; - - // Check extended config time - const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(configFile, oldestOutputFileTime, oldestOutputFileName)); - if (extendedConfigStatus) return extendedConfigStatus; - } - - if (!buildInfoChecked.has(resolvedPath)) { - buildInfoChecked.set(resolvedPath, true); - const buildInfoPath = getOutputPathForBuildInfo(project.options); - if (buildInfoPath) { - const value = readFileWithCache(buildInfoPath); - const buildInfo = value && getBuildInfo(value); - if (buildInfo && buildInfo.version !== version) { - return { - type: UpToDateStatusType.TsVersionOutputOfDate, - version: buildInfo.version - }; - } + // We have an output older than an upstream output - we are out of date + Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); + return { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: ref.path + }; } } + } - if (usesPrepend && pseudoUpToDate) { - return { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: upstreamChangedProject! - }; - } - - // Up to date + if (missingOutputFileName !== undefined) { return { - type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - newestInputFileTime, - newestOutputFileTime, - newestInputFileName, - newestOutputFileName, - oldestOutputFileName + type: UpToDateStatusType.OutputMissing, + missingOutputFileName }; } - function checkConfigFileUpToDateStatus(configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { - // Check tsconfig time - const tsconfigTime = host.getModifiedTime(configFile) || missingFileModifiedTime; - if (oldestOutputFileTime < tsconfigTime) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: configFile - }; - } + if (isOutOfDateWithInputs) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: newestInputFileName + }; } + else { + // Check tsconfig time + const configStatus = checkConfigFileUpToDateStatus(state, project.options.configFilePath!, oldestOutputFileTime, oldestOutputFileName); + if (configStatus) return configStatus; - function invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel) { - invalidateResolvedProject(toResolvedConfigFilePath(resolveProjectName(configFileName)), reloadLevel || ConfigFileProgramReloadLevel.None); + // Check extended config time + const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName)); + if (extendedConfigStatus) return extendedConfigStatus; } - function invalidateResolvedProject(resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - configFileCache.delete(resolved); - buildOrder = undefined; + if (!state.buildInfoChecked.has(resolvedPath)) { + state.buildInfoChecked.set(resolvedPath, true); + const buildInfoPath = getOutputPathForBuildInfo(project.options); + if (buildInfoPath) { + const value = state.readFileWithCache(buildInfoPath); + const buildInfo = value && getBuildInfo(value); + if (buildInfo && buildInfo.version !== version) { + return { + type: UpToDateStatusType.TsVersionOutputOfDate, + version: buildInfo.version + }; + } } - needsSummary = true; - clearProjectStatus(resolved); - addProjToQueue(resolved, reloadLevel); - enableCache(); } - function clearProjectStatus(resolved: ResolvedConfigFilePath) { - projectStatus.delete(resolved); - diagnostics.delete(resolved); + if (usesPrepend && pseudoUpToDate) { + return { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: upstreamChangedProject! + }; } - /** - * return true if new addition - */ - function addProjToQueue(proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { - const value = projectPendingBuild.get(proj); - if (value === undefined) { - projectPendingBuild.set(proj, reloadLevel); - } - else if (value < reloadLevel) { - projectPendingBuild.set(proj, reloadLevel); - } - } + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName + }; + } - function getNextInvalidatedProject(buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { - return hasPendingInvalidatedProjects() ? - forEach(buildOrder, (project, projectIndex) => { - const projectPath = toResolvedConfigFilePath(project); - const reloadLevel = projectPendingBuild.get(projectPath); - if (reloadLevel !== undefined) { - return { project, projectPath, reloadLevel, projectIndex }; - } - }) : - undefined; + function getUpToDateStatus(state: SolutionBuilderState, project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; } - function hasPendingInvalidatedProjects() { - return !!projectPendingBuild.size; + const prior = state.projectStatus.get(resolvedPath); + if (prior !== undefined) { + return prior; } - function scheduleBuildInvalidatedProject() { - if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) { - return; - } - if (timerToBuildInvalidatedProject) { - hostWithWatch.clearTimeout(timerToBuildInvalidatedProject); - } - timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250); - } + const actual = getUpToDateStatusWorker(state, project, resolvedPath); + state.projectStatus.set(resolvedPath, actual); + return actual; + } - function buildNextInvalidatedProject() { - timerToBuildInvalidatedProject = undefined; - if (reportFileChangeDetected) { - reportFileChangeDetected = false; - projectErrorsReported.clear(); - reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); - } - const invalidatedProject = getNextInvalidatedProject(getBuildOrder()); - if (invalidatedProject) { - buildInvalidatedProject(invalidatedProject); - if (hasPendingInvalidatedProjects()) { - if (watch && !timerToBuildInvalidatedProject) { - scheduleBuildInvalidatedProject(); - } - } - else { - disableCache(); - reportErrorSummary(); + function updateOutputTimestampsWorker(state: SolutionBuilderState, proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { + const { host } = state; + const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames()); + if (!skipOutputs || outputs.length !== skipOutputs.size) { + let reportVerbose = !!state.options.verbose; + const now = host.now ? host.now() : new Date(); + for (const file of outputs) { + if (skipOutputs && skipOutputs.has(toPath(state, file))) { + continue; } - } - } - function reportErrorSummary() { - if (watch || host.reportErrorSummary) { - needsSummary = false; - // Report errors from the other projects - getBuildOrder().forEach(project => { - const projectPath = toResolvedConfigFilePath(project); - if (!projectErrorsReported.has(projectPath)) { - reportErrors(diagnostics.get(projectPath) || emptyArray); - } - }); - let totalErrors = 0; - diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); - if (watch) { - reportWatchStatus(getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); + if (reportVerbose) { + reportVerbose = false; + reportStatus(state, verboseMessage, proj.options.configFilePath!); } - else { - host.reportErrorSummary!(totalErrors); + + if (isDeclarationFile(file)) { + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); } + + host.setModifiedTime(file, now); + listEmittedFile(state, proj, file); } } - function buildInvalidatedProject({ project, projectPath, reloadLevel, projectIndex }: InvalidatedProject, cancellationToken?: CancellationToken) { - const config = parseConfigFile(project, projectPath); - if (!config) { - reportParseConfigFileDiagnostic(projectPath); - projectPendingBuild.delete(projectPath); - return; - } + return priorNewestUpdateTime; + } - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(project, projectPath); - watchWildCardDirectories(project, projectPath, config); - watchInputFiles(project, projectPath, config); - } - else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { - // Update file names - const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, parseConfigFileHost); - updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); - config.fileNames = result.fileNames; - watchInputFiles(project, projectPath, config); - } + function updateOutputTimestamps(state: SolutionBuilderState, proj: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath) { + if (state.options.dry) { + return reportStatus(state, Diagnostics.A_non_dry_build_would_update_timestamps_for_output_of_project_0, proj.options.configFilePath!); + } + const priorNewestUpdateTime = updateOutputTimestampsWorker(state, proj, minimumDate, Diagnostics.Updating_output_timestamps_of_project_0); + state.projectStatus.set(resolvedPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime: priorNewestUpdateTime, + oldestOutputFileName: getFirstProjectOutput(proj, !state.host.useCaseSensitiveFileNames()) + }); + } + + function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { + if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; + return config.fileNames.length === 0 || + !!config.errors.length || + !isIncrementalCompilation(config.options); + } - const status = getUpToDateStatus(config, projectPath); - verboseReportProjectStatus(project, status); - if (status.type === UpToDateStatusType.UpToDate && !options.force) { - reportAndStoreErrors(projectPath, config.errors); + function buildInvalidatedProject(state: SolutionBuilderState, { project, projectPath, reloadLevel, projectIndex, buildOrder }: InvalidatedProject, cancellationToken?: CancellationToken) { + const { options, projectPendingBuild } = state; + const config = parseConfigFile(state, project, projectPath); + if (!config) { + reportParseConfigFileDiagnostic(state, projectPath); + projectPendingBuild.delete(projectPath); + return; + } + + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + watchConfigFile(state, project, projectPath); + watchWildCardDirectories(state, project, projectPath, config); + watchInputFiles(state, project, projectPath, config); + } + else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { + // Update file names + const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost); + updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); + config.fileNames = result.fileNames; + watchInputFiles(state, project, projectPath, config); + } + + const status = getUpToDateStatus(state, config, projectPath); + verboseReportProjectStatus(state, project, status); + if (!options.force) { + if (status.type === UpToDateStatusType.UpToDate) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); // Up to date, skip if (options.dry) { // In a dry build, inform the user of this fact - reportStatus(Diagnostics.Project_0_is_up_to_date, project); + reportStatus(state, Diagnostics.Project_0_is_up_to_date, project); } - projectPendingBuild.delete(projectPath); - return; - } - - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !options.force) { - reportAndStoreErrors(projectPath, config.errors); - // Fake that files have been built by updating output file stamps - updateOutputTimestamps(config, projectPath); - projectPendingBuild.delete(projectPath); return; } - if (status.type === UpToDateStatusType.UpstreamBlocked) { - reportAndStoreErrors(projectPath, config.errors); - if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { + reportAndStoreErrors(state, projectPath, config.errors); projectPendingBuild.delete(projectPath); + // Fake that files have been built by updating output file stamps + updateOutputTimestamps(state, config, projectPath); return; } + } - if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(projectPath, config.errors); - // Do nothing - projectPendingBuild.delete(projectPath); - return; - } + if (status.type === UpToDateStatusType.UpstreamBlocked) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + if (options.verbose) reportStatus(state, Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); + return; + } - const buildResult = needsBuild(status, config) ? - buildSingleProject(project, projectPath, cancellationToken) : // Actual build - updateBundle(project, projectPath, cancellationToken); // Fake that files have been built by manipulating prepend and existing output + if (status.type === UpToDateStatusType.ContainerOnly) { + reportAndStoreErrors(state, projectPath, config.errors); projectPendingBuild.delete(projectPath); - // Only composite projects can be referenced by other projects - if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { - queueReferencingProjects(project, projectPath, projectIndex, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); - } + // Do nothing + return; } - function queueReferencingProjects(project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, declarationOutputChanged: boolean) { - // Always use build order to queue projects - const buildOrder = getBuildOrder(); - for (let index = projectIndex + 1; index < buildOrder.length; index++) { - const nextProject = buildOrder[index]; - const nextProjectPath = toResolvedConfigFilePath(nextProject); - if (projectPendingBuild.has(nextProjectPath)) continue; - - const nextProjectConfig = parseConfigFile(nextProject, nextProjectPath); - if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; - for (const ref of nextProjectConfig.projectReferences) { - const resolvedRefPath = resolveProjectName(ref.path); - if (toResolvedConfigFilePath(resolvedRefPath) !== projectPath) continue; - // If the project is referenced with prepend, always build downstream projects, - // If declaration output is changed, build the project - // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps - const status = projectStatus.get(nextProjectPath); - if (status) { - switch (status.type) { - case UpToDateStatusType.UpToDate: - if (!declarationOutputChanged) { - if (ref.prepend) { - projectStatus.set(nextProjectPath, { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: status.oldestOutputFileName, - newerProjectName: project - }); - } - else { - status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; - } - break; - } + const buildResult = needsBuild(state, status, config) ? + buildSingleProject(state, project, projectPath, cancellationToken) : // Actual build + updateBundle(state, project, projectPath, cancellationToken); // Fake that files have been built by manipulating prepend and existing output + projectPendingBuild.delete(projectPath); + // Only composite projects can be referenced by other projects + if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { + queueReferencingProjects(state, project, projectPath, projectIndex, buildOrder, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); + } + } - // falls through - case UpToDateStatusType.UpToDateWithUpstreamTypes: - case UpToDateStatusType.OutOfDateWithPrepend: - if (declarationOutputChanged) { - projectStatus.set(nextProjectPath, { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, + function queueReferencingProjects( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + buildOrder: readonly ResolvedConfigFileName[], + declarationOutputChanged: boolean + ) { + // Always use build order to queue projects + for (let index = projectIndex + 1; index < buildOrder.length; index++) { + const nextProject = buildOrder[index]; + const nextProjectPath = toResolvedConfigFilePath(state, nextProject); + if (state.projectPendingBuild.has(nextProjectPath)) continue; + + const nextProjectConfig = parseConfigFile(state, nextProject, nextProjectPath); + if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; + for (const ref of nextProjectConfig.projectReferences) { + const resolvedRefPath = resolveProjectName(state, ref.path); + if (toResolvedConfigFilePath(state, resolvedRefPath) !== projectPath) continue; + // If the project is referenced with prepend, always build downstream projects, + // If declaration output is changed, build the project + // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps + const status = state.projectStatus.get(nextProjectPath); + if (status) { + switch (status.type) { + case UpToDateStatusType.UpToDate: + if (!declarationOutputChanged) { + if (ref.prepend) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: status.oldestOutputFileName, newerProjectName: project }); } - break; - - case UpToDateStatusType.UpstreamBlocked: - if (toResolvedConfigFilePath(resolveProjectName(status.upstreamProjectName)) === projectPath) { - clearProjectStatus(nextProjectPath); + else { + status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; } break; - } - } - addProjToQueue(nextProjectPath, ConfigFileProgramReloadLevel.None); - break; - } - } - } - - function createBuildOrder(roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { - const temporaryMarks = createMap() as ConfigFileMap; - const permanentMarks = createMap() as ConfigFileMap; - const circularityReportStack: string[] = []; - let buildOrder: ResolvedConfigFileName[] | undefined; - for (const root of roots) { - visit(root); - } + } - return buildOrder || emptyArray; - - function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) { - const projPath = toResolvedConfigFilePath(configFileName); - // Already visited - if (permanentMarks.has(projPath)) return; - // Circular - if (temporaryMarks.has(projPath)) { - if (!inCircularContext) { - // TODO:: Do we report this as error? - reportStatus(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); - } - return; - } + // falls through + case UpToDateStatusType.UpToDateWithUpstreamTypes: + case UpToDateStatusType.OutOfDateWithPrepend: + if (declarationOutputChanged) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, + newerProjectName: project + }); + } + break; - temporaryMarks.set(projPath, true); - circularityReportStack.push(configFileName); - const parsed = parseConfigFile(configFileName, projPath); - if (parsed && parsed.projectReferences) { - for (const ref of parsed.projectReferences) { - const resolvedRefPath = resolveProjectName(ref.path); - visit(resolvedRefPath, inCircularContext || ref.circular); + case UpToDateStatusType.UpstreamBlocked: + if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) { + clearProjectStatus(state, nextProjectPath); + } + break; } } - - circularityReportStack.pop(); - permanentMarks.set(projPath, true); - (buildOrder || (buildOrder = [])).push(configFileName); + addProjToQueue(state, nextProjectPath, ConfigFileProgramReloadLevel.None); + break; } } + } - function buildSingleProject(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj); - return BuildResultFlags.Success; - } - - if (options.verbose) reportStatus(Diagnostics.Building_project_0, proj); + function buildNextProject(state: SolutionBuilderState, cancellationToken?: CancellationToken): SolutionBuilderResult | undefined { + setupInitialBuild(state, cancellationToken); + const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); + if (!invalidatedProject) return undefined; - let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; + buildInvalidatedProject(state, invalidatedProject, cancellationToken); + return { + project: invalidatedProject.project, + result: state.diagnostics.has(invalidatedProject.projectPath) ? + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success + }; + } - const configFile = parseConfigFile(proj, resolvedPath); - if (!configFile) { - // Failed to read the config file - resultFlags |= BuildResultFlags.ConfigFileErrors; - reportParseConfigFileDiagnostic(resolvedPath); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); - return resultFlags; - } - if (configFile.fileNames.length === 0) { - reportAndStoreErrors(resolvedPath, configFile.errors); - // Nothing to build - must be a solution file, basically - return BuildResultFlags.None; - } + function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken): ExitStatus { + const buildOrder = getBuildOrderFor(state, project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - // TODO: handle resolve module name to cache result in project reference redirect - projectCompilerOptions = configFile.options; - // Update module resolution cache if needed - if (moduleResolutionCache) { - const projPath = toPath(proj); - if (moduleResolutionCache.directoryToModuleNameMap.redirectsMap.size === 0) { - // The own map will be for projectCompilerOptions - Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size === 0); - moduleResolutionCache.directoryToModuleNameMap.redirectsMap.set(projPath, moduleResolutionCache.directoryToModuleNameMap.ownMap); - moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.set(projPath, moduleResolutionCache.moduleNameToDirectoryMap.ownMap); - } - else { - // Set correct own map - Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size > 0); + setupInitialBuild(state, cancellationToken); - const ref: ResolvedProjectReference = { - sourceFile: projectCompilerOptions.configFile!, - commandLine: configFile - }; - moduleResolutionCache.directoryToModuleNameMap.setOwnMap(moduleResolutionCache.directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); - moduleResolutionCache.moduleNameToDirectoryMap.setOwnMap(moduleResolutionCache.moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); - } - moduleResolutionCache.directoryToModuleNameMap.setOwnOptions(projectCompilerOptions); - moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(projectCompilerOptions); + let successfulProjects = 0; + let errorProjects = 0; + while (true) { + const invalidatedProject = getNextInvalidatedProject(state, buildOrder); + if (!invalidatedProject) break; + buildInvalidatedProject(state, invalidatedProject, cancellationToken); + if (state.diagnostics.has(invalidatedProject.projectPath)) { + errorProjects++; } - - const program = host.createProgram( - configFile.fileNames, - configFile.options, - compilerHost, - getOldProgram(resolvedPath, configFile), - configFile.errors, - configFile.projectReferences - ); - - // Don't emit anything in the presence of syntactic errors or options diagnostics - const syntaxDiagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; - if (syntaxDiagnostics.length) { - return buildErrors(syntaxDiagnostics, BuildResultFlags.SyntaxErrors, "Syntactic"); + else { + successfulProjects++; } + } - // Same as above but now for semantic diagnostics - const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); - if (semanticDiagnostics.length) { - return buildErrors(semanticDiagnostics, BuildResultFlags.TypeErrors, "Semantic"); - } + disableCache(state); + reportErrorSummary(state); + startWatching(state); - // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly - program.backupState(); - let newestDeclarationFileContentChangedTime = minimumDate; - let anyDtsChanged = false; - let declDiagnostics: Diagnostic[] | undefined; - const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); - const outputFiles: OutputFile[] = []; - emitFilesAndReportErrors( - program, - reportDeclarationDiagnostics, - /*writeFileName*/ undefined, - /*reportSummary*/ undefined, - (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), - cancellationToken - ); - // Don't emit .d.ts if there are decl file errors - if (declDiagnostics) { - program.restoreState(); - return buildErrors(declDiagnostics, BuildResultFlags.DeclarationEmitErrors, "Declaration file"); - } + return errorProjects ? + successfulProjects ? + ExitStatus.DiagnosticsPresent_OutputsGenerated : + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; + } - // Actual Emit - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - let priorChangeTime: Date | undefined; - if (!anyDtsChanged && isDeclarationFile(name)) { - // Check for unchanged .d.ts files - if (host.fileExists(name) && readFileWithCache(name) === text) { - priorChangeTime = host.getModifiedTime(name); + function clean(state: SolutionBuilderState, project?: string) { + const buildOrder = getBuildOrderFor(state, project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + const { options, host } = state; + const filesToDelete = options.dry ? [] as string[] : undefined; + for (const proj of buildOrder) { + const resolvedPath = toResolvedConfigFilePath(state, proj); + const parsed = parseConfigFile(state, proj, resolvedPath); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + reportParseConfigFileDiagnostic(state, resolvedPath); + continue; + } + const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); + for (const output of outputs) { + if (host.fileExists(output)) { + if (filesToDelete) { + filesToDelete.push(output); } else { - resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - anyDtsChanged = true; + host.deleteFile(output); + invalidateProject(state, resolvedPath, ConfigFileProgramReloadLevel.None); } } - - emittedOutputs.set(toPath(name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - if (priorChangeTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - } - }); - - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - return buildErrors(emitDiagnostics, BuildResultFlags.EmitErrors, "Emit"); } + } - if (writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(configFile, name)); - listFiles(program, writeFileName); - } + if (filesToDelete) { + reportStatus(state, Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); + } - // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + return ExitStatus.Success; + } - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) - }; - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, status); - afterProgramCreate(resolvedPath, program); - projectCompilerOptions = baseCompilerOptions; - return resultFlags; - - function buildErrors(diagnostics: ReadonlyArray, errorFlags: BuildResultFlags, errorType: string) { - resultFlags |= errorFlags; - reportAndStoreErrors(resolvedPath, diagnostics); - // List files if any other build error using program (emit errors already report files) - if (writeFileName) listFiles(program, writeFileName); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); - afterProgramCreate(resolvedPath, program); - projectCompilerOptions = baseCompilerOptions; - return resultFlags; - } + function invalidateProject(state: SolutionBuilderState, resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + state.configFileCache.delete(resolved); + state.buildOrder = undefined; } + state.needsSummary = true; + clearProjectStatus(state, resolved); + addProjToQueue(state, resolved, reloadLevel); + enableCache(state); + } - function listEmittedFile(proj: ParsedCommandLine, file: string) { - if (writeFileName && proj.options.listEmittedFiles) { - writeFileName(`TSFILE: ${file}`); - } - } + function invalidateProjectAndScheduleBuilds(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + state.reportFileChangeDetected = true; + invalidateProject(state, resolvedPath, reloadLevel); + scheduleBuildInvalidatedProject(state); + } - function afterProgramCreate(proj: ResolvedConfigFilePath, program: T) { - if (host.afterProgramEmitAndDiagnostics) { - host.afterProgramEmitAndDiagnostics(program); - } - if (watch) { - program.releaseProgram(); - builderPrograms.set(proj, program); - } + function scheduleBuildInvalidatedProject(state: SolutionBuilderState) { + const { hostWithWatch } = state; + if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) { + return; } - - function getOldProgram(proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (options.force) return undefined; - const value = builderPrograms.get(proj); - if (value) return value; - return readBuilderProgram(parsed.options, readFileWithCache) as any as T; + if (state.timerToBuildInvalidatedProject) { + hostWithWatch.clearTimeout(state.timerToBuildInvalidatedProject); } + state.timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250, state); + } - function updateBundle(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); - return BuildResultFlags.Success; - } - - if (options.verbose) reportStatus(Diagnostics.Updating_output_of_project_0, proj); - - // Update js, and source map - const config = Debug.assertDefined(parseConfigFile(proj, resolvedPath)); - projectCompilerOptions = config.options; - const outputFiles = emitUsingBuildInfo( - config, - compilerHost, - ref => { - const refName = resolveProjectName(ref.path); - return parseConfigFile(refName, toResolvedConfigFilePath(refName)); - }); - if (isString(outputFiles)) { - reportStatus(Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(outputFiles)); - return buildSingleProject(proj, resolvedPath, cancellationToken); + function buildNextInvalidatedProject(state: SolutionBuilderState) { + state.timerToBuildInvalidatedProject = undefined; + if (state.reportFileChangeDetected) { + state.reportFileChangeDetected = false; + state.projectErrorsReported.clear(); + reportWatchStatus(state, Diagnostics.File_change_detected_Starting_incremental_compilation); + } + const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); + if (invalidatedProject) { + buildInvalidatedProject(state, invalidatedProject); + if (state.projectPendingBuild.size) { + // Schedule next project for build + if (state.watch && !state.timerToBuildInvalidatedProject) { + scheduleBuildInvalidatedProject(state); + } } - - // Actual Emit - Debug.assert(!!outputFiles.length); - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - emittedOutputs.set(toPath(name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - }); - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - reportAndStoreErrors(resolvedPath, emitDiagnostics); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Emit errors" }); - projectCompilerOptions = baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged | BuildResultFlags.EmitErrors; + else { + disableCache(state); + reportErrorSummary(state); } + } + } - if (writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(config, name)); - } + function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { + if (!state.watch || state.allWatchedConfigFiles.has(resolvedPath)) return; + state.allWatchedConfigFiles.set(resolvedPath, state.watchFile( + state.hostWithWatch, + resolved, + () => { + invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full); + }, + PollingInterval.High, + WatchType.ConfigFile, + resolved + )); + } - // Update timestamps for dts - const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(config, minimumDate, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + function isSameFile(state: SolutionBuilderState, file1: string, file2: string) { + return comparePaths(file1, file2, state.currentDirectory, !state.host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + } - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles[0].name - }; + function isOutputFile(state: SolutionBuilderState, fileName: string, configFile: ParsedCommandLine) { + if (configFile.options.noEmit) return false; - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, status); - projectCompilerOptions = baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged; + // ts or tsx files are not output + if (!fileExtensionIs(fileName, Extension.Dts) && + (fileExtensionIs(fileName, Extension.Ts) || fileExtensionIs(fileName, Extension.Tsx))) { + return false; } - function updateOutputTimestamps(proj: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath) { - if (options.dry) { - return reportStatus(Diagnostics.A_non_dry_build_would_update_timestamps_for_output_of_project_0, proj.options.configFilePath!); - } - const priorNewestUpdateTime = updateOutputTimestampsWorker(proj, minimumDate, Diagnostics.Updating_output_timestamps_of_project_0); - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: priorNewestUpdateTime, - oldestOutputFileName: getFirstProjectOutput(proj, !host.useCaseSensitiveFileNames()) - }; - projectStatus.set(resolvedPath, status); + // If options have --outFile or --out, check if its that + const out = configFile.options.outFile || configFile.options.out; + if (out && (isSameFile(state, fileName, out) || isSameFile(state, fileName, removeFileExtension(out) + Extension.Dts))) { + return true; } - function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { - const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames()); - if (!skipOutputs || outputs.length !== skipOutputs.size) { - if (options.verbose) { - reportStatus(verboseMessage, proj.options.configFilePath!); - } - const now = host.now ? host.now() : new Date(); - for (const file of outputs) { - if (skipOutputs && skipOutputs.has(toPath(file))) { - continue; - } - - if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); - } - - host.setModifiedTime(file, now); - listEmittedFile(proj, file); - } - } + // If declarationDir is specified, return if its a file in that directory + if (configFile.options.declarationDir && containsPath(configFile.options.declarationDir, fileName, state.currentDirectory, !state.host.useCaseSensitiveFileNames())) { + return true; + } - return priorNewestUpdateTime; + // If --outDir, check if file is in that directory + if (configFile.options.outDir && containsPath(configFile.options.outDir, fileName, state.currentDirectory, !state.host.useCaseSensitiveFileNames())) { + return true; } - function clean(project?: string) { - const buildOrder = getBuildOrderFor(project); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + return !forEach(configFile.fileNames, inputFile => isSameFile(state, fileName, inputFile)); + } - const filesToDelete = options.dry ? [] as string[] : undefined; - for (const proj of buildOrder) { - const resolvedPath = toResolvedConfigFilePath(proj); - const parsed = parseConfigFile(proj, resolvedPath); - if (parsed === undefined) { - // File has gone missing; fine to ignore here - reportParseConfigFileDiagnostic(resolvedPath); - continue; - } - const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); - for (const output of outputs) { - if (host.fileExists(output)) { - if (filesToDelete) { - filesToDelete.push(output); - } - else { - host.deleteFile(output); - invalidateResolvedProject(resolvedPath, ConfigFileProgramReloadLevel.None); - } + function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (!state.watch) return; + updateWatchingWildcardDirectories( + getOrCreateValueMapFromConfigFileMap(state.allWatchedWildcardDirectories, resolvedPath), + createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), + (dir, flags) => state.watchDirectory( + state.hostWithWatch, + dir, + fileOrDirectory => { + const fileOrDirectoryPath = toPath(state, fileOrDirectory); + if (fileOrDirectoryPath !== toPath(state, dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, parsed.options)) { + state.writeLog(`Project: ${resolved} Detected file add/remove of non supported extension: ${fileOrDirectory}`); + return; } - } - } - - if (filesToDelete) { - reportStatus(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); - } - - return ExitStatus.Success; - } - function resolveProjectName(name: string): ResolvedConfigFileName { - return resolveConfigFileProjectName(resolvePath(host.getCurrentDirectory(), name)); - } + if (isOutputFile(state, fileOrDirectory, parsed)) { + state.writeLog(`${fileOrDirectory} is output file`); + return; + } - function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] { - return configFileNames.map(resolveProjectName); - } + invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial); + }, + flags, + WatchType.WildcardDirectory, + resolved + ) + ); + } - function enableCache() { - if (cacheState) { - disableCache(); + function watchInputFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (!state.watch) return; + mutateMap( + getOrCreateValueMapFromConfigFileMap(state.allWatchedInputFiles, resolvedPath), + arrayToMap(parsed.fileNames, fileName => toPath(state, fileName)), + { + createNewValue: (path, input) => state.watchFilePath( + state.hostWithWatch, + input, + () => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None), + PollingInterval.Low, + path as Path, + WatchType.SourceFile, + resolved + ), + onDeleteValue: closeFileWatcher, } + ); + } - const originalReadFileWithCache = readFileWithCache; - const originalGetSourceFile = compilerHost.getSourceFile; - - const { originalReadFile, originalFileExists, originalDirectoryExists, - originalCreateDirectory, originalWriteFile, getSourceFileWithCache, - readFileWithCache: newReadFileWithCache - } = changeCompilerHostLikeToUseCache(host, toPath, (...args) => originalGetSourceFile.call(compilerHost, ...args)); - readFileWithCache = newReadFileWithCache; - compilerHost.getSourceFile = getSourceFileWithCache!; - - cacheState = { - originalReadFile, - originalFileExists, - originalDirectoryExists, - originalCreateDirectory, - originalWriteFile, - originalReadFileWithCache, - originalGetSourceFile, - }; - } + function startWatching(state: SolutionBuilderState) { + if (!state.watchAllProjectsPending) return; + state.watchAllProjectsPending = false; + for (const resolved of getBuildOrder(state)) { + const resolvedPath = toResolvedConfigFilePath(state, resolved); + // Watch this file + watchConfigFile(state, resolved, resolvedPath); - function disableCache() { - if (!cacheState) return; - - host.readFile = cacheState.originalReadFile; - host.fileExists = cacheState.originalFileExists; - host.directoryExists = cacheState.originalDirectoryExists; - host.createDirectory = cacheState.originalCreateDirectory; - host.writeFile = cacheState.originalWriteFile; - compilerHost.getSourceFile = cacheState.originalGetSourceFile; - readFileWithCache = cacheState.originalReadFileWithCache; - extendedConfigCache.clear(); - if (moduleResolutionCache) { - moduleResolutionCache.directoryToModuleNameMap.clear(); - moduleResolutionCache.moduleNameToDirectoryMap.clear(); - } - cacheState = undefined; - } + const cfg = parseConfigFile(state, resolved, resolvedPath); + if (cfg) { + // Update watchers for wildcard directories + watchWildCardDirectories(state, resolved, resolvedPath, cfg); - function getBuildOrderFor(project: string | undefined) { - const resolvedProject = project && resolveProjectName(project); - if (resolvedProject) { - const projectPath = toResolvedConfigFilePath(resolvedProject); - const projectIndex = findIndex( - getBuildOrder(), - configFileName => toResolvedConfigFilePath(configFileName) === projectPath - ); - if (projectIndex === -1) return undefined; - } - return resolvedProject ? createBuildOrder([resolvedProject]) : getBuildOrder(); - } - - function setupInitialBuild(cancellationToken: CancellationToken | undefined) { - // Set initial build if not already built - if (allProjectBuildPending) { - allProjectBuildPending = false; - if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } - enableCache(); - const buildOrder = getBuildOrder(); - reportBuildQueue(buildOrder); - buildOrder.forEach(configFileName => - projectPendingBuild.set(toResolvedConfigFilePath(configFileName), ConfigFileProgramReloadLevel.None)); - - if (cancellationToken) { - cancellationToken.throwIfCancellationRequested(); - } + // Watch input files + watchInputFiles(state, resolved, resolvedPath, cfg); } } + } - function buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined { - setupInitialBuild(cancellationToken); - const invalidatedProject = getNextInvalidatedProject(getBuildOrder()); - if (!invalidatedProject) return undefined; - - buildInvalidatedProject(invalidatedProject, cancellationToken); - return { - project: invalidatedProject.project, - result: diagnostics.has(invalidatedProject.projectPath) ? - ExitStatus.DiagnosticsPresent_OutputsSkipped : - ExitStatus.Success - }; - } - - function build(project?: string, cancellationToken?: CancellationToken): ExitStatus { - const buildOrder = getBuildOrderFor(project); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - - setupInitialBuild(cancellationToken); - - let successfulProjects = 0; - let errorProjects = 0; - while (true) { - const invalidatedProject = getNextInvalidatedProject(buildOrder); - if (!invalidatedProject) { - if (needsSummary) { - disableCache(); - reportErrorSummary(); - } - if (watchAllProjectsPending) { - watchAllProjectsPending = false; - startWatching(); - } - break; - } + /** + * 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 { + 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), + getBuildOrder: () => getBuildOrder(state), + getUpToDateStatusOfProject: project => { + const configFileName = resolveProjectName(state, project); + const configFilePath = toResolvedConfigFilePath(state, configFileName); + return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath); + }, + invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None), + buildNextInvalidatedProject: () => buildNextInvalidatedProject(state), + }; + } - buildInvalidatedProject(invalidatedProject, cancellationToken); - if (diagnostics.has(invalidatedProject.projectPath)) { - errorProjects++; - } - else { - successfulProjects++; - } - } + function relName(state: SolutionBuilderState, path: string): string { + return convertToRelativePath(path, state.currentDirectory, f => state.getCanonicalFileName(f)); + } - return errorProjects ? - successfulProjects ? - ExitStatus.DiagnosticsPresent_OutputsGenerated : - ExitStatus.DiagnosticsPresent_OutputsSkipped : - ExitStatus.Success; - } + function reportStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: string[]) { + state.host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); + } - function needsBuild(status: UpToDateStatus, config: ParsedCommandLine) { - if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; - return config.fileNames.length === 0 || - !!config.errors.length || - !isIncrementalCompilation(config.options); + function reportWatchStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { + if (state.hostWithWatch.onWatchStatusChange) { + state.hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions); } + } - function reportParseConfigFileDiagnostic(proj: ResolvedConfigFilePath) { - reportAndStoreErrors(proj, [configFileCache.get(proj) as Diagnostic]); - } + function reportErrors({ host }: SolutionBuilderState, errors: ReadonlyArray) { + errors.forEach(err => host.reportDiagnostic(err)); + } - function reportAndStoreErrors(proj: ResolvedConfigFilePath, errors: ReadonlyArray) { - reportErrors(errors); - projectErrorsReported.set(proj, true); - if (errors.length) { - diagnostics.set(proj, errors); - } + function reportAndStoreErrors(state: SolutionBuilderState, proj: ResolvedConfigFilePath, errors: ReadonlyArray) { + reportErrors(state, errors); + state.projectErrorsReported.set(proj, true); + if (errors.length) { + state.diagnostics.set(proj, errors); } + } - function reportErrors(errors: ReadonlyArray) { - errors.forEach(err => host.reportDiagnostic(err)); - } + function reportParseConfigFileDiagnostic(state: SolutionBuilderState, proj: ResolvedConfigFilePath) { + reportAndStoreErrors(state, proj, [state.configFileCache.get(proj) as Diagnostic]); + } - /** - * Report the build ordering inferred from the current project graph if we're in verbose mode - */ - function reportBuildQueue(buildQueue: readonly ResolvedConfigFileName[]) { - if (options.verbose) { - reportStatus(Diagnostics.Projects_in_this_build_Colon_0, buildQueue.map(s => "\r\n * " + relName(s)).join("")); + function reportErrorSummary(state: SolutionBuilderState) { + if (!state.needsSummary || (!state.watch && !state.host.reportErrorSummary)) return; + state.needsSummary = false; + const { diagnostics } = state; + // Report errors from the other projects + getBuildOrder(state).forEach(project => { + const projectPath = toResolvedConfigFilePath(state, project); + if (!state.projectErrorsReported.has(projectPath)) { + reportErrors(state, diagnostics.get(projectPath) || emptyArray); } + }); + let totalErrors = 0; + diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); + if (state.watch) { + reportWatchStatus(state, getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); } - - function relName(path: string): string { - return convertToRelativePath(path, host.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); + else { + state.host.reportErrorSummary!(totalErrors); } + } - /** - * Report the up-to-date status of a project if we're in verbose mode - */ - function verboseReportProjectStatus(configFileName: string, status: UpToDateStatus) { - if (!options.verbose) return; - return formatUpToDateStatus(configFileName, status, relName, reportStatus); + /** + * Report the build ordering inferred from the current project graph if we're in verbose mode + */ + function reportBuildQueue(state: SolutionBuilderState, buildQueue: readonly ResolvedConfigFileName[]) { + if (state.options.verbose) { + reportStatus(state, Diagnostics.Projects_in_this_build_Colon_0, buildQueue.map(s => "\r\n * " + relName(state, s)).join("")); } } - function formatUpToDateStatus(configFileName: string, status: UpToDateStatus, relName: (fileName: string) => string, formatMessage: (message: DiagnosticMessage, ...args: string[]) => T) { + function reportUpToDateStatus(state: SolutionBuilderState, configFileName: string, status: UpToDateStatus) { switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - relName(configFileName), - relName(status.outOfDateOutputFileName), - relName(status.newerInputFileName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(state, configFileName), + relName(state, status.outOfDateOutputFileName), + relName(state, status.newerInputFileName) + ); case UpToDateStatusType.OutOfDateWithUpstream: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - relName(configFileName), - relName(status.outOfDateOutputFileName), - relName(status.newerProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(state, configFileName), + relName(state, status.outOfDateOutputFileName), + relName(state, status.newerProjectName) + ); case UpToDateStatusType.OutputMissing: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - relName(configFileName), - relName(status.missingOutputFileName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + relName(state, configFileName), + relName(state, status.missingOutputFileName) + ); case UpToDateStatusType.UpToDate: if (status.newestInputFileTime !== undefined) { - return formatMessage(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - relName(configFileName), - relName(status.newestInputFileName || ""), - relName(status.oldestOutputFileName || "")); + return reportStatus( + state, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + relName(state, configFileName), + relName(state, status.newestInputFileName || ""), + relName(state, status.oldestOutputFileName || "") + ); } // Don't report anything for "up to date because it was already built" -- too verbose break; case UpToDateStatusType.OutOfDateWithPrepend: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed, - relName(configFileName), - relName(status.newerProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed, + relName(state, configFileName), + relName(state, status.newerProjectName) + ); case UpToDateStatusType.UpToDateWithUpstreamTypes: - return formatMessage(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, - relName(configFileName)); + return reportStatus( + state, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + relName(state, configFileName) + ); case UpToDateStatusType.UpstreamOutOfDate: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, - relName(configFileName), - relName(status.upstreamProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + relName(state, configFileName), + relName(state, status.upstreamProjectName) + ); case UpToDateStatusType.UpstreamBlocked: - return formatMessage(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, - relName(configFileName), - relName(status.upstreamProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + relName(state, configFileName), + relName(state, status.upstreamProjectName) + ); case UpToDateStatusType.Unbuildable: - return formatMessage(Diagnostics.Failed_to_parse_file_0_Colon_1, - relName(configFileName), - status.reason); + return reportStatus( + state, + Diagnostics.Failed_to_parse_file_0_Colon_1, + relName(state, configFileName), + status.reason + ); case UpToDateStatusType.TsVersionOutputOfDate: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, - relName(configFileName), + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, + relName(state, configFileName), status.version, - version); + version + ); case UpToDateStatusType.ContainerOnly: - // Don't report status on "solution" projects + // Don't report status on "solution" projects case UpToDateStatusType.ComputingUpstream: // Should never leak from getUptoDateStatusWorker break; @@ -1616,4 +1760,13 @@ namespace ts { assertType(status); } } + + /** + * Report the up-to-date status of a project if we're in verbose mode + */ + function verboseReportProjectStatus(state: SolutionBuilderState, configFileName: string, status: UpToDateStatus) { + if (state.options.verbose) { + reportUpToDateStatus(state, configFileName, status); + } + } } diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 7f9f2260d6de2..056e894bf538a 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -437,7 +437,7 @@ export class cNew {}`); function verifyInvalidation(expectedToWriteTests: boolean) { // Rebuild this project tick(); - builder.invalidateProject("/src/logic"); + builder.invalidateProject("/src/logic/tsconfig.json" as ResolvedConfigFilePath); builder.buildNextInvalidatedProject(); // The file should be updated assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 70fe602c8a2f8..f67b575e9795b 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -721,7 +721,7 @@ let x: string = 10;`); host.writeFile(logic[1].path, `${logic[1].content} function foo() { }`); - solutionBuilder.invalidateProject(`${project}/${SubProject.logic}`); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); // not ideal, but currently because of d.ts but no new file is written @@ -734,7 +734,7 @@ function foo() { host.writeFile(logic[1].path, `${logic[1].content} export function gfoo() { }`); - solutionBuilder.invalidateProject(logic[0].path); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); }, expectedProgramFiles); }); @@ -745,7 +745,7 @@ export function gfoo() { compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, references: [{ path: "../core" }] })); - solutionBuilder.invalidateProject(logic[0].path, ConfigFileProgramReloadLevel.Full); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath, ConfigFileProgramReloadLevel.Full); solutionBuilder.buildNextInvalidatedProject(); }, [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]); }); @@ -965,7 +965,7 @@ export function gfoo() { host.writeFile(bTs.path, `${bTs.content} export function gfoo() { }`); - solutionBuilder.invalidateProject(bTsconfig.path); + solutionBuilder.invalidateProject(bTsconfig.path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); }, emptyArray, From 6227fabbed1d1c46fcef46ba6ef5bd4fcc86c1af Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 7 May 2019 13:05:24 -0700 Subject: [PATCH 28/41] Make invalidated project when only need to be built or updated --- src/compiler/tsbuild.ts | 295 ++++++++++++++++++++++++---------------- 1 file changed, 177 insertions(+), 118 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 22bc8ba823896..e015e8bb13f87 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -283,14 +283,30 @@ namespace ts { /*@internal*/ buildNextInvalidatedProject(): void; } - interface InvalidatedProject { + const enum InvalidatedProjectKind { + BuildProject, + UpdateBundle, + UpdateOutputFileStamps + } + + interface UpdateOutputFileStampsProject { + readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; + readonly project: ResolvedConfigFileName; + readonly projectPath: ResolvedConfigFilePath; + readonly config: ParsedCommandLine; + } + + interface BuildOrUpdateBundleProject { + readonly kind: InvalidatedProjectKind.BuildProject | InvalidatedProjectKind.UpdateBundle; readonly project: ResolvedConfigFileName; readonly projectPath: ResolvedConfigFilePath; - readonly reloadLevel: ConfigFileProgramReloadLevel; readonly projectIndex: number; + readonly config: ParsedCommandLine; readonly buildOrder: readonly ResolvedConfigFileName[]; } + type InvalidatedProject = UpdateOutputFileStampsProject | BuildOrUpdateBundleProject; + /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic */ @@ -663,15 +679,90 @@ namespace ts { } function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { - return state.projectPendingBuild.size ? - forEach(buildOrder, (project, projectIndex) => { - const projectPath = toResolvedConfigFilePath(state, project); - const reloadLevel = state.projectPendingBuild.get(projectPath); - if (reloadLevel !== undefined) { - return { project, projectPath, reloadLevel, projectIndex, buildOrder }; + if (!state.projectPendingBuild.size) return undefined; + + const { options, projectPendingBuild } = state; + for (let projectIndex = 0; projectIndex < buildOrder.length; projectIndex++) { + const project = buildOrder[projectIndex]; + const projectPath = toResolvedConfigFilePath(state, project); + const reloadLevel = state.projectPendingBuild.get(projectPath); + if (reloadLevel === undefined) continue; + + const config = parseConfigFile(state, project, projectPath); + if (!config) { + reportParseConfigFileDiagnostic(state, projectPath); + projectPendingBuild.delete(projectPath); + continue; + } + + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + watchConfigFile(state, project, projectPath); + watchWildCardDirectories(state, project, projectPath, config); + watchInputFiles(state, project, projectPath, config); + } + else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { + // Update file names + const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost); + updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); + config.fileNames = result.fileNames; + watchInputFiles(state, project, projectPath, config); + } + + const status = getUpToDateStatus(state, config, projectPath); + verboseReportProjectStatus(state, project, status); + if (!options.force) { + if (status.type === UpToDateStatusType.UpToDate) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportStatus(state, Diagnostics.Project_0_is_up_to_date, project); + } + continue; } - }) : - undefined; + + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { + reportAndStoreErrors(state, projectPath, config.errors); + return { + kind: InvalidatedProjectKind.UpdateOutputFileStamps, + project, + projectPath, + config + }; + + continue; + } + } + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + if (options.verbose) reportStatus(state, Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); + continue; + } + + if (status.type === UpToDateStatusType.ContainerOnly) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + // Do nothing + continue; + } + + + return { + kind: needsBuild(state, status, config) ? + InvalidatedProjectKind.BuildProject : + InvalidatedProjectKind.UpdateBundle, + project, + projectPath, + projectIndex, + config, + buildOrder + }; + } + + return undefined; } function listEmittedFile({ writeFileName }: SolutionBuilderState, proj: ParsedCommandLine, file: string) { @@ -714,7 +805,44 @@ namespace ts { return errorFlags; } - function buildSingleProject(state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { + function updateModuleResolutionCache( + state: SolutionBuilderState, + proj: ResolvedConfigFileName, + config: ParsedCommandLine + ) { + if (!state.moduleResolutionCache) return; + + // Update module resolution cache if needed + const { moduleResolutionCache } = state; + const projPath = toPath(state, proj); + if (moduleResolutionCache.directoryToModuleNameMap.redirectsMap.size === 0) { + // The own map will be for projectCompilerOptions + Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size === 0); + moduleResolutionCache.directoryToModuleNameMap.redirectsMap.set(projPath, moduleResolutionCache.directoryToModuleNameMap.ownMap); + moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.set(projPath, moduleResolutionCache.moduleNameToDirectoryMap.ownMap); + } + else { + // Set correct own map + Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size > 0); + + const ref: ResolvedProjectReference = { + sourceFile: config.options.configFile!, + commandLine: config + }; + moduleResolutionCache.directoryToModuleNameMap.setOwnMap(moduleResolutionCache.directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); + moduleResolutionCache.moduleNameToDirectoryMap.setOwnMap(moduleResolutionCache.moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); + } + moduleResolutionCache.directoryToModuleNameMap.setOwnOptions(config.options); + moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(config.options); + } + + function buildSingleProject( + state: SolutionBuilderState, + proj: ResolvedConfigFileName, + resolvedPath: ResolvedConfigFilePath, + config: ParsedCommandLine, + cancellationToken: CancellationToken | undefined + ): BuildResultFlags { if (state.options.dry) { reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, proj); return BuildResultFlags.Success; @@ -722,54 +850,25 @@ namespace ts { if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, proj); - const { host, projectStatus, diagnostics, compilerHost, moduleResolutionCache, } = state; - const configFile = parseConfigFile(state, proj, resolvedPath); - if (!configFile) { - // Failed to read the config file - reportParseConfigFileDiagnostic(state, resolvedPath); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); - return BuildResultFlags.ConfigFileErrors; - } - - if (configFile.fileNames.length === 0) { - reportAndStoreErrors(state, resolvedPath, configFile.errors); + if (config.fileNames.length === 0) { + reportAndStoreErrors(state, resolvedPath, config.errors); // Nothing to build - must be a solution file, basically return BuildResultFlags.None; } - state.projectCompilerOptions = configFile.options; + const { host, projectStatus, diagnostics, compilerHost } = state; + state.projectCompilerOptions = config.options; // Update module resolution cache if needed - if (moduleResolutionCache) { - const projPath = toPath(state, proj); - if (moduleResolutionCache.directoryToModuleNameMap.redirectsMap.size === 0) { - // The own map will be for projectCompilerOptions - Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size === 0); - moduleResolutionCache.directoryToModuleNameMap.redirectsMap.set(projPath, moduleResolutionCache.directoryToModuleNameMap.ownMap); - moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.set(projPath, moduleResolutionCache.moduleNameToDirectoryMap.ownMap); - } - else { - // Set correct own map - Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size > 0); - - const ref: ResolvedProjectReference = { - sourceFile: configFile.options.configFile!, - commandLine: configFile - }; - moduleResolutionCache.directoryToModuleNameMap.setOwnMap(moduleResolutionCache.directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); - moduleResolutionCache.moduleNameToDirectoryMap.setOwnMap(moduleResolutionCache.moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); - } - moduleResolutionCache.directoryToModuleNameMap.setOwnOptions(configFile.options); - moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(configFile.options); - } + updateModuleResolutionCache(state, proj, config); // Create program const program = host.createProgram( - configFile.fileNames, - configFile.options, + config.fileNames, + config.options, compilerHost, - getOldProgram(state, resolvedPath, configFile), - configFile.errors, - configFile.projectReferences + getOldProgram(state, resolvedPath, config), + config.errors, + config.projectReferences ); // Don't emit anything in the presence of syntactic errors or options diagnostics @@ -867,24 +966,30 @@ namespace ts { } if (state.writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(state, configFile, name)); + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); listFiles(program, state.writeFileName); } // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); diagnostics.delete(resolvedPath); projectStatus.set(resolvedPath, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) + oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()) }); afterProgramCreate(state, resolvedPath, program); state.projectCompilerOptions = state.baseCompilerOptions; return resultFlags; } - function updateBundle(state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { + function updateBundle( + state: SolutionBuilderState, + proj: ResolvedConfigFileName, + resolvedPath: ResolvedConfigFilePath, + config: ParsedCommandLine, + cancellationToken: CancellationToken | undefined + ): BuildResultFlags { if (state.options.dry) { reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); return BuildResultFlags.Success; @@ -894,7 +999,6 @@ namespace ts { // Update js, and source map const { projectStatus, diagnostics, compilerHost } = state; - const config = Debug.assertDefined(parseConfigFile(state, proj, resolvedPath)); state.projectCompilerOptions = config.options; const outputFiles = emitUsingBuildInfo( config, @@ -905,7 +1009,7 @@ namespace ts { }); if (isString(outputFiles)) { reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(state, outputFiles)); - return buildSingleProject(state, proj, resolvedPath, cancellationToken); + return buildSingleProject(state, proj, resolvedPath, config, cancellationToken); } // Actual Emit @@ -1210,68 +1314,24 @@ namespace ts { !isIncrementalCompilation(config.options); } - function buildInvalidatedProject(state: SolutionBuilderState, { project, projectPath, reloadLevel, projectIndex, buildOrder }: InvalidatedProject, cancellationToken?: CancellationToken) { - const { options, projectPendingBuild } = state; - const config = parseConfigFile(state, project, projectPath); - if (!config) { - reportParseConfigFileDiagnostic(state, projectPath); - projectPendingBuild.delete(projectPath); - return; - } - - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(state, project, projectPath); - watchWildCardDirectories(state, project, projectPath, config); - watchInputFiles(state, project, projectPath, config); - } - else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { - // Update file names - const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost); - updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); - config.fileNames = result.fileNames; - watchInputFiles(state, project, projectPath, config); - } - - const status = getUpToDateStatus(state, config, projectPath); - verboseReportProjectStatus(state, project, status); - if (!options.force) { - if (status.type === UpToDateStatusType.UpToDate) { - reportAndStoreErrors(state, projectPath, config.errors); - projectPendingBuild.delete(projectPath); - // Up to date, skip - if (options.dry) { - // In a dry build, inform the user of this fact - reportStatus(state, Diagnostics.Project_0_is_up_to_date, project); - } - return; - } - - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { - reportAndStoreErrors(state, projectPath, config.errors); - projectPendingBuild.delete(projectPath); - // Fake that files have been built by updating output file stamps - updateOutputTimestamps(state, config, projectPath); - return; - } - } - - if (status.type === UpToDateStatusType.UpstreamBlocked) { - reportAndStoreErrors(state, projectPath, config.errors); - projectPendingBuild.delete(projectPath); - if (options.verbose) reportStatus(state, Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); - return; - } - - if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(state, projectPath, config.errors); + function buildInvalidatedProject( + state: SolutionBuilderState, + invalidatedProject: InvalidatedProject, + cancellationToken?: CancellationToken + ) { + const { projectPendingBuild } = state; + if (invalidatedProject.kind === InvalidatedProjectKind.UpdateOutputFileStamps) { + // Fake that files have been built by updating output file stamps + const { projectPath, config } = invalidatedProject; + updateOutputTimestamps(state, config, projectPath); projectPendingBuild.delete(projectPath); - // Do nothing return; } - const buildResult = needsBuild(state, status, config) ? - buildSingleProject(state, project, projectPath, cancellationToken) : // Actual build - updateBundle(state, project, projectPath, cancellationToken); // Fake that files have been built by manipulating prepend and existing output + const { kind, project, projectPath, projectIndex, config, buildOrder } = invalidatedProject; + const buildResult = kind === InvalidatedProjectKind.BuildProject ? + buildSingleProject(state, project, projectPath, config, cancellationToken) : // Actual build + updateBundle(state, project, projectPath, config, cancellationToken); // Fake that files have been built by manipulating prepend and existing output projectPendingBuild.delete(projectPath); // Only composite projects can be referenced by other projects if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { @@ -1467,12 +1527,11 @@ namespace ts { if (state.watch && !state.timerToBuildInvalidatedProject) { scheduleBuildInvalidatedProject(state); } - } - else { - disableCache(state); - reportErrorSummary(state); + return; } } + disableCache(state); + reportErrorSummary(state); } function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { From 5270b7e9b0da74d258bf96045a51878623acc5f7 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 7 May 2019 14:14:30 -0700 Subject: [PATCH 29/41] Make invalidated projects as api so we can expose it later --- src/compiler/tsbuild.ts | 210 +++++++++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 78 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e015e8bb13f87..b99567e54a783 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -283,30 +283,6 @@ namespace ts { /*@internal*/ buildNextInvalidatedProject(): void; } - const enum InvalidatedProjectKind { - BuildProject, - UpdateBundle, - UpdateOutputFileStamps - } - - interface UpdateOutputFileStampsProject { - readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; - readonly project: ResolvedConfigFileName; - readonly projectPath: ResolvedConfigFilePath; - readonly config: ParsedCommandLine; - } - - interface BuildOrUpdateBundleProject { - readonly kind: InvalidatedProjectKind.BuildProject | InvalidatedProjectKind.UpdateBundle; - readonly project: ResolvedConfigFileName; - readonly projectPath: ResolvedConfigFilePath; - readonly projectIndex: number; - readonly config: ParsedCommandLine; - readonly buildOrder: readonly ResolvedConfigFileName[]; - } - - type InvalidatedProject = UpdateOutputFileStampsProject | BuildOrUpdateBundleProject; - /** * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic */ @@ -678,6 +654,121 @@ namespace ts { } } + const enum InvalidatedProjectKind { + Build, + UpdateBundle, + UpdateOutputFileStamps + } + + interface InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind; + readonly project: ResolvedConfigFileName; + readonly projectPath: ResolvedConfigFilePath; + /** + * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly + */ + done(cancellationToken?: CancellationToken): void; + } + + interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; + updateOutputFileStatmps(): void; + } + + interface BuildInvalidedProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.Build; + build(cancellationToken?: CancellationToken): BuildResultFlags; + } + + interface UpdateBundleProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateBundle; + updateBundle(cancellationToken?: CancellationToken): BuildResultFlags; + } + + type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; + + function createUpdateOutputFileStampsProject(state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, config: ParsedCommandLine): UpdateOutputFileStampsProject { + let updateOutputFileStampsPending = true; + return { + kind: InvalidatedProjectKind.UpdateOutputFileStamps, + project, + projectPath, + updateOutputFileStatmps: () => { + updateOutputTimestamps(state, config, projectPath); + updateOutputFileStampsPending = false; + }, + done: () => { + if (updateOutputFileStampsPending) { + updateOutputTimestamps(state, config, projectPath); + } + state.projectPendingBuild.delete(projectPath); + } + }; + } + + function createBuildInvalidedProject( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[] + ): BuildInvalidedProject { + let buildPending = true; + return { + kind: InvalidatedProjectKind.Build, + project, + projectPath, + build, + done: cancellationToken => { + if (buildPending) build(cancellationToken); + state.projectPendingBuild.delete(projectPath); + } + }; + + function build(cancellationToken?: CancellationToken) { + const buildResult = buildSingleProject(state, project, projectPath, config, cancellationToken); + queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, buildResult); + buildPending = false; + return buildResult; + } + } + + function createUpdateBundleProject( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[] + ): UpdateBundleProject { + let updatePending = true; + return { + kind: InvalidatedProjectKind.UpdateBundle, + project, + projectPath, + updateBundle: update, + done: cancellationToken => { + if (updatePending) update(cancellationToken); + state.projectPendingBuild.delete(projectPath); + } + }; + + function update(cancellationToken?: CancellationToken) { + const buildResult = updateBundle(state, project, projectPath, config, cancellationToken); + queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, buildResult); + updatePending = false; + return buildResult; + } + } + + function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { + if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; + return config.fileNames.length === 0 || + !!config.errors.length || + !isIncrementalCompilation(config.options); + } + function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { if (!state.projectPendingBuild.size) return undefined; @@ -724,14 +815,12 @@ namespace ts { if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { reportAndStoreErrors(state, projectPath, config.errors); - return { - kind: InvalidatedProjectKind.UpdateOutputFileStamps, + return createUpdateOutputFileStampsProject( + state, project, projectPath, config - }; - - continue; + ); } } @@ -749,17 +838,9 @@ namespace ts { continue; } - - return { - kind: needsBuild(state, status, config) ? - InvalidatedProjectKind.BuildProject : - InvalidatedProjectKind.UpdateBundle, - project, - projectPath, - projectIndex, - config, - buildOrder - }; + return needsBuild(state, status, config) ? + createBuildInvalidedProject(state, project, projectPath, projectIndex, config, buildOrder) : + createUpdateBundleProject(state, project, projectPath, projectIndex, config, buildOrder); } return undefined; @@ -1307,46 +1388,19 @@ namespace ts { }); } - function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { - if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; - return config.fileNames.length === 0 || - !!config.errors.length || - !isIncrementalCompilation(config.options); - } - - function buildInvalidatedProject( - state: SolutionBuilderState, - invalidatedProject: InvalidatedProject, - cancellationToken?: CancellationToken - ) { - const { projectPendingBuild } = state; - if (invalidatedProject.kind === InvalidatedProjectKind.UpdateOutputFileStamps) { - // Fake that files have been built by updating output file stamps - const { projectPath, config } = invalidatedProject; - updateOutputTimestamps(state, config, projectPath); - projectPendingBuild.delete(projectPath); - return; - } - - const { kind, project, projectPath, projectIndex, config, buildOrder } = invalidatedProject; - const buildResult = kind === InvalidatedProjectKind.BuildProject ? - buildSingleProject(state, project, projectPath, config, cancellationToken) : // Actual build - updateBundle(state, project, projectPath, config, cancellationToken); // Fake that files have been built by manipulating prepend and existing output - projectPendingBuild.delete(projectPath); - // Only composite projects can be referenced by other projects - if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { - queueReferencingProjects(state, project, projectPath, projectIndex, buildOrder, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); - } - } - function queueReferencingProjects( state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, + config: ParsedCommandLine, buildOrder: readonly ResolvedConfigFileName[], - declarationOutputChanged: boolean + buildResult: BuildResultFlags ) { + // Queue only if there are no errors + if (buildResult & BuildResultFlags.AnyErrors) return; + // Only composite projects can be referenced by other projects + if (!config.options.composite) return; // Always use build order to queue projects for (let index = projectIndex + 1; index < buildOrder.length; index++) { const nextProject = buildOrder[index]; @@ -1365,7 +1419,7 @@ namespace ts { if (status) { switch (status.type) { case UpToDateStatusType.UpToDate: - if (!declarationOutputChanged) { + if (buildResult & BuildResultFlags.DeclarationOutputUnchanged) { if (ref.prepend) { state.projectStatus.set(nextProjectPath, { type: UpToDateStatusType.OutOfDateWithPrepend, @@ -1382,7 +1436,7 @@ namespace ts { // falls through case UpToDateStatusType.UpToDateWithUpstreamTypes: case UpToDateStatusType.OutOfDateWithPrepend: - if (declarationOutputChanged) { + if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { state.projectStatus.set(nextProjectPath, { type: UpToDateStatusType.OutOfDateWithUpstream, outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, @@ -1409,7 +1463,7 @@ namespace ts { const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); if (!invalidatedProject) return undefined; - buildInvalidatedProject(state, invalidatedProject, cancellationToken); + invalidatedProject.done(cancellationToken); return { project: invalidatedProject.project, result: state.diagnostics.has(invalidatedProject.projectPath) ? @@ -1429,7 +1483,7 @@ namespace ts { while (true) { const invalidatedProject = getNextInvalidatedProject(state, buildOrder); if (!invalidatedProject) break; - buildInvalidatedProject(state, invalidatedProject, cancellationToken); + invalidatedProject.done(cancellationToken); if (state.diagnostics.has(invalidatedProject.projectPath)) { errorProjects++; } @@ -1521,7 +1575,7 @@ namespace ts { } const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); if (invalidatedProject) { - buildInvalidatedProject(state, invalidatedProject); + invalidatedProject.done(); if (state.projectPendingBuild.size) { // Schedule next project for build if (state.watch && !state.timerToBuildInvalidatedProject) { From e4fe4acc17d3f7dbae6e911c49b1604e114c1660 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 7 May 2019 14:22:34 -0700 Subject: [PATCH 30/41] Make update bundle return invalidated project if it cant update the bundle --- src/compiler/tsbuild.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index b99567e54a783..05e6d0d27902a 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -682,7 +682,7 @@ namespace ts { interface UpdateBundleProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.UpdateBundle; - updateBundle(cancellationToken?: CancellationToken): BuildResultFlags; + updateBundle(): BuildResultFlags | BuildInvalidedProject; } type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; @@ -749,13 +749,21 @@ namespace ts { projectPath, updateBundle: update, done: cancellationToken => { - if (updatePending) update(cancellationToken); + if (updatePending) { + const result = update(); + if ((result as BuildInvalidedProject).project) { + return (result as BuildInvalidedProject).done(cancellationToken); + } + } state.projectPendingBuild.delete(projectPath); } }; - function update(cancellationToken?: CancellationToken) { - const buildResult = updateBundle(state, project, projectPath, config, cancellationToken); + function update() { + const buildResult = updateBundle(state, project, projectPath, config); + if (isString(buildResult)) { + return createBuildInvalidedProject(state, project, projectPath, projectIndex, config, buildOrder); + } queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, buildResult); updatePending = false; return buildResult; @@ -1068,9 +1076,8 @@ namespace ts { state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, - config: ParsedCommandLine, - cancellationToken: CancellationToken | undefined - ): BuildResultFlags { + config: ParsedCommandLine + ): BuildResultFlags | string { if (state.options.dry) { reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); return BuildResultFlags.Success; @@ -1090,7 +1097,7 @@ namespace ts { }); if (isString(outputFiles)) { reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(state, outputFiles)); - return buildSingleProject(state, proj, resolvedPath, config, cancellationToken); + return outputFiles; } // Actual Emit From 9f9ae000cb395543650306c53b9995aed94efcb8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 7 May 2019 15:53:01 -0700 Subject: [PATCH 31/41] Enable getSemanticDiagnosticsOfNextAffectedFile for EmitAndSemanticDiagnosticsBuilder --- src/compiler/builder.ts | 22 ++++---- src/harness/fakes.ts | 52 ++++++++++--------- .../unittests/tscWatch/incremental.ts | 16 +++++- .../reference/api/tsserverlibrary.d.ts | 2 +- tests/baselines/reference/api/typescript.d.ts | 2 +- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 78058bf5a5341..7190407dd8621 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -796,6 +796,7 @@ namespace ts { (result as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; } else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + (result as EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; (result as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; } else { @@ -913,6 +914,11 @@ namespace ts { ); } + // Add file to affected file pending emit to handle for later emit time + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + addToAffectedFilesPendingEmit(state, [(affected as SourceFile).path]); + } + // Get diagnostics for the affected file if its not ignored if (ignoreSourceFile && ignoreSourceFile(affected as SourceFile)) { // Get next affected file @@ -951,18 +957,8 @@ namespace ts { // When semantic builder asks for diagnostics of the whole program, // ensure that all the affected files are handled - let affected: SourceFile | Program | undefined; - let affectedFilesPendingEmit: Path[] | undefined; - while (affected = getNextAffectedFile(state, cancellationToken, computeHash)) { - if (affected !== state.program && kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - (affectedFilesPendingEmit || (affectedFilesPendingEmit = [])).push((affected as SourceFile).path); - } - doneWithAffectedFile(state, affected); - } - - // In case of emit builder, cache the files to be emitted - if (affectedFilesPendingEmit) { - addToAffectedFilesPendingEmit(state, affectedFilesPendingEmit); + // tslint:disable-next-line no-empty + while (getSemanticDiagnosticsOfNextAffectedFile(cancellationToken)) { } let diagnostics: Diagnostic[] | undefined; @@ -1181,7 +1177,7 @@ namespace ts { * The builder that can handle the changes in program and iterate through changed file to emit the files * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files */ - export interface EmitAndSemanticDiagnosticsBuilderProgram extends BuilderProgram { + export interface EmitAndSemanticDiagnosticsBuilderProgram extends SemanticDiagnosticsBuilderProgram { /** * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 489a41dd677d7..c3a3bb621ce59 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -388,6 +388,33 @@ namespace fakes { return ts.compareStringsCaseSensitive(ts.isString(a) ? a : a[0], ts.isString(b) ? b : b[0]); } + export function sanitizeBuildInfoProgram(buildInfo: ts.BuildInfo) { + if (buildInfo.program) { + // reference Map + if (buildInfo.program.referencedMap) { + const referencedMap: ts.MapLike = {}; + for (const path of ts.getOwnKeys(buildInfo.program.referencedMap).sort()) { + referencedMap[path] = buildInfo.program.referencedMap[path].sort(); + } + buildInfo.program.referencedMap = referencedMap; + } + + // exportedModulesMap + if (buildInfo.program.exportedModulesMap) { + const exportedModulesMap: ts.MapLike = {}; + for (const path of ts.getOwnKeys(buildInfo.program.exportedModulesMap).sort()) { + exportedModulesMap[path] = buildInfo.program.exportedModulesMap[path].sort(); + } + buildInfo.program.exportedModulesMap = exportedModulesMap; + } + + // semanticDiagnosticsPerFile + if (buildInfo.program.semanticDiagnosticsPerFile) { + buildInfo.program.semanticDiagnosticsPerFile.sort(compareProgramBuildInfoDiagnostic); + } + } + } + export const version = "FakeTSVersion"; export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost { @@ -405,30 +432,7 @@ namespace fakes { public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) { if (!ts.isBuildInfoFile(fileName)) return super.writeFile(fileName, content, writeByteOrderMark); const buildInfo = ts.getBuildInfo(content); - if (buildInfo.program) { - // reference Map - if (buildInfo.program.referencedMap) { - const referencedMap: ts.MapLike = {}; - for (const path of ts.getOwnKeys(buildInfo.program.referencedMap).sort()) { - referencedMap[path] = buildInfo.program.referencedMap[path].sort(); - } - buildInfo.program.referencedMap = referencedMap; - } - - // exportedModulesMap - if (buildInfo.program.exportedModulesMap) { - const exportedModulesMap: ts.MapLike = {}; - for (const path of ts.getOwnKeys(buildInfo.program.exportedModulesMap).sort()) { - exportedModulesMap[path] = buildInfo.program.exportedModulesMap[path].sort(); - } - buildInfo.program.exportedModulesMap = exportedModulesMap; - } - - // semanticDiagnosticsPerFile - if (buildInfo.program.semanticDiagnosticsPerFile) { - buildInfo.program.semanticDiagnosticsPerFile.sort(compareProgramBuildInfoDiagnostic); - } - } + sanitizeBuildInfoProgram(buildInfo); buildInfo.version = version; super.writeFile(fileName, ts.getBuildInfoText(buildInfo), writeByteOrderMark); } diff --git a/src/testRunner/unittests/tscWatch/incremental.ts b/src/testRunner/unittests/tscWatch/incremental.ts index 35ec5bff55552..54109dec84639 100644 --- a/src/testRunner/unittests/tscWatch/incremental.ts +++ b/src/testRunner/unittests/tscWatch/incremental.ts @@ -105,9 +105,23 @@ namespace ts.tscWatch { result.close(); } + function sanitizeBuildInfo(content: string) { + const buildInfo = getBuildInfo(content); + fakes.sanitizeBuildInfoProgram(buildInfo); + return getBuildInfoText(buildInfo); + } + function checkFileEmit(actual: Map, expected: ReadonlyArray) { assert.equal(actual.size, expected.length, `Actual: ${JSON.stringify(arrayFrom(actual.entries()), /*replacer*/ undefined, " ")}\nExpected: ${JSON.stringify(expected, /*replacer*/ undefined, " ")}`); - expected.forEach(file => assert.equal(actual.get(file.path), file.content, `Emit for ${file.path}`)); + expected.forEach(file => { + let expectedContent = file.content; + let actualContent = actual.get(file.path); + if (isBuildInfoFile(file.path)) { + actualContent = actualContent && sanitizeBuildInfo(actualContent); + expectedContent = sanitizeBuildInfo(expectedContent); + } + assert.equal(actualContent, expectedContent, `Emit for ${file.path}`); + }); } const libFileInfo: BuilderState.FileInfo = { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index f13d0ab3f5121..df479429c1705 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4417,7 +4417,7 @@ declare namespace ts { * The builder that can handle the changes in program and iterate through changed file to emit the files * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files */ - interface EmitAndSemanticDiagnosticsBuilderProgram extends BuilderProgram { + interface EmitAndSemanticDiagnosticsBuilderProgram extends SemanticDiagnosticsBuilderProgram { /** * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index e5e5046794aaa..4062969e8cd97 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4417,7 +4417,7 @@ declare namespace ts { * The builder that can handle the changes in program and iterate through changed file to emit the files * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files */ - interface EmitAndSemanticDiagnosticsBuilderProgram extends BuilderProgram { + interface EmitAndSemanticDiagnosticsBuilderProgram extends SemanticDiagnosticsBuilderProgram { /** * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host From f0b7e08d2ca2d6380bea31f36d32f28d3b905a84 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 9 May 2019 13:13:50 -0700 Subject: [PATCH 32/41] Move towards BuildInvalidatedProject api where one can query program and perform its operations --- src/compiler/tsbuild.ts | 490 ++++++++++++++++++++++++++-------------- src/compiler/watch.ts | 40 +++- src/tsc/tsc.ts | 2 +- 3 files changed, 359 insertions(+), 173 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 05e6d0d27902a..5498c71098354 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -675,17 +675,44 @@ namespace ts { updateOutputFileStatmps(): void; } - interface BuildInvalidedProject extends InvalidatedProjectBase { + interface BuildInvalidedProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.Build; - build(cancellationToken?: CancellationToken): BuildResultFlags; + /* + * Emitting with this builder program without the api provided for this project + * can result in build system going into invalid state as files written reflect the state of the project + */ + getBuilderProgram(): T | undefined; + getProgram(): Program | undefined; + getCompilerOptions(): CompilerOptions; + 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>; + /* + * Calling emit directly with targetSourceFile and emitOnlyDtsFiles set to true is not advised since + * emit in build system is responsible in updating status of the project + * If called with targetSourceFile and emitOnlyDtsFiles set to true, the emit just passes to underlying builder and + * wont reflect the status of file as being emitted in the builder + * (if that emit of that source file is required it would be emitted again when making sure invalidated project is completed) + * This emit is not considered actual emit (and hence uptodate status is not reflected if + */ + 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 { + interface UpdateBundleProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.UpdateBundle; - updateBundle(): BuildResultFlags | BuildInvalidedProject; + updateBundle(): BuildResultFlags | BuildInvalidedProject; } - type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; + type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; function createUpdateOutputFileStampsProject(state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, config: ParsedCommandLine): UpdateOutputFileStampsProject { let updateOutputFileStampsPending = true; @@ -706,42 +733,318 @@ namespace ts { }; } - function createBuildInvalidedProject( - state: SolutionBuilderState, + function createBuildInvalidedProject( + state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, config: ParsedCommandLine, buildOrder: readonly ResolvedConfigFileName[] - ): BuildInvalidedProject { - let buildPending = true; + ): BuildInvalidedProject { + enum Step { + CreateProgram, + SyntaxDiagnostics, + SemanticDiagnostics, + Emit, + QueueReferencingProjects, + Done + } + + let step = Step.CreateProgram; + let program: T | undefined; + let buildResult: BuildResultFlags | undefined; + return { kind: InvalidatedProjectKind.Build, project, projectPath, - build, + getBuilderProgram: () => withProgramOrUndefined(identity), + getProgram: () => + withProgramOrUndefined( + program => program.getProgramOrUndefined() + ), + getCompilerOptions: () => config.options, + getSourceFile: fileName => + withProgramOrUndefined( + program => program.getSourceFile(fileName) + ), + getSourceFiles: () => + withProgramOrEmptyArray( + program => program.getSourceFiles() + ), + getOptionsDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getOptionsDiagnostics(cancellationToken) + ), + getGlobalDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getGlobalDiagnostics(cancellationToken) + ), + getConfigFileParsingDiagnostics: () => + withProgramOrEmptyArray( + program => program.getConfigFileParsingDiagnostics() + ), + getSyntacticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSyntacticDiagnostics(sourceFile, cancellationToken) + ), + getAllDependencies: sourceFile => + withProgramOrEmptyArray( + program => program.getAllDependencies(sourceFile) + ), + getSemanticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSemanticDiagnostics(sourceFile, cancellationToken) + ), + getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) => + withProgramOrUndefined( + program => + ((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) && + (program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile) + ), + emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => { + if (targetSourceFile || emitOnlyDtsFiles) { + return withProgramOrUndefined( + program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) + ); + } + executeSteps(Step.SemanticDiagnostics, cancellationToken); + if (step !== Step.Emit) return undefined; + return emit(writeFile, cancellationToken, customTransformers); + }, + getCurrentDirectory: () => state.currentDirectory, done: cancellationToken => { - if (buildPending) build(cancellationToken); + executeSteps(Step.Done, cancellationToken); state.projectPendingBuild.delete(projectPath); } }; - function build(cancellationToken?: CancellationToken) { - const buildResult = buildSingleProject(state, project, projectPath, config, cancellationToken); - queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, buildResult); - buildPending = false; - return buildResult; + function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { + executeSteps(Step.CreateProgram); + return program && action(program); + } + + function withProgramOrEmptyArray(action: (program: T) => ReadonlyArray): ReadonlyArray { + return withProgramOrUndefined(action) || emptyArray; + } + + function createProgram() { + Debug.assert(program === undefined); + + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, project); + buildResult = BuildResultFlags.Success; + step = Step.QueueReferencingProjects; + return; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, project); + + if (config.fileNames.length === 0) { + reportAndStoreErrors(state, projectPath, config.errors); + // Nothing to build - must be a solution file, basically + buildResult = BuildResultFlags.None; + step = Step.QueueReferencingProjects; + return; + } + + const { host, compilerHost } = state; + state.projectCompilerOptions = config.options; + // Update module resolution cache if needed + updateModuleResolutionCache(state, project, config); + + // Create program + program = host.createProgram( + config.fileNames, + config.options, + compilerHost, + getOldProgram(state, projectPath, config), + config.errors, + config.projectReferences + ); + step++; + } + + function handleDiagnostics(diagnostics: ReadonlyArray, errorFlags: BuildResultFlags, errorType: string) { + if (diagnostics.length) { + buildResult = buildErrors( + state, + projectPath, + program, + diagnostics, + errorFlags, + errorType + ); + step = Step.QueueReferencingProjects; + } + else { + step++; + } + } + + function getSyntaxDiagnostics(cancellationToken?: CancellationToken) { + Debug.assertDefined(program); + handleDiagnostics( + [ + ...program!.getConfigFileParsingDiagnostics(), + ...program!.getOptionsDiagnostics(cancellationToken), + ...program!.getGlobalDiagnostics(cancellationToken), + ...program!.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken) + ], + BuildResultFlags.SyntaxErrors, + "Syntactic" + ); + } + + function getSemanticDiagnostics(cancellationToken?: CancellationToken) { + handleDiagnostics( + Debug.assertDefined(program).getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken), + BuildResultFlags.TypeErrors, + "Semantic" + ); + } + + function emit(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitResult { + Debug.assertDefined(program); + Debug.assert(step === Step.Emit); + // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly + program!.backupState(); + let declDiagnostics: Diagnostic[] | undefined; + const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); + const outputFiles: OutputFile[] = []; + const { emitResult } = emitFilesAndReportErrors( + program!, + reportDeclarationDiagnostics, + /*writeFileName*/ undefined, + /*reportSummary*/ undefined, + (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), + cancellationToken, + /*emitOnlyDts*/ false, + customTransformers + ); + // Don't emit .d.ts if there are decl file errors + if (declDiagnostics) { + program!.restoreState(); + buildResult = buildErrors( + state, + projectPath, + program, + declDiagnostics, + BuildResultFlags.DeclarationEmitErrors, + "Declaration file" + ); + step = Step.QueueReferencingProjects; + return { + emitSkipped: true, + diagnostics: emitResult.diagnostics + }; + } + + // Actual Emit + const { host, compilerHost, diagnostics, projectStatus } = state; + let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; + let newestDeclarationFileContentChangedTime = minimumDate; + let anyDtsChanged = false; + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + let priorChangeTime: Date | undefined; + if (!anyDtsChanged && isDeclarationFile(name)) { + // Check for unchanged .d.ts files + if (host.fileExists(name) && state.readFileWithCache(name) === text) { + priorChangeTime = host.getModifiedTime(name); + } + else { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + anyDtsChanged = true; + } + } + + emittedOutputs.set(toPath(state, name), name); + writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); + } + }); + + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + buildResult = buildErrors( + state, + projectPath, + program, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); + step = Step.QueueReferencingProjects; + return emitResult; + } + + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); + listFiles(program!, state.writeFileName); + } + + // Update time stamps for rest of the outputs + newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + diagnostics.delete(projectPath); + projectStatus.set(projectPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, + oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()) + }); + afterProgramCreate(state, projectPath, program!); + state.projectCompilerOptions = state.baseCompilerOptions; + step++; + buildResult = resultFlags; + return emitResult; + } + + function executeSteps(till: Step, cancellationToken?: CancellationToken) { + while (step <= till && step < Step.Done) { + const currentStep = step; + switch (step) { + case Step.CreateProgram: + createProgram(); + break; + + case Step.SyntaxDiagnostics: + getSyntaxDiagnostics(cancellationToken); + break; + + case Step.SemanticDiagnostics: + getSemanticDiagnostics(cancellationToken); + break; + + case Step.Emit: + emit(/*writeFileCallback*/ undefined, cancellationToken); + break; + + case Step.QueueReferencingProjects: + queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, Debug.assertDefined(buildResult)); + step++; + break; + + // Should never be done + case Step.Done: + default: + assertType(step); + + } + Debug.assert(step > currentStep); + } } } - function createUpdateBundleProject( - state: SolutionBuilderState, + function createUpdateBundleProject( + state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, config: ParsedCommandLine, buildOrder: readonly ResolvedConfigFileName[] - ): UpdateBundleProject { + ): UpdateBundleProject { let updatePending = true; return { kind: InvalidatedProjectKind.UpdateBundle, @@ -777,7 +1080,7 @@ namespace ts { !isIncrementalCompilation(config.options); } - function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { + function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { if (!state.projectPendingBuild.size) return undefined; const { options, projectPendingBuild } = state; @@ -925,153 +1228,6 @@ namespace ts { moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(config.options); } - function buildSingleProject( - state: SolutionBuilderState, - proj: ResolvedConfigFileName, - resolvedPath: ResolvedConfigFilePath, - config: ParsedCommandLine, - cancellationToken: CancellationToken | undefined - ): BuildResultFlags { - if (state.options.dry) { - reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, proj); - return BuildResultFlags.Success; - } - - if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, proj); - - if (config.fileNames.length === 0) { - reportAndStoreErrors(state, resolvedPath, config.errors); - // Nothing to build - must be a solution file, basically - return BuildResultFlags.None; - } - - const { host, projectStatus, diagnostics, compilerHost } = state; - state.projectCompilerOptions = config.options; - // Update module resolution cache if needed - updateModuleResolutionCache(state, proj, config); - - // Create program - const program = host.createProgram( - config.fileNames, - config.options, - compilerHost, - getOldProgram(state, resolvedPath, config), - config.errors, - config.projectReferences - ); - - // Don't emit anything in the presence of syntactic errors or options diagnostics - const syntaxDiagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; - if (syntaxDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - program, - syntaxDiagnostics, - BuildResultFlags.SyntaxErrors, - "Syntactic" - ); - } - - // Same as above but now for semantic diagnostics - const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); - if (semanticDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - program, - semanticDiagnostics, - BuildResultFlags.TypeErrors, - "Semantic" - ); - } - - // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly - program.backupState(); - let declDiagnostics: Diagnostic[] | undefined; - const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); - const outputFiles: OutputFile[] = []; - emitFilesAndReportErrors( - program, - reportDeclarationDiagnostics, - /*writeFileName*/ undefined, - /*reportSummary*/ undefined, - (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), - cancellationToken - ); - // Don't emit .d.ts if there are decl file errors - if (declDiagnostics) { - program.restoreState(); - return buildErrors( - state, - resolvedPath, - program, - declDiagnostics, - BuildResultFlags.DeclarationEmitErrors, - "Declaration file" - ); - } - - // Actual Emit - let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; - let newestDeclarationFileContentChangedTime = minimumDate; - let anyDtsChanged = false; - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - let priorChangeTime: Date | undefined; - if (!anyDtsChanged && isDeclarationFile(name)) { - // Check for unchanged .d.ts files - if (host.fileExists(name) && state.readFileWithCache(name) === text) { - priorChangeTime = host.getModifiedTime(name); - } - else { - resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - anyDtsChanged = true; - } - } - - emittedOutputs.set(toPath(state, name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - if (priorChangeTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - } - }); - - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - program, - emitDiagnostics, - BuildResultFlags.EmitErrors, - "Emit" - ); - } - - if (state.writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(state, config, name)); - listFiles(program, state.writeFileName); - } - - // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()) - }); - afterProgramCreate(state, resolvedPath, program); - state.projectCompilerOptions = state.baseCompilerOptions; - return resultFlags; - } - function updateBundle( state: SolutionBuilderState, proj: ResolvedConfigFileName, diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 4f974add57d80..9378478664f14 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -116,7 +116,7 @@ namespace ts { getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; getConfigFileParsingDiagnostics(): ReadonlyArray; - emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult; + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; } export function listFiles(program: ProgramToEmitFilesAndReportErrors, writeFileName: (s: string) => void) { @@ -136,7 +136,9 @@ namespace ts { writeFileName?: (s: string) => void, reportSummary?: ReportEmitErrorSummary, writeFile?: WriteFileCallback, - cancellationToken?: CancellationToken + cancellationToken?: CancellationToken, + emitOnlyDtsFiles?: boolean, + customTransformers?: CustomTransformers ) { // First get and report any syntactic errors. const diagnostics = program.getConfigFileParsingDiagnostics().slice(); @@ -155,7 +157,8 @@ namespace ts { } // Emit and report any errors we ran into. - const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken); + const emitResult = program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + const { emittedFiles, diagnostics: emitDiagnostics } = emitResult; addRange(diagnostics, emitDiagnostics); sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic); @@ -172,7 +175,34 @@ namespace ts { reportSummary(getErrorCountForSummary(diagnostics)); } - if (emitSkipped && diagnostics.length > 0) { + return { + emitResult, + diagnostics, + }; + } + + export function emitFilesAndReportErrorsAndGetExitStatus( + program: ProgramToEmitFilesAndReportErrors, + reportDiagnostic: DiagnosticReporter, + writeFileName?: (s: string) => void, + reportSummary?: ReportEmitErrorSummary, + writeFile?: WriteFileCallback, + cancellationToken?: CancellationToken, + emitOnlyDtsFiles?: boolean, + customTransformers?: CustomTransformers + ) { + const { emitResult, diagnostics } = emitFilesAndReportErrors( + program, + reportDiagnostic, + writeFileName, + reportSummary, + writeFile, + cancellationToken, + emitOnlyDtsFiles, + customTransformers + ); + + if (emitResult.emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; } @@ -395,7 +425,7 @@ namespace ts { const system = input.system || sys; const host = input.host || (input.host = createIncrementalCompilerHost(input.options, system)); const builderProgram = createIncrementalProgram(input); - const exitStatus = emitFilesAndReportErrors( + const exitStatus = emitFilesAndReportErrorsAndGetExitStatus( builderProgram, input.reportDiagnostic || createDiagnosticReporter(system), s => host.trace && host.trace(s), diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 47814a1f68b88..6a340109238b8 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -246,7 +246,7 @@ namespace ts { configFileParsingDiagnostics }; const program = createProgram(programOptions); - const exitStatus = emitFilesAndReportErrors( + const exitStatus = emitFilesAndReportErrorsAndGetExitStatus( program, reportDiagnostic, s => sys.write(s + sys.newLine), From 8c489bfdf8bd141089c45d888c4ad4b02da53b4b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 9 May 2019 15:30:16 -0700 Subject: [PATCH 33/41] UpdateBundleProject to contain emit method --- src/compiler/emitter.ts | 9 +- src/compiler/tsbuild.ts | 378 +++++++++++++++++++++------------------- 2 files changed, 203 insertions(+), 184 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 8f91e3d25c163..2b1e1309609ae 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -638,7 +638,12 @@ namespace ts { } /*@internal*/ - export function emitUsingBuildInfo(config: ParsedCommandLine, host: EmitUsingBuildInfoHost, getCommandLine: (ref: ProjectReference) => ParsedCommandLine | undefined): EmitUsingBuildInfoResult { + export function emitUsingBuildInfo( + config: ParsedCommandLine, + host: EmitUsingBuildInfoHost, + getCommandLine: (ref: ProjectReference) => ParsedCommandLine | undefined, + customTransformers?: CustomTransformers + ): EmitUsingBuildInfoResult { const { buildInfoPath, jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath } = getOutputPathsForBundle(config.options, /*forceDtsPaths*/ false); const buildInfoText = host.readFile(Debug.assertDefined(buildInfoPath)); if (!buildInfoText) return buildInfoPath!; @@ -723,7 +728,7 @@ namespace ts { useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), getProgramBuildInfo: returnUndefined }; - emitFiles(notImplementedResolver, emitHost, /*targetSourceFile*/ undefined, /*emitOnlyDtsFiles*/ false, getTransformers(config.options)); + emitFiles(notImplementedResolver, emitHost, /*targetSourceFile*/ undefined, /*emitOnlyDtsFiles*/ false, getTransformers(config.options, customTransformers)); return outputFiles; } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 5498c71098354..d66874f3a4172 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -709,7 +709,7 @@ namespace ts { interface UpdateBundleProject extends InvalidatedProjectBase { readonly kind: InvalidatedProjectKind.UpdateBundle; - updateBundle(): BuildResultFlags | BuildInvalidedProject; + emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; } type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; @@ -733,91 +733,109 @@ namespace ts { }; } - function createBuildInvalidedProject( + function createBuildOrUpdateInvalidedProject( + kind: InvalidatedProjectKind.Build | InvalidatedProjectKind.UpdateBundle, state: SolutionBuilderState, project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, config: ParsedCommandLine, - buildOrder: readonly ResolvedConfigFileName[] - ): BuildInvalidedProject { + buildOrder: readonly ResolvedConfigFileName[], + ): BuildInvalidedProject | UpdateBundleProject { enum Step { CreateProgram, SyntaxDiagnostics, SemanticDiagnostics, Emit, + EmitBundle, + BuildInvalidatedProjectOfBundle, QueueReferencingProjects, Done } - let step = Step.CreateProgram; + let step = kind === InvalidatedProjectKind.Build ? Step.CreateProgram : Step.EmitBundle; let program: T | undefined; let buildResult: BuildResultFlags | undefined; + let invalidatedProjectOfBundle: BuildInvalidedProject | undefined; - return { - kind: InvalidatedProjectKind.Build, - project, - projectPath, - getBuilderProgram: () => withProgramOrUndefined(identity), - getProgram: () => - withProgramOrUndefined( - program => program.getProgramOrUndefined() - ), - getCompilerOptions: () => config.options, - getSourceFile: fileName => - withProgramOrUndefined( - program => program.getSourceFile(fileName) - ), - getSourceFiles: () => - withProgramOrEmptyArray( - program => program.getSourceFiles() - ), - getOptionsDiagnostics: cancellationToken => - withProgramOrEmptyArray( - program => program.getOptionsDiagnostics(cancellationToken) - ), - getGlobalDiagnostics: cancellationToken => - withProgramOrEmptyArray( - program => program.getGlobalDiagnostics(cancellationToken) - ), - getConfigFileParsingDiagnostics: () => - withProgramOrEmptyArray( - program => program.getConfigFileParsingDiagnostics() - ), - getSyntacticDiagnostics: (sourceFile, cancellationToken) => - withProgramOrEmptyArray( - program => program.getSyntacticDiagnostics(sourceFile, cancellationToken) - ), - getAllDependencies: sourceFile => - withProgramOrEmptyArray( - program => program.getAllDependencies(sourceFile) - ), - getSemanticDiagnostics: (sourceFile, cancellationToken) => - withProgramOrEmptyArray( - program => program.getSemanticDiagnostics(sourceFile, cancellationToken) - ), - getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) => - withProgramOrUndefined( - program => - ((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) && - (program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile) - ), - emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => { - if (targetSourceFile || emitOnlyDtsFiles) { - return withProgramOrUndefined( - program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) - ); + return kind === InvalidatedProjectKind.Build ? + { + kind, + project, + projectPath, + getBuilderProgram: () => withProgramOrUndefined(identity), + getProgram: () => + withProgramOrUndefined( + program => program.getProgramOrUndefined() + ), + getCompilerOptions: () => config.options, + getSourceFile: fileName => + withProgramOrUndefined( + program => program.getSourceFile(fileName) + ), + getSourceFiles: () => + withProgramOrEmptyArray( + program => program.getSourceFiles() + ), + getOptionsDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getOptionsDiagnostics(cancellationToken) + ), + getGlobalDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getGlobalDiagnostics(cancellationToken) + ), + getConfigFileParsingDiagnostics: () => + withProgramOrEmptyArray( + program => program.getConfigFileParsingDiagnostics() + ), + getSyntacticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSyntacticDiagnostics(sourceFile, cancellationToken) + ), + getAllDependencies: sourceFile => + withProgramOrEmptyArray( + program => program.getAllDependencies(sourceFile) + ), + getSemanticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSemanticDiagnostics(sourceFile, cancellationToken) + ), + getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) => + withProgramOrUndefined( + program => + ((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) && + (program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile) + ), + emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => { + if (targetSourceFile || emitOnlyDtsFiles) { + return withProgramOrUndefined( + program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) + ); + } + executeSteps(Step.SemanticDiagnostics, cancellationToken); + if (step !== Step.Emit) return undefined; + return emit(writeFile, cancellationToken, customTransformers); + }, + getCurrentDirectory: () => state.currentDirectory, + done: cancellationToken => { + executeSteps(Step.Done, cancellationToken); + state.projectPendingBuild.delete(projectPath); } - executeSteps(Step.SemanticDiagnostics, cancellationToken); - if (step !== Step.Emit) return undefined; - return emit(writeFile, cancellationToken, customTransformers); - }, - getCurrentDirectory: () => state.currentDirectory, - done: cancellationToken => { - executeSteps(Step.Done, cancellationToken); - state.projectPendingBuild.delete(projectPath); - } - }; + } : + { + kind, + project, + projectPath, + 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); + } + }; function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { executeSteps(Step.CreateProgram); @@ -941,7 +959,7 @@ namespace ts { } // Actual Emit - const { host, compilerHost, diagnostics, projectStatus } = state; + const { host, compilerHost } = state; let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; let newestDeclarationFileContentChangedTime = minimumDate; let anyDtsChanged = false; @@ -967,6 +985,25 @@ namespace ts { } }); + finishEmit( + emitterDiagnostics, + emittedOutputs, + newestDeclarationFileContentChangedTime, + /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ anyDtsChanged, + outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()), + resultFlags + ); + return emitResult; + } + + function finishEmit( + emitterDiagnostics: DiagnosticCollection, + emittedOutputs: FileMap, + priorNewestUpdateTime: Date, + newestDeclarationFileContentChangedTimeIsMaximumDate: boolean, + oldestOutputFileName: string, + resultFlags: BuildResultFlags + ) { const emitDiagnostics = emitterDiagnostics.getDiagnostics(); if (emitDiagnostics.length) { buildResult = buildErrors( @@ -978,27 +1015,87 @@ namespace ts { "Emit" ); step = Step.QueueReferencingProjects; - return emitResult; + return emitDiagnostics; } if (state.writeFileName) { emittedOutputs.forEach(name => listEmittedFile(state, config, name)); - listFiles(program!, state.writeFileName); + if (program) listFiles(program, state.writeFileName); } // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - diagnostics.delete(projectPath); - projectStatus.set(projectPath, { + const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, priorNewestUpdateTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + state.diagnostics.delete(projectPath); + state.projectStatus.set(projectPath, { type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()) + newestDeclarationFileContentChangedTime: newestDeclarationFileContentChangedTimeIsMaximumDate ? + maximumDate : + newestDeclarationFileContentChangedTime, + oldestOutputFileName }); - afterProgramCreate(state, projectPath, program!); + if (program) afterProgramCreate(state, projectPath, program); state.projectCompilerOptions = state.baseCompilerOptions; - step++; + step = Step.QueueReferencingProjects; buildResult = resultFlags; - return emitResult; + return emitDiagnostics; + } + + function emitBundle(writeFileCallback?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined { + Debug.assert(kind === InvalidatedProjectKind.UpdateBundle); + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, project); + buildResult = BuildResultFlags.Success; + step = Step.QueueReferencingProjects; + return undefined; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, project); + + // Update js, and source map + const { compilerHost } = state; + state.projectCompilerOptions = config.options; + const outputFiles = emitUsingBuildInfo( + config, + compilerHost, + ref => { + const refName = resolveProjectName(state, ref.path); + return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName)); + }, + customTransformers + ); + + if (isString(outputFiles)) { + reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, project, relName(state, outputFiles)); + step = Step.BuildInvalidatedProjectOfBundle; + return invalidatedProjectOfBundle = createBuildOrUpdateInvalidedProject( + InvalidatedProjectKind.Build, + state, + project, + projectPath, + projectIndex, + config, + buildOrder + ) as BuildInvalidedProject; + } + + // Actual Emit + Debug.assert(!!outputFiles.length); + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + emittedOutputs.set(toPath(state, name), name); + writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + }); + + const emitDiagnostics = finishEmit( + emitterDiagnostics, + emittedOutputs, + minimumDate, + /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ false, + outputFiles[0].name, + BuildResultFlags.DeclarationOutputUnchanged + ); + return { emitSkipped: false, diagnostics: emitDiagnostics }; } function executeSteps(till: Step, cancellationToken?: CancellationToken) { @@ -1021,6 +1118,15 @@ namespace ts { emit(/*writeFileCallback*/ undefined, cancellationToken); break; + case Step.EmitBundle: + emitBundle(); + break; + + case Step.BuildInvalidatedProjectOfBundle: + Debug.assertDefined(invalidatedProjectOfBundle).done(cancellationToken); + step = Step.Done; + break; + case Step.QueueReferencingProjects: queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, Debug.assertDefined(buildResult)); step++; @@ -1037,42 +1143,6 @@ namespace ts { } } - function createUpdateBundleProject( - state: SolutionBuilderState, - project: ResolvedConfigFileName, - projectPath: ResolvedConfigFilePath, - projectIndex: number, - config: ParsedCommandLine, - buildOrder: readonly ResolvedConfigFileName[] - ): UpdateBundleProject { - let updatePending = true; - return { - kind: InvalidatedProjectKind.UpdateBundle, - project, - projectPath, - updateBundle: update, - done: cancellationToken => { - if (updatePending) { - const result = update(); - if ((result as BuildInvalidedProject).project) { - return (result as BuildInvalidedProject).done(cancellationToken); - } - } - state.projectPendingBuild.delete(projectPath); - } - }; - - function update() { - const buildResult = updateBundle(state, project, projectPath, config); - if (isString(buildResult)) { - return createBuildInvalidedProject(state, project, projectPath, projectIndex, config, buildOrder); - } - queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, buildResult); - updatePending = false; - return buildResult; - } - } - function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; return config.fileNames.length === 0 || @@ -1149,9 +1219,17 @@ namespace ts { continue; } - return needsBuild(state, status, config) ? - createBuildInvalidedProject(state, project, projectPath, projectIndex, config, buildOrder) : - createUpdateBundleProject(state, project, projectPath, projectIndex, config, buildOrder); + return createBuildOrUpdateInvalidedProject( + needsBuild(state, status, config) ? + InvalidatedProjectKind.Build : + InvalidatedProjectKind.UpdateBundle, + state, + project, + projectPath, + projectIndex, + config, + buildOrder, + ); } return undefined; @@ -1228,70 +1306,6 @@ namespace ts { moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(config.options); } - function updateBundle( - state: SolutionBuilderState, - proj: ResolvedConfigFileName, - resolvedPath: ResolvedConfigFilePath, - config: ParsedCommandLine - ): BuildResultFlags | string { - if (state.options.dry) { - reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); - return BuildResultFlags.Success; - } - - if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, proj); - - // Update js, and source map - const { projectStatus, diagnostics, compilerHost } = state; - state.projectCompilerOptions = config.options; - const outputFiles = emitUsingBuildInfo( - config, - compilerHost, - ref => { - const refName = resolveProjectName(state, ref.path); - return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName)); - }); - if (isString(outputFiles)) { - reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(state, outputFiles)); - return outputFiles; - } - - // Actual Emit - Debug.assert(!!outputFiles.length); - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - emittedOutputs.set(toPath(state, name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - }); - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - return buildErrors( - state, - resolvedPath, - /*program*/ undefined, - emitDiagnostics, - BuildResultFlags.EmitErrors, - "Emit" - ); - } - - if (state.writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(state, config, name)); - } - - // Update timestamps for dts - const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, minimumDate, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles[0].name - }); - state.projectCompilerOptions = state.baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged; - } - function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { // Check tsconfig time const tsconfigTime = state.host.getModifiedTime(configFile) || missingFileModifiedTime; From 97fcea14d51f4187d9373dad235fb3aeee5bf08f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 9 May 2019 13:30:53 -0700 Subject: [PATCH 34/41] Api to get next invalidated project --- src/compiler/tsbuild.ts | 117 ++++++++++-------- src/testRunner/unittests/tsbuild/sample.ts | 12 +- src/testRunner/unittests/tsbuildWatchMode.ts | 4 +- .../reference/api/tsserverlibrary.d.ts | 51 ++++++-- tests/baselines/reference/api/typescript.d.ts | 51 ++++++-- 5 files changed, 164 insertions(+), 71 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index d66874f3a4172..f30800c100d1f 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 056e894bf538a..130ebd9b8eb78 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 f67b575e9795b..54f174356b545 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 df479429c1705..05d823728e424 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 4062969e8cd97..357186f140d3b 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"; From d7c3b5e5d04f2949bb42982fe3a2bdbda60909dc Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 14 May 2019 16:32:46 -0700 Subject: [PATCH 35/41] Add getParsedCommandLine as optional method on SolutionBuilderHost --- src/compiler/tsbuild.ts | 20 +++++++++++++++---- .../reference/api/tsserverlibrary.d.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 1 + 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index f30800c100d1f..99bb27aba2c0d 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -245,6 +245,7 @@ namespace ts { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; deleteFile(fileName: string): void; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here reportSolutionBuilderStatus: DiagnosticReporter; @@ -493,10 +494,17 @@ namespace ts { } let diagnostic: Diagnostic | undefined; - const { parseConfigFileHost, baseCompilerOptions, extendedConfigCache } = state; - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - const parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; + const { parseConfigFileHost, baseCompilerOptions, extendedConfigCache, host } = state; + let parsed: ParsedCommandLine | undefined; + if (host.getParsedCommandLine) { + parsed = host.getParsedCommandLine(configFileName); + if (!parsed) diagnostic = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + } + else { + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; + parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; + } configFileCache.set(configFilePath, parsed || diagnostic!); return parsed; } @@ -1730,6 +1738,10 @@ namespace ts { } function invalidateProject(state: SolutionBuilderState, resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + // If host implements getParsedCommandLine, we cant get list of files from parseConfigFileHost + if (state.host.getParsedCommandLine && reloadLevel === ConfigFileProgramReloadLevel.Partial) { + reloadLevel = ConfigFileProgramReloadLevel.Full; + } if (reloadLevel === ConfigFileProgramReloadLevel.Full) { state.configFileCache.delete(resolved); state.buildOrder = undefined; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index b9d1f1ffab705..01cef9552dbe9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4591,6 +4591,7 @@ declare namespace ts { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; deleteFile(fileName: string): void; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; reportDiagnostic: DiagnosticReporter; reportSolutionBuilderStatus: DiagnosticReporter; afterProgramEmitAndDiagnostics?(program: T): void; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index a140501c41165..ce0e7b81c82be 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4591,6 +4591,7 @@ declare namespace ts { getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; deleteFile(fileName: string): void; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; reportDiagnostic: DiagnosticReporter; reportSolutionBuilderStatus: DiagnosticReporter; afterProgramEmitAndDiagnostics?(program: T): void; From 89d1475fde16da386f065e30b5ee7746e383379b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 15 May 2019 09:14:23 -0700 Subject: [PATCH 36/41] Add writeFileCallbacks to done method and also on host --- src/compiler/tsbuild.ts | 19 +++++++++++++------ .../reference/api/tsserverlibrary.d.ts | 8 +++++++- tests/baselines/reference/api/typescript.d.ts | 8 +++++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 99bb27aba2c0d..77de45bee011b 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -242,6 +242,13 @@ namespace ts { export type ReportEmitErrorSummary = (errorCount: number) => void; export interface SolutionBuilderHostBase extends ProgramHost { + createDirectory?(path: string): void; + /** + * Should provide create directory and writeFile if done of invalidatedProjects is not invoked with + * writeFileCallback + */ + writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; + getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; deleteFile(fileName: string): void; @@ -673,7 +680,7 @@ namespace ts { /** * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly */ - done(cancellationToken?: CancellationToken): ExitStatus; + done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus; getCompilerOptions(): CompilerOptions; getCurrentDirectory(): string; } @@ -861,8 +868,8 @@ namespace ts { done, }; - function done(cancellationToken?: CancellationToken) { - executeSteps(Step.Done, cancellationToken); + function done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { + executeSteps(Step.Done, cancellationToken, writeFile, customTransformers); return doneInvalidatedProject(state, projectPath); } @@ -1127,7 +1134,7 @@ namespace ts { return { emitSkipped: false, diagnostics: emitDiagnostics }; } - function executeSteps(till: Step, cancellationToken?: CancellationToken) { + function executeSteps(till: Step, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { while (step <= till && step < Step.Done) { const currentStep = step; switch (step) { @@ -1144,11 +1151,11 @@ namespace ts { break; case Step.Emit: - emit(/*writeFileCallback*/ undefined, cancellationToken); + emit(writeFile, cancellationToken, customTransformers); break; case Step.EmitBundle: - emitBundle(); + emitBundle(writeFile, customTransformers); break; case Step.BuildInvalidatedProjectOfBundle: diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 01cef9552dbe9..05f8195b992f4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4588,6 +4588,12 @@ declare namespace ts { } type ReportEmitErrorSummary = (errorCount: number) => void; interface SolutionBuilderHostBase extends ProgramHost { + createDirectory?(path: string): void; + /** + * Should provide create directory and writeFile if done of invalidatedProjects is not invoked with + * writeFileCallback + */ + writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; deleteFile(fileName: string): void; @@ -4625,7 +4631,7 @@ declare namespace ts { /** * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly */ - done(cancellationToken?: CancellationToken): ExitStatus; + done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus; getCompilerOptions(): CompilerOptions; getCurrentDirectory(): string; } diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index ce0e7b81c82be..86a37c9f498ae 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4588,6 +4588,12 @@ declare namespace ts { } type ReportEmitErrorSummary = (errorCount: number) => void; interface SolutionBuilderHostBase extends ProgramHost { + createDirectory?(path: string): void; + /** + * Should provide create directory and writeFile if done of invalidatedProjects is not invoked with + * writeFileCallback + */ + writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; getModifiedTime(fileName: string): Date | undefined; setModifiedTime(fileName: string, date: Date): void; deleteFile(fileName: string): void; @@ -4625,7 +4631,7 @@ declare namespace ts { /** * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly */ - done(cancellationToken?: CancellationToken): ExitStatus; + done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus; getCompilerOptions(): CompilerOptions; getCurrentDirectory(): string; } From 629bc0c04d7126e3481a23742a9688a422ec69e3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 15 May 2019 09:51:02 -0700 Subject: [PATCH 37/41] Always emit tsbuild info if path says so (irrespecitive of if there exists bundle and project) --- src/compiler/emitter.ts | 1 - src/compiler/tsbuild.ts | 2 +- src/harness/fakes.ts | 7 ++++- src/testRunner/unittests/tsbuild/sample.ts | 35 ++++++++++++++++++++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 6a339e265943b..e6e3c44afcd6f 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -284,7 +284,6 @@ namespace ts { // Write build information if applicable if (!buildInfoPath || targetSourceFile || emitSkipped) return; const program = host.getProgramBuildInfo(); - if (!bundle && !program) return; if (host.isEmitBlocked(buildInfoPath) || compilerOptions.noEmit) { emitSkipped = true; return; diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 77de45bee011b..43b1e24a573db 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1524,7 +1524,7 @@ namespace ts { if (buildInfoPath) { const value = state.readFileWithCache(buildInfoPath); const buildInfo = value && getBuildInfo(value); - if (buildInfo && buildInfo.version !== version) { + if (buildInfo && (buildInfo.bundle || buildInfo.program) && buildInfo.version !== version) { return { type: UpToDateStatusType.TsVersionOutputOfDate, version: buildInfo.version diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index c3a3bb621ce59..31e64e2c93129 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -418,7 +418,12 @@ namespace fakes { export const version = "FakeTSVersion"; export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost { - createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram; + createProgram: ts.CreateProgram; + + constructor(sys: System | vfs.FileSystem, options?: ts.CompilerOptions, setParentNodes?: boolean, createProgram?: ts.CreateProgram) { + super(sys, options, setParentNodes); + this.createProgram = createProgram || ts.createEmitAndSemanticDiagnosticsBuilderProgram; + } readFile(path: string) { const value = super.readFile(path); diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 130ebd9b8eb78..25ad761c8f18b 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -2,9 +2,9 @@ namespace ts { describe("unittests:: tsbuild:: on 'sample1' project", () => { let projFs: vfs.FileSystem; const { time, tick } = getTime(); - const testsOutputs = ["/src/tests/index.js"]; - const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts"]; - const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map"]; + const testsOutputs = ["/src/tests/index.js", "/src/tests/index.d.ts", "/src/tests/tsconfig.tsbuildinfo"]; + const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts", "/src/logic/tsconfig.tsbuildinfo"]; + const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/core/tsconfig.tsbuildinfo"]; const allExpectedOutputs = [...testsOutputs, ...logicOutputs, ...coreOutputs]; before(() => { @@ -272,6 +272,35 @@ namespace ts { ); }); + it("does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs, /*options*/ undefined, /*setParentNodes*/ undefined, createAbstractBuilder); + let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + builder.build(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], + [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], + [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"], + [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] + ); + verifyOutputsPresent(fs, allExpectedOutputs); + + host.clearDiagnostics(); + tick(); + builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + changeCompilerVersion(host); + builder.build(); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/logic/tsconfig.json", "src/logic/index.ts", "src/logic/index.js"], + [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/tests/tsconfig.json", "src/tests/index.ts", "src/tests/index.js"] + ); + }); + it("rebuilds from start if --f is passed", () => { const { host, builder } = initializeWithBuild({ force: true }); builder.build(); From 0cb980dd6e009c28de63b79f24bd4f47d4bf33f2 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 15 May 2019 14:56:06 -0700 Subject: [PATCH 38/41] Add api to build referenced projects --- src/compiler/tsbuild.ts | 39 +++++++++++++------ src/testRunner/unittests/tsbuild/sample.ts | 16 ++++++++ .../reference/api/tsserverlibrary.d.ts | 2 + tests/baselines/reference/api/typescript.d.ts | 2 + 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 43b1e24a573db..8a43dd446f2b7 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -275,6 +275,8 @@ namespace ts { export interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + cleanReferences(project?: string): ExitStatus; getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; // Currently used for testing but can be made public if needed: @@ -565,7 +567,7 @@ namespace ts { (state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)))); } - function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined) { + function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined) { const resolvedProject = project && resolveProjectName(state, project); if (resolvedProject) { const projectPath = toResolvedConfigFilePath(state, resolvedProject); @@ -575,7 +577,10 @@ namespace ts { ); if (projectIndex === -1) return undefined; } - return resolvedProject ? createBuildOrder(state, [resolvedProject]) : getBuildOrder(state); + const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) : getBuildOrder(state); + Debug.assert(!onlyReferences || resolvedProject !== undefined); + Debug.assert(!onlyReferences || buildOrder[buildOrder.length - 1] === resolvedProject); + return onlyReferences ? buildOrder.slice(0, buildOrder.length - 1) : buildOrder; } function enableCache(state: SolutionBuilderState) { @@ -653,7 +658,6 @@ namespace ts { if (state.options.watch) { reportWatchStatus(state, Diagnostics.Starting_compilation_in_watch_mode); } enableCache(state); const buildOrder = getBuildOrder(state); - reportBuildQueue(state, buildOrder); buildOrder.forEach(configFileName => state.projectPendingBuild.set( toResolvedConfigFilePath(state, configFileName), @@ -1186,7 +1190,11 @@ namespace ts { !isIncrementalCompilation(config.options); } - function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { + function getNextInvalidatedProject( + state: SolutionBuilderState, + buildOrder: readonly ResolvedConfigFileName[], + reportQueue: boolean + ): InvalidatedProject | undefined { if (!state.projectPendingBuild.size) return undefined; if (state.currentInvalidatedProject) { // Only if same buildOrder the currentInvalidated project can be sent again @@ -1202,6 +1210,11 @@ namespace ts { const reloadLevel = state.projectPendingBuild.get(projectPath); if (reloadLevel === undefined) continue; + if (reportQueue) { + reportQueue = false; + reportBuildQueue(state, buildOrder); + } + const config = parseConfigFile(state, project, projectPath); if (!config) { reportParseConfigFileDiagnostic(state, projectPath); @@ -1678,17 +1691,19 @@ namespace ts { } } - function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken): ExitStatus { - const buildOrder = getBuildOrderFor(state, project); + function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken, onlyReferences?: boolean): ExitStatus { + const buildOrder = getBuildOrderFor(state, project, onlyReferences); if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; setupInitialBuild(state, cancellationToken); + let reportQueue = true; let successfulProjects = 0; let errorProjects = 0; while (true) { - const invalidatedProject = getNextInvalidatedProject(state, buildOrder); + const invalidatedProject = getNextInvalidatedProject(state, buildOrder, reportQueue); if (!invalidatedProject) break; + reportQueue = false; invalidatedProject.done(cancellationToken); if (state.diagnostics.has(invalidatedProject.projectPath)) { errorProjects++; @@ -1709,8 +1724,8 @@ namespace ts { ExitStatus.Success; } - function clean(state: SolutionBuilderState, project?: string) { - const buildOrder = getBuildOrderFor(state, project); + function clean(state: SolutionBuilderState, project?: string, onlyReferences?: boolean) { + const buildOrder = getBuildOrderFor(state, project, onlyReferences); if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; const { options, host } = state; @@ -1783,7 +1798,7 @@ namespace ts { state.projectErrorsReported.clear(); reportWatchStatus(state, Diagnostics.File_change_detected_Starting_incremental_compilation); } - const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); + const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state), /*reportQueue*/ false); if (invalidatedProject) { invalidatedProject.done(); if (state.projectPendingBuild.size) { @@ -1923,9 +1938,11 @@ namespace ts { return { build: (project, cancellationToken) => build(state, project, cancellationToken), clean: project => clean(state, project), + buildReferences: (project, cancellationToken) => build(state, project, cancellationToken, /*onlyReferences*/ true), + cleanReferences: project => clean(state, project, /*onlyReferences*/ true), getNextInvalidatedProject: cancellationToken => { setupInitialBuild(state, cancellationToken); - return getNextInvalidatedProject(state, getBuildOrder(state)); + return getNextInvalidatedProject(state, getBuildOrder(state), /*reportQueue*/ false); }, getBuildOrder: () => getBuildOrder(state), getUpToDateStatusOfProject: project => { diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 25ad761c8f18b..df2fb62db25b6 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -417,6 +417,22 @@ namespace ts { verifyOutputsAbsent(fs, absentOutputs); } }); + + it("building using buildReferencedProject", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + builder.buildReferences("/src/tests"); + host.assertDiagnosticMessages( + getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json"), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], + [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], + [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], + ); + verifyOutputsPresent(fs, [...coreOutputs, ...logicOutputs]); + verifyOutputsAbsent(fs, testsOutputs); + }); }); describe("downstream-blocked compilations", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 05f8195b992f4..8e78922e63617 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4610,6 +4610,8 @@ declare namespace ts { interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + cleanReferences(project?: string): ExitStatus; getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; } /** diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 86a37c9f498ae..3d0df0fabec83 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4610,6 +4610,8 @@ declare namespace ts { interface SolutionBuilder { build(project?: string, cancellationToken?: CancellationToken): ExitStatus; clean(project?: string): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + cleanReferences(project?: string): ExitStatus; getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; } /** From ec4ea0e4748930748c8d76f57708f6a44f92e17b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 16 May 2019 11:21:09 -0700 Subject: [PATCH 39/41] Watch only built projects --- src/compiler/tsbuild.ts | 22 +++++++++++--------- src/testRunner/unittests/tsbuildWatchMode.ts | 22 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 8a43dd446f2b7..1c1c093bc7a1f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -569,15 +569,16 @@ namespace ts { function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined) { const resolvedProject = project && resolveProjectName(state, project); + const buildOrderFromState = getBuildOrder(state); if (resolvedProject) { const projectPath = toResolvedConfigFilePath(state, resolvedProject); const projectIndex = findIndex( - getBuildOrder(state), + buildOrderFromState, configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath ); if (projectIndex === -1) return undefined; } - const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) : getBuildOrder(state); + const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) : buildOrderFromState; Debug.assert(!onlyReferences || resolvedProject !== undefined); Debug.assert(!onlyReferences || buildOrder[buildOrder.length - 1] === resolvedProject); return onlyReferences ? buildOrder.slice(0, buildOrder.length - 1) : buildOrder; @@ -1714,8 +1715,8 @@ namespace ts { } disableCache(state); - reportErrorSummary(state); - startWatching(state); + reportErrorSummary(state, buildOrder); + startWatching(state, buildOrder); return errorProjects ? successfulProjects ? @@ -1798,7 +1799,8 @@ namespace ts { state.projectErrorsReported.clear(); reportWatchStatus(state, Diagnostics.File_change_detected_Starting_incremental_compilation); } - const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state), /*reportQueue*/ false); + const buildOrder = getBuildOrder(state); + const invalidatedProject = getNextInvalidatedProject(state, buildOrder, /*reportQueue*/ false); if (invalidatedProject) { invalidatedProject.done(); if (state.projectPendingBuild.size) { @@ -1810,7 +1812,7 @@ namespace ts { } } disableCache(state); - reportErrorSummary(state); + reportErrorSummary(state, buildOrder); } function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { @@ -1908,10 +1910,10 @@ namespace ts { ); } - function startWatching(state: SolutionBuilderState) { + function startWatching(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]) { if (!state.watchAllProjectsPending) return; state.watchAllProjectsPending = false; - for (const resolved of getBuildOrder(state)) { + for (const resolved of buildOrder) { const resolvedPath = toResolvedConfigFilePath(state, resolved); // Watch this file watchConfigFile(state, resolved, resolvedPath); @@ -1985,12 +1987,12 @@ namespace ts { reportAndStoreErrors(state, proj, [state.configFileCache.get(proj) as Diagnostic]); } - function reportErrorSummary(state: SolutionBuilderState) { + function reportErrorSummary(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]) { if (!state.needsSummary || (!state.watch && !state.host.reportErrorSummary)) return; state.needsSummary = false; const { diagnostics } = state; // Report errors from the other projects - getBuildOrder(state).forEach(project => { + buildOrder.forEach(project => { const projectPath = toResolvedConfigFilePath(state, project); if (!state.projectErrorsReported.has(projectPath)) { reportErrors(state, diagnostics.get(projectPath) || emptyArray); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 54f174356b545..a630e28f1d893 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -143,6 +143,28 @@ namespace ts.tscWatch { createSolutionInWatchMode(allFiles); }); + it("verify building references watches only those projects", () => { + const system = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`], { watch: true }); + solutionBuilder.buildReferences(`${project}/${SubProject.tests}`); + + checkWatchedFiles(system, testProjectExpectedWatchedFiles.slice(0, testProjectExpectedWatchedFiles.length - tests.length)); + checkWatchedDirectories(system, emptyArray, /*recursive*/ false); + checkWatchedDirectories(system, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true); + + checkOutputErrorsInitial(system, emptyArray); + const testOutput = getOutputStamps(system, SubProject.tests, "index"); + const outputFileStamps = getOutputFileStamps(system); + for (const stamp of outputFileStamps.slice(0, outputFileStamps.length - testOutput.length)) { + assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); + } + for (const stamp of testOutput) { + assert.isUndefined(stamp[1], `${stamp[0]} expected to be missing`); + } + return system; + }); + describe("validates the changes and watched files", () => { const newFileWithoutExtension = "newFile"; const newFile: File = { From 138f757709624fbd06a34a97b9d514b0c9f40508 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 16 May 2019 12:50:17 -0700 Subject: [PATCH 40/41] Fix the test since tsbuildinfo is now always emitted (629bc0c) --- src/testRunner/unittests/config/projectReferences.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testRunner/unittests/config/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts index 3acffd7a8c6ae..26ed746d27919 100644 --- a/src/testRunner/unittests/config/projectReferences.ts +++ b/src/testRunner/unittests/config/projectReferences.ts @@ -325,7 +325,7 @@ namespace ts { }; testProjectReferences(spec, "/alpha/tsconfig.json", (program, host) => { program.emit(); - assert.deepEqual(host.outputs.map(e => e.file).sort(), ["/alpha/bin/src/a.d.ts", "/alpha/bin/src/a.js"]); + assert.deepEqual(host.outputs.map(e => e.file).sort(), ["/alpha/bin/src/a.d.ts", "/alpha/bin/src/a.js", "/alpha/bin/tsconfig.tsbuildinfo"]); }); }); }); From 098c9008b88b233819d063a93d5190823eb8c6f8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 16 May 2019 14:49:53 -0700 Subject: [PATCH 41/41] Make more build options internal which correspond to internal compiler options Also fix return type of readBuilderProgram --- src/compiler/builder.ts | 2 +- src/compiler/tsbuild.ts | 8 ++++---- tests/baselines/reference/api/tsserverlibrary.d.ts | 6 +----- tests/baselines/reference/api/typescript.d.ts | 6 +----- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 7190407dd8621..d4c2132d36baa 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -993,7 +993,7 @@ namespace ts { return map; } - export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo): EmitAndSemanticDiagnosticsBuilderProgram & SemanticDiagnosticsBuilderProgram { + export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo): EmitAndSemanticDiagnosticsBuilderProgram { const fileInfos = createMapFromTemplate(program.fileInfos); const state: ReusableBuilderProgramState = { fileInfos, diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 1c1c093bc7a1f..94718a1b3680f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -163,10 +163,10 @@ namespace ts { /*@internal*/ watch?: boolean; /*@internal*/ help?: boolean; - preserveWatchOutput?: boolean; - listEmittedFiles?: boolean; - listFiles?: boolean; - pretty?: boolean; + /*@internal*/ preserveWatchOutput?: boolean; + /*@internal*/ listEmittedFiles?: boolean; + /*@internal*/ listFiles?: boolean; + /*@internal*/ pretty?: boolean; incremental?: boolean; traceResolution?: boolean; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8e78922e63617..0351c6b343aca 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4449,7 +4449,7 @@ declare namespace ts { function createAbstractBuilder(rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray): BuilderProgram; } declare namespace ts { - function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined): (EmitAndSemanticDiagnosticsBuilderProgram & SemanticDiagnosticsBuilderProgram) | undefined; + function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined): EmitAndSemanticDiagnosticsBuilderProgram | undefined; function createIncrementalCompilerHost(options: CompilerOptions, system?: System): CompilerHost; interface IncrementalProgramOptions { rootNames: ReadonlyArray; @@ -4578,10 +4578,6 @@ declare namespace ts { dry?: boolean; force?: boolean; verbose?: boolean; - preserveWatchOutput?: boolean; - listEmittedFiles?: boolean; - listFiles?: boolean; - pretty?: boolean; incremental?: boolean; traceResolution?: boolean; [option: string]: CompilerOptionsValue | undefined; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 3d0df0fabec83..9becdad6aa097 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4449,7 +4449,7 @@ declare namespace ts { function createAbstractBuilder(rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: ReadonlyArray, projectReferences?: ReadonlyArray): BuilderProgram; } declare namespace ts { - function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined): (EmitAndSemanticDiagnosticsBuilderProgram & SemanticDiagnosticsBuilderProgram) | undefined; + function readBuilderProgram(compilerOptions: CompilerOptions, readFile: (path: string) => string | undefined): EmitAndSemanticDiagnosticsBuilderProgram | undefined; function createIncrementalCompilerHost(options: CompilerOptions, system?: System): CompilerHost; interface IncrementalProgramOptions { rootNames: ReadonlyArray; @@ -4578,10 +4578,6 @@ declare namespace ts { dry?: boolean; force?: boolean; verbose?: boolean; - preserveWatchOutput?: boolean; - listEmittedFiles?: boolean; - listFiles?: boolean; - pretty?: boolean; incremental?: boolean; traceResolution?: boolean; [option: string]: CompilerOptionsValue | undefined;