diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 78058bf5a5341..d4c2132d36baa 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; @@ -997,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, @@ -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/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index fbc6d8d6fea1d..2a70ecffed5a6 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/emitter.ts b/src/compiler/emitter.ts index d335ceb1b6037..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; @@ -638,7 +637,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 +727,12 @@ namespace ts { useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), getProgramBuildInfo: returnUndefined }; - emitFiles(notImplementedResolver, emitHost, /*targetSourceFile*/ undefined, getTransformers(config.options), /*emitOnlyDtsFiles*/ false); + emitFiles( + notImplementedResolver, + emitHost, + /*targetSourceFile*/ undefined, + getTransformers(config.options, customTransformers) + ); return outputFiles; } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index a5eae8922da30..94718a1b3680f 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -1,64 +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; - } - - interface DependencyGraph { - buildQueue: ResolvedConfigFileName[]; - /** value in config File map is true if project is referenced using prepend */ - referencingProjectsMap: ConfigFileMap>; - } - - 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, @@ -200,80 +141,93 @@ 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; - getSize(): number; - clear(): void; + export function resolveConfigFileProjectName(project: string): ResolvedConfigFileName { + if (fileExtensionIs(project, Extension.Json)) { + return project as ResolvedConfigFileName; + } + + return combinePaths(project, "tsconfig.json") as ResolvedConfigFileName; } +} - type ResolvedConfigFilePath = ResolvedConfigFileName & Path; - type ConfigFileMap = FileMap; - type ToResolvedConfigFilePath = (fileName: ResolvedConfigFileName) => ResolvedConfigFilePath; - type ToPath = (fileName: string) => Path; +namespace ts { + const minimumDate = new Date(-8640000000000000); + const maximumDate = new Date(8640000000000000); - /** - * 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 { - // tslint:disable-next-line:no-null-keyword - const lookup = createMap(); + export interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; - return { - setValue, - getValue, - removeKey, - forEach, - hasKey, - getSize, - clear - }; + /*@internal*/ clean?: boolean; + /*@internal*/ watch?: boolean; + /*@internal*/ help?: boolean; - function forEach(action: (value: T, key: V) => void) { - lookup.forEach(action); - } + /*@internal*/ preserveWatchOutput?: boolean; + /*@internal*/ listEmittedFiles?: boolean; + /*@internal*/ listFiles?: boolean; + /*@internal*/ pretty?: boolean; + incremental?: boolean; - function hasKey(fileName: U) { - return lookup.has(toPath(fileName)); - } + traceResolution?: boolean; + /* @internal */ diagnostics?: boolean; + /* @internal */ extendedDiagnostics?: boolean; - function removeKey(fileName: U) { - lookup.delete(toPath(fileName)); - } + [option: string]: CompilerOptionsValue | undefined; + } - function setValue(fileName: U, value: T) { - lookup.set(toPath(fileName), value); - } + enum BuildResultFlags { + None = 0, - function getValue(fileName: U): T | undefined { - return lookup.get(toPath(fileName)); - } + /** + * 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, - function getSize() { - return lookup.size; - } + ConfigFileErrors = 1 << 2, + SyntaxErrors = 1 << 3, + TypeErrors = 1 << 4, + DeclarationEmitErrors = 1 << 5, + EmitErrors = 1 << 6, - function clear() { - lookup.clear(); - } + AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors + } + + /*@internal*/ + export 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 ConfigFileMap = FileMap; + function createConfigFileMap(): ConfigFileMap { + return createMap() as ConfigFileMap; } - function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFileName, createT: () => T): T { - const existingValue = configFileMap.getValue(resolved); + function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, 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: ConfigFileMap>, resolved: ResolvedConfigFilePath): Map { return getOrCreateValueFromConfigFileMap>(configFileMap, resolved, createMap); } @@ -285,10 +239,20 @@ namespace ts { return fileExtensionIs(fileName, Extension.Dts); } + 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; + 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; @@ -298,7 +262,7 @@ namespace ts { afterProgramEmitAndDiagnostics?(program: T): void; // For testing - now?(): Date; + /*@internal*/ now?(): Date; } export interface SolutionBuilderHost extends SolutionBuilderHostBase { @@ -308,23 +272,20 @@ namespace ts { export interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { } - export interface SolutionBuilder { - buildAllProjects(): ExitStatus; - cleanAllProjects(): ExitStatus; - - // 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*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; - /*@internal*/ buildInvalidatedProject(): void; + 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; - /*@internal*/ resetBuildContext(opts?: BuildOptions): void; - } + // Currently used for testing but can be made public if needed: + /*@internal*/ getBuildOrder(): ReadonlyArray; - export interface SolutionBuilderWithWatch extends SolutionBuilder { - /*@internal*/ startWatching(): void; + // Testing only + /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; + /*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void; + /*@internal*/ buildNextInvalidatedProject(): void; } /** @@ -369,1251 +330,1774 @@ namespace ts { return result; } - /** - * 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; + 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 { + return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); + } + + 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; + currentInvalidatedProject: InvalidatedProject | undefined; + + // 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 - let options = defaultOptions; - 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); - const missingRoots = createMap(); - let globalDependencyGraph: DependencyGraph | 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 baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); + const compilerHost = createCompilerHostFromProgramHost(host, () => state.projectCompilerOptions); setGetSourceFileAsHashVersioned(compilerHost, host); - compilerHost.getParsedCommandLine = parseConfigFile; - + 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; - let cacheState: { - originalReadFile: CompilerHost["readFile"]; - originalFileExists: CompilerHost["fileExists"]; - originalDirectoryExists: CompilerHost["directoryExists"]; - originalCreateDirectory: CompilerHost["createDirectory"]; - originalWriteFile: CompilerHost["writeFile"] | undefined; - originalReadFileWithCache: CompilerHost["readFile"]; - originalGetSourceFile: CompilerHost["getSourceFile"]; - originalResolveModuleNames: CompilerHost["resolveModuleNames"]; - } | undefined; - - const buildInfoChecked = createFileMap(toPath); - const extendedConfigCache = createMap(); + if (!compilerHost.resolveModuleNames) { + 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); + } + + const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); + + 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, + currentInvalidatedProject: undefined, + + // Watch state + watch, + allWatchedWildcardDirectories: createConfigFileMap(), + allWatchedInputFiles: createConfigFileMap(), + allWatchedConfigFiles: createConfigFileMap(), + + timerToBuildInvalidatedProject: undefined, + reportFileChangeDetected: false, + watchFile, + watchFilePath, + watchDirectory, + writeLog, + }; - // Watch state - const builderPrograms = createFileMap(toPath); - 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); - - // Watches for the solution - const allWatchedWildcardDirectories = createFileMap>(toPath); - const allWatchedInputFiles = createFileMap>(toPath); - const allWatchedConfigFiles = createFileMap(toPath); + return state; + } - return { - buildAllProjects, - getUpToDateStatusOfFile, - cleanAllProjects, - resetBuildContext, - getBuildGraph, + function toPath(state: SolutionBuilderState, fileName: string) { + return ts.toPath(fileName, state.currentDirectory, state.getCanonicalFileName); + } - invalidateProject, - buildInvalidatedProject, + function toResolvedConfigFilePath(state: SolutionBuilderState, fileName: ResolvedConfigFileName): ResolvedConfigFilePath { + const { resolvedConfigFilePaths } = state; + const path = resolvedConfigFilePaths.get(fileName); + if (path !== undefined) return path; - resolveProjectName, + const resolvedPath = toPath(state, fileName) as ResolvedConfigFilePath; + resolvedConfigFilePaths.set(fileName, resolvedPath); + return resolvedPath; + } - startWatching - }; + function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { + return !!(entry as ParsedCommandLine).options; + } - function toPath(fileName: ResolvedConfigFileName): ResolvedConfigFilePath; - function toPath(fileName: string): Path; - function toPath(fileName: string) { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); - } - - function resetBuildContext(opts = defaultOptions) { - options = opts; - baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); - configFileCache.clear(); - unchangedOutputs.clear(); - projectStatus.clear(); - missingRoots.clear(); - globalDependencyGraph = undefined; - buildInfoChecked.clear(); - - diagnostics.clear(); - projectPendingBuild.clear(); - projectErrorsReported.clear(); - invalidatedProjectQueue.length = 0; - nextProjectToBuild = 0; - 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 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 isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { - return !!(entry as ParsedCommandLine).options; + let diagnostic: Diagnostic | undefined; + 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); } - - function parseConfigFile(configFilePath: ResolvedConfigFileName): ParsedCommandLine | undefined { - const value = configFileCache.getValue(configFilePath); - if (value) { - return isParsedCommandLine(value) ? value : undefined; - } - - let diagnostic: Diagnostic | undefined; + else { parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - const parsed = getParsedCommandLineOfConfigFile(configFilePath, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); + parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; - configFileCache.setValue(configFilePath, parsed || diagnostic!); - return parsed; } + configFileCache.set(configFilePath, parsed || diagnostic!); + return parsed; + } - function reportStatus(message: DiagnosticMessage, ...args: string[]) { - host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); - } + function resolveProjectName(state: SolutionBuilderState, name: string): ResolvedConfigFileName { + return resolveConfigFileProjectName(resolvePath(state.currentDirectory, name)); + } - function reportWatchStatus(message: DiagnosticMessage, ...args: (string | number | undefined)[]) { - if (hostWithWatch.onWatchStatusChange) { - hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), host.getNewLine(), baseCompilerOptions); + 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; } - } - function startWatching() { - const graph = getGlobalDependencyGraph(); - for (const resolved of graph.buildQueue) { - // Watch this file - watchConfigFile(resolved); - - const cfg = parseConfigFile(resolved); - if (cfg) { - // Update watchers for wildcard directories - watchWildCardDirectories(resolved, cfg); - - // Watch input files - watchInputFiles(resolved, cfg); + 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); } } + circularityReportStack.pop(); + permanentMarks.set(projPath, true); + (buildOrder || (buildOrder = [])).push(configFileName); } + } - function watchConfigFile(resolved: ResolvedConfigFileName) { - if (options.watch && !allWatchedConfigFiles.hasKey(resolved)) { - allWatchedConfigFiles.setValue(resolved, watchFile( - hostWithWatch, - resolved, - () => { - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Full); - }, - PollingInterval.High, - WatchType.ConfigFile, - resolved - )); - } - } - - function watchWildCardDirectories(resolved: ResolvedConfigFileName, parsed: ParsedCommandLine) { - if (!options.watch) return; - updateWatchingWildcardDirectories( - getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolved), - 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; - } - - if (isOutputFile(fileOrDirectory, parsed)) { - writeLog(`${fileOrDirectory} is output file`); - return; - } - - invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Partial); - }, - flags, - WatchType.WildcardDirectory, - resolved - ); - } - ); - } + function getBuildOrder(state: SolutionBuilderState) { + return state.buildOrder || + (state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)))); + } - function watchInputFiles(resolved: ResolvedConfigFileName, parsed: ParsedCommandLine) { - if (!options.watch) return; - mutateMap( - getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolved), - arrayToMap(parsed.fileNames, toPath), - { - createNewValue: (path, input) => watchFilePath( - hostWithWatch, - input, - () => invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.None), - PollingInterval.Low, - path as Path, - WatchType.SourceFile, - resolved - ), - onDeleteValue: closeFileWatcher, - } + 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( + buildOrderFromState, + configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath ); + if (projectIndex === -1) return undefined; } + 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; + } - function isOutputFile(fileName: string, configFile: ParsedCommandLine) { - if (configFile.options.noEmit) return false; - - // ts or tsx files are not output - if (!fileExtensionIs(fileName, Extension.Dts) && - (fileExtensionIs(fileName, Extension.Ts) || fileExtensionIs(fileName, Extension.Tsx))) { - return false; - } - - // 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; - } + 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, + }; + } - // 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; - } + function disableCache(state: SolutionBuilderState) { + if (!state.cache) return; - // If --outDir, check if file is in that directory - if (configFile.options.outDir && containsPath(configFile.options.outDir, fileName, currentDirectory, !host.useCaseSensitiveFileNames())) { - return true; - } + const { cache, host, compilerHost, extendedConfigCache, moduleResolutionCache } = state; - return !forEach(configFile.fileNames, inputFile => isSameFile(fileName, inputFile)); + 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 isSameFile(file1: string, file2: string) { - return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; - } + function clearProjectStatus(state: SolutionBuilderState, resolved: ResolvedConfigFilePath) { + state.projectStatus.delete(resolved); + state.diagnostics.delete(resolved); + } - function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { - reportFileChangeDetected = true; - invalidateResolvedProject(resolved, reloadLevel); - scheduleBuildInvalidatedProject(); + function addProjToQueue({ projectPendingBuild }: SolutionBuilderState, proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + const value = projectPendingBuild.get(proj); + if (value === undefined) { + projectPendingBuild.set(proj, reloadLevel); } - - function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { - return getUpToDateStatus(parseConfigFile(configFileName)); + else if (value < reloadLevel) { + projectPendingBuild.set(proj, reloadLevel); } + } - function getBuildGraph(configFileNames: ReadonlyArray) { - return createDependencyGraph(resolveProjectNames(configFileNames)); + 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); + buildOrder.forEach(configFileName => + state.projectPendingBuild.set( + toResolvedConfigFilePath(state, configFileName), + ConfigFileProgramReloadLevel.None + ) + ); + + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); } + } - function getGlobalDependencyGraph() { - return globalDependencyGraph || (globalDependencyGraph = getBuildGraph(rootNames)); - } + export enum InvalidatedProjectKind { + Build, + UpdateBundle, + UpdateOutputFileStamps + } - function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { - if (project === undefined) { - return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; - } + export interface InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind; + readonly project: ResolvedConfigFileName; + /*@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, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus; + getCompilerOptions(): CompilerOptions; + getCurrentDirectory(): string; + } - const prior = projectStatus.getValue(project.options.configFilePath as ResolvedConfigFilePath); - if (prior !== undefined) { - return prior; - } + export interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; + updateOutputFileStatmps(): void; + } - const actual = getUpToDateStatusWorker(project); - projectStatus.setValue(project.options.configFilePath as ResolvedConfigFilePath, actual); - return actual; - } + export interface BuildInvalidedProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.Build; + /* + * 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; + 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; + } - function getUpToDateStatusWorker(project: ParsedCommandLine): 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` - }; - } + export interface UpdateBundleProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateBundle; + emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; + } - const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; - if (inputTime > newestInputFileTime) { - newestInputFileName = inputFile; - newestInputFileTime = inputTime; - } - } + export type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; + + function doneInvalidatedProject( + state: SolutionBuilderState, + projectPath: ResolvedConfigFilePath + ) { + state.projectPendingBuild.delete(projectPath); + state.currentInvalidatedProject = undefined; + return state.diagnostics.has(projectPath) ? + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; + } - // Container if no files are specified in the project - if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) { - return { - type: UpToDateStatusType.ContainerOnly - }; + 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; + }, + done: () => { + if (updateOutputFileStampsPending) { + updateOutputTimestamps(state, config, projectPath); + } + return doneInvalidatedProject(state, projectPath); } + }; + } - // Collect the expected outputs of this project - const outputs = getAllProjectOutputs(project, !host.useCaseSensitiveFileNames()); + function createBuildOrUpdateInvalidedProject( + kind: InvalidatedProjectKind.Build | InvalidatedProjectKind.UpdateBundle, + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[], + ): BuildInvalidedProject | UpdateBundleProject { + enum Step { + CreateProgram, + SyntaxDiagnostics, + SemanticDiagnostics, + Emit, + EmitBundle, + BuildInvalidatedProjectOfBundle, + QueueReferencingProjects, + Done + } + + 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 ? + { + kind, + project, + projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, + getBuilderProgram: () => withProgramOrUndefined(identity), + getProgram: () => + withProgramOrUndefined( + program => program.getProgramOrUndefined() + ), + 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); + }, + 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, + }; - // 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; - } + function done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { + executeSteps(Step.Done, cancellationToken, writeFile, customTransformers); + return doneInvalidatedProject(state, projectPath); + } - const outputTime = host.getModifiedTime(output) || missingFileModifiedTime; - if (outputTime < oldestOutputFileTime) { - oldestOutputFileTime = outputTime; - oldestOutputFileName = output; - } + function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { + executeSteps(Step.CreateProgram); + return program && action(program); + } - // 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; - } + function withProgramOrEmptyArray(action: (program: T) => ReadonlyArray): ReadonlyArray { + return withProgramOrUndefined(action) || emptyArray; + } - if (outputTime > newestOutputFileTime) { - newestOutputFileTime = outputTime; - newestOutputFileName = output; - } + function createProgram() { + Debug.assert(program === undefined); - // 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 unchangedTime = unchangedOutputs.getValue(output); - if (unchangedTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); - } - else { - const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); - } - } + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, project); + buildResult = BuildResultFlags.Success; + step = Step.QueueReferencingProjects; + return; } - let pseudoUpToDate = false; - let usesPrepend = false; - let upstreamChangedProject: string | undefined; - if (project.projectReferences) { - projectStatus.setValue(project.options.configFilePath as ResolvedConfigFileName, { type: UpToDateStatusType.ComputingUpstream }); - for (const ref of project.projectReferences) { - usesPrepend = usesPrepend || !!(ref.prepend); - const resolvedRef = resolveProjectReferencePath(ref); - const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef)); - - // Its a circular reference ignore the status of this project - if (refStatus.type === UpToDateStatusType.ComputingUpstream) { - continue; - } - - // An upstream project is blocked - if (refStatus.type === UpToDateStatusType.Unbuildable) { - return { - type: UpToDateStatusType.UpstreamBlocked, - upstreamProjectName: ref.path - }; - } + if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, project); - // 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 (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; } - if (missingOutputFileName !== undefined) { - return { - type: UpToDateStatusType.OutputMissing, - missingOutputFileName - }; - } + const { host, compilerHost } = state; + state.projectCompilerOptions = config.options; + // Update module resolution cache if needed + updateModuleResolutionCache(state, project, config); - if (isOutOfDateWithInputs) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: newestInputFileName - }; + // 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 { - // 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; + step++; } + } - if (!buildInfoChecked.hasKey(project.options.configFilePath as ResolvedConfigFileName)) { - buildInfoChecked.setValue(project.options.configFilePath as ResolvedConfigFileName, 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 - }; - } - } - } + function getSyntaxDiagnostics(cancellationToken?: CancellationToken) { + Debug.assertDefined(program); + handleDiagnostics( + [ + ...program!.getConfigFileParsingDiagnostics(), + ...program!.getOptionsDiagnostics(cancellationToken), + ...program!.getGlobalDiagnostics(cancellationToken), + ...program!.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken) + ], + BuildResultFlags.SyntaxErrors, + "Syntactic" + ); + } - if (usesPrepend && pseudoUpToDate) { - return { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: upstreamChangedProject! - }; - } + function getSemanticDiagnostics(cancellationToken?: CancellationToken) { + handleDiagnostics( + Debug.assertDefined(program).getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken), + BuildResultFlags.TypeErrors, + "Semantic" + ); + } - // Up to date - return { - type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - newestInputFileTime, - newestOutputFileTime, - newestInputFileName, - newestOutputFileName, - oldestOutputFileName - }; - } - - function checkConfigFileUpToDateStatus(configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { - // Check tsconfig time - const tsconfigTime = host.getModifiedTime(configFile) || missingFileModifiedTime; - if (oldestOutputFileTime < tsconfigTime) { + 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 { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: configFile + emitSkipped: true, + diagnostics: emitResult.diagnostics }; } - } - function invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel) { - invalidateResolvedProject(resolveProjectName(configFileName), reloadLevel); - } + // Actual Emit + const { host, compilerHost } = 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; + } + } - function invalidateResolvedProject(resolved: ResolvedConfigFileName, reloadLevel?: ConfigFileProgramReloadLevel) { - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - configFileCache.removeKey(resolved); - globalDependencyGraph = undefined; - } - projectStatus.removeKey(resolved); - diagnostics.removeKey(resolved); + emittedOutputs.set(toPath(state, name), name); + writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); + } + }); - addProjToQueue(resolved, reloadLevel); - enableCache(); + finishEmit( + emitterDiagnostics, + emittedOutputs, + newestDeclarationFileContentChangedTime, + /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ anyDtsChanged, + outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()), + resultFlags + ); + return emitResult; } - /** - * return true if new addition - */ - function addProjToQueue(proj: ResolvedConfigFileName, reloadLevel?: ConfigFileProgramReloadLevel) { - const value = projectPendingBuild.getValue(proj); - if (value === undefined) { - projectPendingBuild.setValue(proj, reloadLevel || ConfigFileProgramReloadLevel.None); - invalidatedProjectQueue.push(proj); - } - else if (value < (reloadLevel || ConfigFileProgramReloadLevel.None)) { - projectPendingBuild.setValue(proj, reloadLevel || ConfigFileProgramReloadLevel.None); + function finishEmit( + emitterDiagnostics: DiagnosticCollection, + emittedOutputs: FileMap, + priorNewestUpdateTime: Date, + newestDeclarationFileContentChangedTimeIsMaximumDate: boolean, + oldestOutputFileName: string, + resultFlags: BuildResultFlags + ) { + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + buildResult = buildErrors( + state, + projectPath, + program, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); + step = Step.QueueReferencingProjects; + return emitDiagnostics; } - } - 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; - } - return { project, reloadLevel }; + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); + if (program) listFiles(program, state.writeFileName); } - } - function hasPendingInvalidatedProjects() { - return !!projectPendingBuild.getSize(); + // Update time stamps for rest of the outputs + 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: newestDeclarationFileContentChangedTimeIsMaximumDate ? + maximumDate : + newestDeclarationFileContentChangedTime, + oldestOutputFileName + }); + if (program) afterProgramCreate(state, projectPath, program); + state.projectCompilerOptions = state.baseCompilerOptions; + step = Step.QueueReferencingProjects; + buildResult = resultFlags; + return emitDiagnostics; } - function scheduleBuildInvalidatedProject() { - if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) { - return; + 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 (timerToBuildInvalidatedProject) { - hostWithWatch.clearTimeout(timerToBuildInvalidatedProject); + + 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; } - timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildInvalidatedProject, 250); + + // 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 buildInvalidatedProject() { - timerToBuildInvalidatedProject = undefined; - if (reportFileChangeDetected) { - reportFileChangeDetected = false; - projectErrorsReported.clear(); - reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); - } - const buildProject = getNextInvalidatedProject(); - if (buildProject) { - buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); - if (hasPendingInvalidatedProjects()) { - if (options.watch && !timerToBuildInvalidatedProject) { - scheduleBuildInvalidatedProject(); - } - } - else { - disableCache(); - reportErrorSummary(); + function executeSteps(till: Step, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { + 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(writeFile, cancellationToken, customTransformers); + break; + + case Step.EmitBundle: + emitBundle(writeFile, customTransformers); + 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++; + break; + + // Should never be done + case Step.Done: + default: + assertType(step); + } + Debug.assert(step > currentStep); } } + } - function reportErrorSummary() { - if (options.watch || (host as SolutionBuilderHost).reportErrorSummary) { - // Report errors from the other projects - getGlobalDependencyGraph().buildQueue.forEach(project => { - if (!projectErrorsReported.hasKey(project)) { - reportErrors(diagnostics.getValue(project) || emptyArray); - } - }); - let totalErrors = 0; - diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); - if (options.watch) { - reportWatchStatus(getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); - } - else { - (host as SolutionBuilderHost).reportErrorSummary!(totalErrors); - } - } + 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[], + reportQueue: boolean + ): 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; } - function buildSingleInvalidatedProject(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { - const proj = parseConfigFile(resolved); - if (!proj) { - reportParseConfigFileDiagnostic(resolved); - return; + 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; + + if (reportQueue) { + reportQueue = false; + reportBuildQueue(state, buildOrder); + } + + const config = parseConfigFile(state, project, projectPath); + if (!config) { + reportParseConfigFileDiagnostic(state, projectPath); + projectPendingBuild.delete(projectPath); + continue; } if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(resolved); - watchWildCardDirectories(resolved, proj); - watchInputFiles(resolved, proj); + 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(proj.configFileSpecs!, getDirectoryPath(resolved), proj.options, parseConfigFileHost); - updateErrorForNoInputFiles(result, resolved, proj.configFileSpecs!, proj.errors, canJsonReportNoInutFiles(proj.raw)); - proj.fileNames = result.fileNames; - watchInputFiles(resolved, proj); - } + 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; + } - const status = getUpToDateStatus(proj); - verboseReportProjectStatus(resolved, status); + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { + reportAndStoreErrors(state, projectPath, config.errors); + return createUpdateOutputFileStampsProject( + state, + project, + projectPath, + config, + buildOrder + ); + } + } if (status.type === UpToDateStatusType.UpstreamBlocked) { - if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); - return; - } + 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 createBuildOrUpdateInvalidedProject( + needsBuild(state, status, config) ? + InvalidatedProjectKind.Build : + InvalidatedProjectKind.UpdateBundle, + state, + project, + projectPath, + projectIndex, + config, + buildOrder, + ); + } - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { - // Fake that files have been built by updating output file stamps - updateOutputTimestamps(proj); - return; - } + return undefined; + } - const buildResult = needsBuild(status, resolved) ? - buildSingleProject(resolved) : // Actual build - 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; - - // 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 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); - if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { - if (status && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes || status.type === UpToDateStatusType.OutOfDateWithPrepend)) { - projectStatus.setValue(project, { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, - newerProjectName: resolved - }); - } - } - else if (status && status.type === UpToDateStatusType.UpToDate) { - if (prepend) { - projectStatus.setValue(project, { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: status.oldestOutputFileName, - newerProjectName: resolved - }); - } - else { - status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; - } - } - addProjToQueue(project); - } - } + function listEmittedFile({ writeFileName }: SolutionBuilderState, proj: ParsedCommandLine, file: string) { + if (writeFileName && proj.options.listEmittedFiles) { + writeFileName(`TSFILE: ${file}`); } + } - function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph { - const temporaryMarks = createFileMap(toPath); - const permanentMarks = createFileMap(toPath); - const circularityReportStack: string[] = []; - const buildOrder: ResolvedConfigFileName[] = []; - const referencingProjectsMap = createFileMap>(toPath); - for (const root of roots) { - visit(root); - } + 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; + } - return { - buildQueue: buildOrder, - referencingProjectsMap - }; + 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 visit(projPath: ResolvedConfigFileName, inCircularContext?: boolean) { - // Already visited - if (permanentMarks.hasKey(projPath)) return; - // Circular - if (temporaryMarks.hasKey(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; - } + 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; + } - temporaryMarks.setValue(projPath, true); - circularityReportStack.push(projPath); - const parsed = parseConfigFile(projPath); - if (parsed && parsed.projectReferences) { - 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); - } - } + 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); + } - circularityReportStack.pop(); - permanentMarks.setValue(projPath, true); - buildOrder.push(projPath); - } + 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 buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj); - return BuildResultFlags.Success; + 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` + }; } - if (options.verbose) reportStatus(Diagnostics.Building_project_0, proj); - - let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; - - const configFile = parseConfigFile(proj); - if (!configFile) { - // Failed to read the config file - resultFlags |= BuildResultFlags.ConfigFileErrors; - reportParseConfigFileDiagnostic(proj); - projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); - return resultFlags; - } - if (configFile.fileNames.length === 0) { - reportAndStoreErrors(proj, configFile.errors); - // Nothing to build - must be a solution file, basically - return BuildResultFlags.None; + const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; } + } - // 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); + // Container if no files are specified in the project + if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) { + return { + type: UpToDateStatusType.ContainerOnly + }; + } - 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); + // 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 program = host.createProgram( - configFile.fileNames, - configFile.options, - compilerHost, - getOldProgram(proj, configFile), - configFile.errors, - configFile.projectReferences - ); + const outputTime = host.getModifiedTime(output) || missingFileModifiedTime; + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; + } - // Don't emit anything in the presence of syntactic errors or options diagnostics - const syntaxDiagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(), - ...program.getGlobalDiagnostics(), - ...program.getSyntacticDiagnostics()]; - if (syntaxDiagnostics.length) { - return buildErrors(syntaxDiagnostics, BuildResultFlags.SyntaxErrors, "Syntactic"); + // 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; } - // Same as above but now for semantic diagnostics - const semanticDiagnostics = program.getSemanticDiagnostics(); - if (semanticDiagnostics.length) { - return buildErrors(semanticDiagnostics, BuildResultFlags.TypeErrors, "Semantic"); + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; } - // 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 })); - // Don't emit .d.ts if there are decl file errors - if (declDiagnostics) { - program.restoreState(); - return buildErrors(declDiagnostics, BuildResultFlags.DeclarationEmitErrors, "Declaration file"); + // 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); } + } - // Actual Emit - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createFileMap(toPath as ToPath); - 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); - } - else { - resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - anyDtsChanged = true; - } + 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); + + // Its a circular reference ignore the status of this project + if (refStatus.type === UpToDateStatusType.ComputingUpstream) { + continue; } - emittedOutputs.setValue(name, name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - if (priorChangeTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - unchangedOutputs.setValue(name, priorChangeTime); + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path + }; } - }); - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - return buildErrors(emitDiagnostics, BuildResultFlags.EmitErrors, "Emit"); - } + // 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 (writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(configFile, name)); - listFiles(program, writeFileName); + // 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 + }; + } } + } - // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + if (missingOutputFileName !== undefined) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName + }; + } - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) + if (isOutOfDateWithInputs) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: newestInputFileName }; - diagnostics.removeKey(proj); - projectStatus.setValue(proj, status); - afterProgramCreate(proj, program); - projectCompilerOptions = baseCompilerOptions; - return resultFlags; - - function buildErrors(diagnostics: ReadonlyArray, errorFlags: BuildResultFlags, errorType: string) { - resultFlags |= errorFlags; - reportAndStoreErrors(proj, 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` }); - afterProgramCreate(proj, program); - projectCompilerOptions = baseCompilerOptions; - return resultFlags; - } } + else { + // Check tsconfig time + const configStatus = checkConfigFileUpToDateStatus(state, project.options.configFilePath!, oldestOutputFileTime, oldestOutputFileName); + if (configStatus) return configStatus; - function listEmittedFile(proj: ParsedCommandLine, file: string) { - if (writeFileName && proj.options.listEmittedFiles) { - writeFileName(`TSFILE: ${file}`); - } + // Check extended config time + const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName)); + if (extendedConfigStatus) return extendedConfigStatus; } - function afterProgramCreate(proj: ResolvedConfigFileName, program: T) { - if (host.afterProgramEmitAndDiagnostics) { - host.afterProgramEmitAndDiagnostics(program); - } - if (options.watch) { - program.releaseProgram(); - builderPrograms.setValue(proj, program); + 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.bundle || buildInfo.program) && buildInfo.version !== version) { + return { + type: UpToDateStatusType.TsVersionOutputOfDate, + version: buildInfo.version + }; + } } } - function getOldProgram(proj: ResolvedConfigFileName, parsed: ParsedCommandLine) { - if (options.force) return undefined; - const value = builderPrograms.getValue(proj); - if (value) return value; - return readBuilderProgram(parsed.options, readFileWithCache) as any as T; + if (usesPrepend && pseudoUpToDate) { + return { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: upstreamChangedProject! + }; } - function updateBundle(proj: ResolvedConfigFileName): BuildResultFlags { - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); - return BuildResultFlags.Success; - } + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName + }; + } - if (options.verbose) reportStatus(Diagnostics.Updating_output_of_project_0, proj); + function getUpToDateStatus(state: SolutionBuilderState, project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + } - // Update js, and source map - const config = Debug.assertDefined(parseConfigFile(proj)); - projectCompilerOptions = config.options; - const outputFiles = emitUsingBuildInfo( - config, - compilerHost, - ref => parseConfigFile(resolveProjectName(ref.path))); - if (isString(outputFiles)) { - reportStatus(Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(outputFiles)); - return buildSingleProject(proj); - } + const prior = state.projectStatus.get(resolvedPath); + if (prior !== undefined) { + return prior; + } - // Actual Emit - Debug.assert(!!outputFiles.length); - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createFileMap(toPath as ToPath); - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - emittedOutputs.setValue(name, name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - }); - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - reportAndStoreErrors(proj, emitDiagnostics); - projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Emit errors" }); - projectCompilerOptions = baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged | BuildResultFlags.EmitErrors; - } + const actual = getUpToDateStatusWorker(state, project, resolvedPath); + state.projectStatus.set(resolvedPath, actual); + return actual; + } - if (writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(config, name)); - } + 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; + } - // Update timestamps for dts - const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(config, minimumDate, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + if (reportVerbose) { + reportVerbose = false; + reportStatus(state, verboseMessage, proj.options.configFilePath!); + } - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles[0].name - }; + if (isDeclarationFile(file)) { + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); + } - diagnostics.removeKey(proj); - projectStatus.setValue(proj, status); - projectCompilerOptions = baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged; + host.setModifiedTime(file, now); + listEmittedFile(state, proj, file); + } } - function updateOutputTimestamps(proj: ParsedCommandLine) { - 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.setValue(proj.options.configFilePath as ResolvedConfigFilePath, status); + return priorNewestUpdateTime; + } + + 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 updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { - const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames()); - if (!skipOutputs || outputs.length !== skipOutputs.getSize()) { - 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)) { - continue; - } + function queueReferencingProjects( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[], + 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]; + 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 (buildResult & BuildResultFlags.DeclarationOutputUnchanged) { + if (ref.prepend) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: status.oldestOutputFileName, + newerProjectName: project + }); + } + else { + status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; + } + break; + } - if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); - } + // falls through + case UpToDateStatusType.UpToDateWithUpstreamTypes: + case UpToDateStatusType.OutOfDateWithPrepend: + if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, + newerProjectName: project + }); + } + break; - host.setModifiedTime(file, now); - listEmittedFile(proj, file); + case UpToDateStatusType.UpstreamBlocked: + if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) { + clearProjectStatus(state, nextProjectPath); + } + break; + } } + addProjToQueue(state, nextProjectPath, ConfigFileProgramReloadLevel.None); + break; } + } + } - return priorNewestUpdateTime; + 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, reportQueue); + if (!invalidatedProject) break; + reportQueue = false; + invalidatedProject.done(cancellationToken); + if (state.diagnostics.has(invalidatedProject.projectPath)) { + errorProjects++; + } + else { + successfulProjects++; + } } - function getFilesToClean(): string[] { - // Get the same graph for cleaning we'd use for building - const graph = getGlobalDependencyGraph(); - const filesToDelete: string[] = []; - for (const proj of graph.buildQueue) { - const parsed = parseConfigFile(proj); - if (parsed === undefined) { - // File has gone missing; fine to ignore here - reportParseConfigFileDiagnostic(proj); - continue; - } - const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); - for (const output of outputs) { - if (host.fileExists(output)) { + disableCache(state); + reportErrorSummary(state, buildOrder); + startWatching(state, buildOrder); + + return errorProjects ? + successfulProjects ? + ExitStatus.DiagnosticsPresent_OutputsGenerated : + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; + } + + function clean(state: SolutionBuilderState, project?: string, onlyReferences?: boolean) { + const buildOrder = getBuildOrderFor(state, project, onlyReferences); + 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 { + host.deleteFile(output); + invalidateProject(state, resolvedPath, ConfigFileProgramReloadLevel.None); + } } } - return filesToDelete; } - function cleanAllProjects() { - const filesToDelete = getFilesToClean(); - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); - return ExitStatus.Success; - } + if (filesToDelete) { + reportStatus(state, Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); + } - for (const output of filesToDelete) { - host.deleteFile(output); - } + return ExitStatus.Success; + } - return ExitStatus.Success; + 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; } - - function resolveProjectName(name: string): ResolvedConfigFileName { - return resolveConfigFileProjectName(resolvePath(host.getCurrentDirectory(), name)); + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + state.configFileCache.delete(resolved); + state.buildOrder = undefined; } + state.needsSummary = true; + clearProjectStatus(state, resolved); + addProjToQueue(state, resolved, reloadLevel); + enableCache(state); + } - function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] { - return configFileNames.map(resolveProjectName); + function invalidateProjectAndScheduleBuilds(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + state.reportFileChangeDetected = true; + invalidateProject(state, resolvedPath, reloadLevel); + scheduleBuildInvalidatedProject(state); + } + + function scheduleBuildInvalidatedProject(state: SolutionBuilderState) { + const { hostWithWatch } = state; + if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) { + return; + } + if (state.timerToBuildInvalidatedProject) { + hostWithWatch.clearTimeout(state.timerToBuildInvalidatedProject); } + state.timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250, state); + } - function enableCache() { - if (cacheState) { - disableCache(); + 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 buildOrder = getBuildOrder(state); + const invalidatedProject = getNextInvalidatedProject(state, buildOrder, /*reportQueue*/ false); + if (invalidatedProject) { + invalidatedProject.done(); + if (state.projectPendingBuild.size) { + // Schedule next project for build + if (state.watch && !state.timerToBuildInvalidatedProject) { + scheduleBuildInvalidatedProject(state); + } + return; } + } + disableCache(state); + reportErrorSummary(state, buildOrder); + } - 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!; - - const originalResolveModuleNames = compilerHost.resolveModuleNames; - if (!compilerHost.resolveModuleNames) { - const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference).resolvedModule!; - compilerHost.resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference) => - loadWithLocalCache(Debug.assertEachDefined(moduleNames), containingFile, redirectedReference, loader); - } + 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 + )); + } - cacheState = { - originalReadFile, - originalFileExists, - originalDirectoryExists, - originalCreateDirectory, - originalWriteFile, - originalReadFileWithCache, - originalGetSourceFile, - originalResolveModuleNames - }; + function isSameFile(state: SolutionBuilderState, file1: string, file2: string) { + return comparePaths(file1, file2, state.currentDirectory, !state.host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + } + + function isOutputFile(state: SolutionBuilderState, fileName: string, configFile: ParsedCommandLine) { + if (configFile.options.noEmit) return false; + + // ts or tsx files are not output + if (!fileExtensionIs(fileName, Extension.Dts) && + (fileExtensionIs(fileName, Extension.Ts) || fileExtensionIs(fileName, Extension.Tsx))) { + return false; } - 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; - compilerHost.resolveModuleNames = cacheState.originalResolveModuleNames; - extendedConfigCache.clear(); - if (moduleResolutionCache) { - moduleResolutionCache.directoryToModuleNameMap.clear(); - moduleResolutionCache.moduleNameToDirectoryMap.clear(); - } - cacheState = undefined; - } - - function buildAllProjects(): ExitStatus { - if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } - enableCache(); - - const graph = getGlobalDependencyGraph(); - reportBuildQueue(graph); - let anyFailed = false; - for (const next of graph.buildQueue) { - const proj = parseConfigFile(next); - if (proj === undefined) { - reportParseConfigFileDiagnostic(next); - anyFailed = true; - break; - } + // 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; + } - // report errors early when using continue or break statements - const errors = proj.errors; - const status = getUpToDateStatus(proj); - verboseReportProjectStatus(next, status); + // 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; + } - const projName = proj.options.configFilePath!; - if (status.type === UpToDateStatusType.UpToDate && !options.force) { - reportAndStoreErrors(next, 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); + // 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; + } + + return !forEach(configFile.fileNames, inputFile => isSameFile(state, fileName, inputFile)); + } + + 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; } - continue; - } - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !options.force) { - reportAndStoreErrors(next, errors); - // Fake build - updateOutputTimestamps(proj); - continue; - } + if (isOutputFile(state, fileOrDirectory, parsed)) { + state.writeLog(`${fileOrDirectory} is output file`); + return; + } - 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); - continue; - } + invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial); + }, + flags, + WatchType.WildcardDirectory, + resolved + ) + ); + } - if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(next, errors); - // Do nothing - continue; - } + 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 buildResult = needsBuild(status, next) ? - buildSingleProject(next) : // Actual build - updateBundle(next); // Fake that files have been built by manipulating prepend and existing output + function startWatching(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]) { + if (!state.watchAllProjectsPending) return; + state.watchAllProjectsPending = false; + for (const resolved of buildOrder) { + const resolvedPath = toResolvedConfigFilePath(state, resolved); + // Watch this file + watchConfigFile(state, resolved, resolvedPath); - anyFailed = anyFailed || !!(buildResult & BuildResultFlags.AnyErrors); + const cfg = parseConfigFile(state, resolved, resolvedPath); + if (cfg) { + // Update watchers for wildcard directories + watchWildCardDirectories(state, resolved, resolvedPath, cfg); + + // Watch input files + watchInputFiles(state, resolved, resolvedPath, cfg); } - reportErrorSummary(); - disableCache(); - return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; } + } - function needsBuild(status: UpToDateStatus, configFile: ResolvedConfigFileName) { - if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; - const config = parseConfigFile(configFile); - return !config || - config.fileNames.length === 0 || - !!config.errors.length || - !isIncrementalCompilation(config.options); - } + /** + * 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), + 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), /*reportQueue*/ false); + }, + 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), + }; + } - function reportParseConfigFileDiagnostic(proj: ResolvedConfigFileName) { - reportAndStoreErrors(proj, [configFileCache.getValue(proj) as Diagnostic]); - } + function relName(state: SolutionBuilderState, path: string): string { + return convertToRelativePath(path, state.currentDirectory, f => state.getCanonicalFileName(f)); + } - function reportAndStoreErrors(proj: ResolvedConfigFileName, errors: ReadonlyArray) { - reportErrors(errors); - projectErrorsReported.setValue(proj, true); - diagnostics.setValue(proj, errors); - } + function reportStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: string[]) { + state.host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); + } - function reportErrors(errors: ReadonlyArray) { - errors.forEach(err => host.reportDiagnostic(err)); + 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); } + } - /** - * Report the build ordering inferred from the current project graph if we're in verbose mode - */ - function reportBuildQueue(graph: DependencyGraph) { - if (options.verbose) { - reportStatus(Diagnostics.Projects_in_this_build_Colon_0, graph.buildQueue.map(s => "\r\n * " + relName(s)).join("")); - } - } + function reportErrors({ host }: SolutionBuilderState, errors: ReadonlyArray) { + errors.forEach(err => host.reportDiagnostic(err)); + } - function relName(path: string): string { - return convertToRelativePath(path, host.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); + function reportAndStoreErrors(state: SolutionBuilderState, proj: ResolvedConfigFilePath, errors: ReadonlyArray) { + reportErrors(state, errors); + state.projectErrorsReported.set(proj, true); + if (errors.length) { + state.diagnostics.set(proj, errors); } + } - /** - * 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); - } + function reportParseConfigFileDiagnostic(state: SolutionBuilderState, proj: ResolvedConfigFilePath) { + reportAndStoreErrors(state, proj, [state.configFileCache.get(proj) as Diagnostic]); } - export function resolveConfigFileProjectName(project: string): ResolvedConfigFileName { - if (fileExtensionIs(project, Extension.Json)) { - return project as ResolvedConfigFileName; + 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 + buildOrder.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); + } + else { + state.host.reportErrorSummary!(totalErrors); } + } - return combinePaths(project, "tsconfig.json") as ResolvedConfigFileName; + /** + * 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("")); + } } - export 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; @@ -1621,4 +2105,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/compiler/types.ts b/src/compiler/types.ts index 75fb702609b70..396d3b635bffa 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3068,6 +3068,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..9378478664f14 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); } @@ -113,12 +111,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, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; } export function listFiles(program: ProgramToEmitFilesAndReportErrors, writeFileName: (s: string) => void) { @@ -132,25 +130,35 @@ 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, + emitOnlyDtsFiles?: boolean, + customTransformers?: CustomTransformers + ) { // 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 emitResult = program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + const { emittedFiles, diagnostics: emitDiagnostics } = emitResult; addRange(diagnostics, emitDiagnostics); sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic); @@ -167,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; } @@ -375,6 +410,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 = emitFilesAndReportErrorsAndGetExitStatus( + 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); @@ -395,7 +457,7 @@ namespace ts { return host; } - interface IncrementalProgramOptions { + export interface IncrementalProgramOptions { rootNames: ReadonlyArray; options: CompilerOptions; configFileParsingDiagnostics?: ReadonlyArray; @@ -403,7 +465,8 @@ namespace ts { host?: CompilerHost; createProgram?: CreateProgram; } - function createIncrementalProgram({ + + export function createIncrementalProgram({ rootNames, options, configFileParsingDiagnostics, projectReferences, host, createProgram }: IncrementalProgramOptions): T { host = host || createIncrementalCompilerHost(options); @@ -412,33 +475,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/src/harness/fakes.ts b/src/harness/fakes.ts index 489a41dd677d7..31e64e2c93129 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -388,10 +388,42 @@ 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 { - 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); @@ -405,30 +437,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/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"]); }); }); }); diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts index 17c6644ccc66c..38b8b4b537915 100644 --- a/src/testRunner/unittests/tsbuild/emptyFiles.ts +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -14,13 +14,11 @@ 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. - 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", () => { @@ -29,13 +27,11 @@ 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. - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); }); } diff --git a/src/testRunner/unittests/tsbuild/graphOrdering.ts b/src/testRunner/unittests/tsbuild/graphOrdering.ts index bc1af3f71da94..a8f4c3a57a992 100644 --- a/src/testRunner/unittests/tsbuild/graphOrdering.ts +++ b/src/testRunner/unittests/tsbuild/graphOrdering.ts @@ -22,34 +22,32 @@ 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[]) { - 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.sameMembers(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}`); } } diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts index 579573a876e6e..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; @@ -172,7 +184,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 48beeef4f61da..f562d4dc26b77 100644 --- a/src/testRunner/unittests/tsbuild/outFile.ts +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -363,21 +363,17 @@ 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) { - 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", () => { @@ -388,18 +384,16 @@ namespace ts { ...outputFiles[project.third] ]; const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host); + 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]); - builder.resetBuildContext(); - builder.buildAllProjects(); + builder = createSolutionBuilder(host); + 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,25 +410,23 @@ 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)) { - 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", () => { const fs = outFileFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); host.clearDiagnostics(); - builder.resetBuildContext(); + 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], @@ -453,16 +445,16 @@ namespace ts { // Build with command line incremental const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, { incremental: true }); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host, { incremental: true }); + builder.build(); host.assertDiagnosticMessages(...initialExpectedDiagnostics); host.clearDiagnostics(); tick(); // 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.buildAllProjects(); + builder = createSolutionBuilder(host, { verbose: true }); + 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]], @@ -475,8 +467,8 @@ 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.buildAllProjects(); + builder = createSolutionBuilder(host, { verbose: true, incremental: true }); + 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 +481,33 @@ 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 + verifyOutputsAbsent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); + // second is built + 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); + }); + describe("Prepend output with .tsbuildinfo", () => { // Prologues describe("Prologues", () => { @@ -904,7 +923,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"], @@ -923,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 630cd8575eccf..186a43b3c3e9d 100644 --- a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -19,11 +19,9 @@ 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`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); it("verify that it reports error for same .tsbuildinfo file because no rootDir in the base", () => { @@ -39,7 +37,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"], @@ -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", () => { @@ -75,7 +69,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"], @@ -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", () => { @@ -112,7 +102,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"], @@ -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 3fc50d72ea5ee..66bb0dc6e9533 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -19,13 +19,11 @@ 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 - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); } } @@ -64,20 +62,18 @@ 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 }); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host, [configFile], { verbose: true }); + 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"], [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.resetBuildContext(); + 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"] @@ -89,20 +85,18 @@ 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 }); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host, [configFile], { verbose: true }); + 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"], [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.resetBuildContext(); + 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"] @@ -128,8 +122,8 @@ 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 }); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host, [configFile], { verbose: true }); + 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"], @@ -137,11 +131,11 @@ 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.resetBuildContext(); + 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 5ad63d76dc7ee..df2fb62db25b6 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", "/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(() => { projFs = loadProjectFromDisk("tests/projects/sample1", time); @@ -21,13 +22,11 @@ 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 - for (const output of allExpectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, allExpectedOutputs); }); it("builds correctly when outDir is specified", () => { @@ -39,13 +38,11 @@ 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 - for (const output of expectedOutputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } + verifyOutputsPresent(fs, expectedOutputs); }); it("builds correctly when declarationDir is specified", () => { @@ -57,13 +54,11 @@ 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 - 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", () => { @@ -71,15 +66,13 @@ 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"], [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"]); }); }); @@ -88,7 +81,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"], @@ -96,9 +89,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", () => { @@ -106,12 +97,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,18 +117,44 @@ 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`); - } - 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, testsOutputs); + verifyOutputsAbsent(fs, [...logicOutputs, ...coreOutputs]); + 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); }); }); @@ -146,15 +163,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) { @@ -171,11 +189,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 }); - builder.buildAllProjects(); + let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + builder.build(); host.clearDiagnostics(); tick(); - builder.resetBuildContext(opts ? { ...opts, verbose: true } : undefined); + builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true }); return { fs, host, builder }; } @@ -183,8 +201,7 @@ namespace ts { const fs = projFs.shadow(); const host = new fakes.SolutionBuilderHost(fs); const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.resetBuildContext(); - 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"], @@ -199,7 +216,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"], @@ -212,7 +229,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"], @@ -226,7 +243,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"), @@ -243,7 +260,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], @@ -255,9 +272,38 @@ 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.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"], @@ -272,7 +318,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,8 +333,8 @@ 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 }); - builder.buildAllProjects(); + 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"], @@ -300,9 +346,9 @@ 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(); + 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"], @@ -311,6 +357,82 @@ 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*/); + verifyOutputsAbsent(fs, testsOutputs); + verifyOutputsPresent(fs, [...logicOutputs, ...coreOutputs]); + 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*/); + verifyOutputsAbsent(fs, allExpectedOutputs); + assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); + }); + + 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"], {}); + 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 project = builder.getNextInvalidatedProject(); + const result = project && project.done(); + assert.deepEqual(project && { project: project.project, result }, expected); + verifyOutputsPresent(fs, presentOutputs); + 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", () => { @@ -321,7 +443,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"], @@ -341,7 +463,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 @@ -354,7 +476,7 @@ namespace ts { 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")); + const status = builder.getUpToDateStatusOfProject("/src/logic"); assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); verifyInvalidation(/*expectedToWriteTests*/ false); @@ -366,8 +488,8 @@ export class cNew {}`); function verifyInvalidation(expectedToWriteTests: boolean) { // Rebuild this project tick(); - builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProject(); + 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"); assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); @@ -377,7 +499,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"); } @@ -395,7 +517,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", @@ -422,7 +544,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..614796b2a8592 100644 --- a/src/testRunner/unittests/tsbuild/transitiveReferences.ts +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -30,11 +30,9 @@ 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`); - } + verifyOutputsPresent(fs, allExpectedOutputs); assert.deepEqual(host.traces, expectedFileTraces); } diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 005956a98ad1d..a630e28f1d893 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -17,14 +17,14 @@ 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); - solutionBuilder.buildAllProjects(); - solutionBuilder.startWatching(); + function createSolutionBuilderWithWatch(system: TsBuildWatchSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = ts.createSolutionBuilderWithWatch(host, rootNames, defaultOptions || { watch: true }); + solutionBuilder.build(); return solutionBuilder; } @@ -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 = { @@ -607,7 +629,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`); @@ -676,7 +698,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", () => { @@ -721,8 +743,8 @@ let x: string = 10;`); host.writeFile(logic[1].path, `${logic[1].content} function foo() { }`); - solutionBuilder.invalidateProject(`${project}/${SubProject.logic}`); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); + 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 @@ -734,8 +756,8 @@ function foo() { host.writeFile(logic[1].path, `${logic[1].content} export function gfoo() { }`); - solutionBuilder.invalidateProject(logic[0].path); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); + solutionBuilder.buildNextInvalidatedProject(); }, expectedProgramFiles); }); @@ -745,8 +767,8 @@ export function gfoo() { compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, references: [{ path: "../core" }] })); - solutionBuilder.invalidateProject(logic[0].path, ConfigFileProgramReloadLevel.Full); - solutionBuilder.buildInvalidatedProject(); + 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")]); }); }); @@ -899,7 +921,7 @@ export function gfoo() { } function verifyScenario( - edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, + edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, expectedEditErrors: ReadonlyArray, expectedProgramFiles: ReadonlyArray, expectedWatchedFiles: ReadonlyArray, @@ -965,8 +987,8 @@ export function gfoo() { host.writeFile(bTs.path, `${bTs.content} export function gfoo() { }`); - solutionBuilder.invalidateProject(bTsconfig.path); - solutionBuilder.buildInvalidatedProject(); + solutionBuilder.invalidateProject(bTsconfig.path.toLowerCase() as ResolvedConfigFilePath); + solutionBuilder.buildNextInvalidatedProject(); }, emptyArray, expectedProgramFiles, @@ -1140,9 +1162,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/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/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 500480463bdbb..6a340109238b8 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 "); @@ -206,28 +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(); + 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; } - // 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)); + const buildHost = createSolutionBuilderHost(sys, /*createProgram*/ undefined, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty(buildOptions)), createReportErrorSummary(buildOptions)); updateCreateProgram(buildHost); - buildHost.afterProgramEmitAndDiagnostics = (program: BuilderProgram) => reportStatistics(program.getProgram()); - + buildHost.afterProgramEmitAndDiagnostics = program => reportStatistics(program.getProgram()); const builder = createSolutionBuilder(buildHost, projects, buildOptions); - if (buildOptions.clean) { - return sys.exit(builder.cleanAllProjects()); - } - - if (buildOptions.watch) { - builder.buildAllProjects(); - return (builder as SolutionBuilderWithWatch).startWatching(); - } - - return sys.exit(builder.buildAllProjects()); + return sys.exit(buildOptions.clean ? builder.clean() : builder.build()); } function createReportErrorSummary(options: CompilerOptions | BuildOptions): ReportEmitErrorSummary | undefined { @@ -251,7 +246,7 @@ namespace ts { configFileParsingDiagnostics }; const program = createProgram(programOptions); - const exitStatus = emitFilesAndReportErrors( + const exitStatus = emitFilesAndReportErrorsAndGetExitStatus( program, reportDiagnostic, s => sys.write(s + sys.newLine), diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ca97395891a31..0351c6b343aca 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1910,7 +1910,8 @@ declare namespace ts { enum ExitStatus { Success = 0, DiagnosticsPresent_OutputsSkipped = 1, - DiagnosticsPresent_OutputsGenerated = 2 + DiagnosticsPresent_OutputsGenerated = 2, + InvalidProject_OutputsSkipped = 3 } interface EmitResult { emitSkipped: boolean; @@ -4422,7 +4423,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 @@ -4448,6 +4449,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 | 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; @@ -4561,6 +4573,91 @@ declare namespace ts { */ function createWatchProgram(host: WatchCompilerHostOfConfigFile): WatchOfConfigFile; } +declare namespace ts { + interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + incremental?: boolean; + traceResolution?: boolean; + [option: string]: CompilerOptionsValue | undefined; + } + 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; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; + 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; + buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + cleanReferences(project?: string): ExitStatus; + getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; + } + /** + * 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; + 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, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): 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"; type ActionInvalidate = "action::invalidate"; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index e136f2c96e4df..9becdad6aa097 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1910,7 +1910,8 @@ declare namespace ts { enum ExitStatus { Success = 0, DiagnosticsPresent_OutputsSkipped = 1, - DiagnosticsPresent_OutputsGenerated = 2 + DiagnosticsPresent_OutputsGenerated = 2, + InvalidProject_OutputsSkipped = 3 } interface EmitResult { emitSkipped: boolean; @@ -4422,7 +4423,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 @@ -4448,6 +4449,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 | 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; @@ -4561,6 +4573,91 @@ declare namespace ts { */ function createWatchProgram(host: WatchCompilerHostOfConfigFile): WatchOfConfigFile; } +declare namespace ts { + interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + incremental?: boolean; + traceResolution?: boolean; + [option: string]: CompilerOptionsValue | undefined; + } + 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; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; + 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; + buildReferences(project: string, cancellationToken?: CancellationToken): ExitStatus; + cleanReferences(project?: string): ExitStatus; + getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; + } + /** + * 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; + 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, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): 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"; type ActionInvalidate = "action::invalidate";