diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 9e625041287..22bc8ba8238 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -198,7 +198,8 @@ namespace ts { AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors } - type ResolvedConfigFilePath = ResolvedConfigFileName & Path; + /*@internal*/ + export type ResolvedConfigFilePath = ResolvedConfigFileName & Path; interface FileMap extends Map { get(key: U): T | undefined; has(key: U): boolean; @@ -212,6 +213,9 @@ namespace ts { clear(): void; } type ConfigFileMap = FileMap; + function createConfigFileMap(): ConfigFileMap { + return createMap() as ConfigFileMap; + } function getOrCreateValueFromConfigFileMap(configFileMap: ConfigFileMap, resolved: ResolvedConfigFilePath, createT: () => T): T { const existingValue = configFileMap.get(resolved); @@ -275,15 +279,16 @@ namespace ts { // Testing only /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; - /*@internal*/ invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel): void; + /*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void; /*@internal*/ buildNextInvalidatedProject(): void; } interface InvalidatedProject { - project: ResolvedConfigFileName; - projectPath: ResolvedConfigFilePath; - reloadLevel: ConfigFileProgramReloadLevel; - projectIndex: number; + readonly project: ResolvedConfigFileName; + readonly projectPath: ResolvedConfigFilePath; + readonly reloadLevel: ConfigFileProgramReloadLevel; + readonly projectIndex: number; + readonly buildOrder: readonly ResolvedConfigFileName[]; } /** @@ -336,1279 +341,1418 @@ namespace ts { return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions); } + type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; + interface SolutionBuilderStateCache { + originalReadFile: CompilerHost["readFile"]; + originalFileExists: CompilerHost["fileExists"]; + originalDirectoryExists: CompilerHost["directoryExists"]; + originalCreateDirectory: CompilerHost["createDirectory"]; + originalWriteFile: CompilerHost["writeFile"] | undefined; + originalReadFileWithCache: CompilerHost["readFile"]; + originalGetSourceFile: CompilerHost["getSourceFile"]; + } + + interface SolutionBuilderState { + readonly host: SolutionBuilderHost; + readonly hostWithWatch: SolutionBuilderWithWatchHost; + readonly currentDirectory: string; + readonly getCanonicalFileName: GetCanonicalFileName; + readonly parseConfigFileHost: ParseConfigFileHost; + readonly writeFileName: ((s: string) => void) | undefined; + + // State of solution + readonly options: BuildOptions; + readonly baseCompilerOptions: CompilerOptions; + readonly rootNames: ReadonlyArray; + + readonly resolvedConfigFilePaths: Map; + readonly configFileCache: ConfigFileMap; + /** Map from config file name to up-to-date status */ + readonly projectStatus: ConfigFileMap; + readonly buildInfoChecked: ConfigFileMap; + readonly extendedConfigCache: Map; + + readonly builderPrograms: ConfigFileMap; + readonly diagnostics: ConfigFileMap; + readonly projectPendingBuild: ConfigFileMap; + readonly projectErrorsReported: ConfigFileMap; + + readonly compilerHost: CompilerHost; + readonly moduleResolutionCache: ModuleResolutionCache | undefined; + + // Mutable state + buildOrder: readonly ResolvedConfigFileName[] | undefined; + readFileWithCache: (f: string) => string | undefined; + projectCompilerOptions: CompilerOptions; + cache: SolutionBuilderStateCache | undefined; + allProjectBuildPending: boolean; + needsSummary: boolean; + watchAllProjectsPending: boolean; + + // Watch state + readonly watch: boolean; + readonly allWatchedWildcardDirectories: ConfigFileMap>; + readonly allWatchedInputFiles: ConfigFileMap>; + readonly allWatchedConfigFiles: ConfigFileMap; + + timerToBuildInvalidatedProject: any; + reportFileChangeDetected: boolean; + watchFile: WatchFile; + watchFilePath: WatchFilePath; + watchDirectory: WatchDirectory; + writeLog: (s: string) => void; + } + + function createSolutionBuilderState(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, options: BuildOptions): SolutionBuilderState { + const host = hostOrHostWithWatch as SolutionBuilderHost; + const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; + const currentDirectory = host.getCurrentDirectory(); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + + // State of the solution + const baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); + const compilerHost = createCompilerHostFromProgramHost(host, () => state.projectCompilerOptions); + setGetSourceFileAsHashVersioned(compilerHost, host); + compilerHost.getParsedCommandLine = fileName => parseConfigFile(state, fileName as ResolvedConfigFileName, toResolvedConfigFilePath(state, fileName as ResolvedConfigFileName)); + compilerHost.resolveModuleNames = maybeBind(host, host.resolveModuleNames); + compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives); + const moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined; + if (!compilerHost.resolveModuleNames) { + const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, 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, + + // Watch state + watch, + allWatchedWildcardDirectories: createConfigFileMap(), + allWatchedInputFiles: createConfigFileMap(), + allWatchedConfigFiles: createConfigFileMap(), + + timerToBuildInvalidatedProject: undefined, + reportFileChangeDetected: false, + watchFile, + watchFilePath, + watchDirectory, + writeLog, + }; + + return state; + } + + function toPath(state: SolutionBuilderState, fileName: string) { + return ts.toPath(fileName, state.currentDirectory, state.getCanonicalFileName); + } + + function toResolvedConfigFilePath(state: SolutionBuilderState, fileName: ResolvedConfigFileName): ResolvedConfigFilePath { + const { resolvedConfigFilePaths } = state; + const path = resolvedConfigFilePaths.get(fileName); + if (path !== undefined) return path; + + const resolvedPath = toPath(state, fileName) as ResolvedConfigFilePath; + resolvedConfigFilePaths.set(fileName, resolvedPath); + return resolvedPath; + } + + function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { + return !!(entry as ParsedCommandLine).options; + } + + function parseConfigFile(state: SolutionBuilderState, configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { + const { configFileCache } = state; + const value = configFileCache.get(configFilePath); + if (value) { + return isParsedCommandLine(value) ? value : undefined; + } + + let diagnostic: Diagnostic | undefined; + const { parseConfigFileHost, baseCompilerOptions, extendedConfigCache } = state; + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; + const parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; + configFileCache.set(configFilePath, parsed || diagnostic!); + return parsed; + } + + function resolveProjectName(state: SolutionBuilderState, name: string): ResolvedConfigFileName { + return resolveConfigFileProjectName(resolvePath(state.currentDirectory, name)); + } + + function createBuildOrder(state: SolutionBuilderState, roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { + const temporaryMarks = createMap() as ConfigFileMap; + const permanentMarks = createMap() as ConfigFileMap; + const circularityReportStack: string[] = []; + let buildOrder: ResolvedConfigFileName[] | undefined; + for (const root of roots) { + visit(root); + } + + return buildOrder || emptyArray; + + function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) { + const projPath = toResolvedConfigFilePath(state, configFileName); + // Already visited + if (permanentMarks.has(projPath)) return; + // Circular + if (temporaryMarks.has(projPath)) { + if (!inCircularContext) { + // TODO:: Do we report this as error? + reportStatus(state, Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); + } + return; + } + + temporaryMarks.set(projPath, true); + circularityReportStack.push(configFileName); + const parsed = parseConfigFile(state, configFileName, projPath); + if (parsed && parsed.projectReferences) { + for (const ref of parsed.projectReferences) { + const resolvedRefPath = resolveProjectName(state, ref.path); + visit(resolvedRefPath, inCircularContext || ref.circular); + } + } + + circularityReportStack.pop(); + permanentMarks.set(projPath, true); + (buildOrder || (buildOrder = [])).push(configFileName); + } + } + + function getBuildOrder(state: SolutionBuilderState) { + return state.buildOrder || + (state.buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)))); + } + + function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined) { + const resolvedProject = project && resolveProjectName(state, project); + if (resolvedProject) { + const projectPath = toResolvedConfigFilePath(state, resolvedProject); + const projectIndex = findIndex( + getBuildOrder(state), + configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath + ); + if (projectIndex === -1) return undefined; + } + return resolvedProject ? createBuildOrder(state, [resolvedProject]) : getBuildOrder(state); + } + + function enableCache(state: SolutionBuilderState) { + if (state.cache) { + disableCache(state); + } + + const { compilerHost, host } = state; + + const originalReadFileWithCache = state.readFileWithCache; + const originalGetSourceFile = compilerHost.getSourceFile; + + const { + originalReadFile, originalFileExists, originalDirectoryExists, + originalCreateDirectory, originalWriteFile, + getSourceFileWithCache, readFileWithCache + } = changeCompilerHostLikeToUseCache( + host, + fileName => toPath(state, fileName), + (...args) => originalGetSourceFile.call(compilerHost, ...args) + ); + state.readFileWithCache = readFileWithCache; + compilerHost.getSourceFile = getSourceFileWithCache!; + + state.cache = { + originalReadFile, + originalFileExists, + originalDirectoryExists, + originalCreateDirectory, + originalWriteFile, + originalReadFileWithCache, + originalGetSourceFile, + }; + } + + function disableCache(state: SolutionBuilderState) { + if (!state.cache) return; + + const { cache, host, compilerHost, extendedConfigCache, moduleResolutionCache } = state; + + host.readFile = cache.originalReadFile; + host.fileExists = cache.originalFileExists; + host.directoryExists = cache.originalDirectoryExists; + host.createDirectory = cache.originalCreateDirectory; + host.writeFile = cache.originalWriteFile; + compilerHost.getSourceFile = cache.originalGetSourceFile; + state.readFileWithCache = cache.originalReadFileWithCache; + extendedConfigCache.clear(); + if (moduleResolutionCache) { + moduleResolutionCache.directoryToModuleNameMap.clear(); + moduleResolutionCache.moduleNameToDirectoryMap.clear(); + } + state.cache = undefined; + } + + function clearProjectStatus(state: SolutionBuilderState, resolved: ResolvedConfigFilePath) { + state.projectStatus.delete(resolved); + state.diagnostics.delete(resolved); + } + + function addProjToQueue({ projectPendingBuild }: SolutionBuilderState, proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + const value = projectPendingBuild.get(proj); + if (value === undefined) { + projectPendingBuild.set(proj, reloadLevel); + } + else if (value < reloadLevel) { + projectPendingBuild.set(proj, reloadLevel); + } + } + + function setupInitialBuild(state: SolutionBuilderState, cancellationToken: CancellationToken | undefined) { + // Set initial build if not already built + if (!state.allProjectBuildPending) return; + state.allProjectBuildPending = false; + if (state.options.watch) { reportWatchStatus(state, Diagnostics.Starting_compilation_in_watch_mode); } + enableCache(state); + const buildOrder = getBuildOrder(state); + reportBuildQueue(state, buildOrder); + buildOrder.forEach(configFileName => + state.projectPendingBuild.set( + toResolvedConfigFilePath(state, configFileName), + ConfigFileProgramReloadLevel.None + ) + ); + + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); + } + } + + function getNextInvalidatedProject(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { + return state.projectPendingBuild.size ? + forEach(buildOrder, (project, projectIndex) => { + const projectPath = toResolvedConfigFilePath(state, project); + const reloadLevel = state.projectPendingBuild.get(projectPath); + if (reloadLevel !== undefined) { + return { project, projectPath, reloadLevel, projectIndex, buildOrder }; + } + }) : + undefined; + } + + function listEmittedFile({ writeFileName }: SolutionBuilderState, proj: ParsedCommandLine, file: string) { + if (writeFileName && proj.options.listEmittedFiles) { + writeFileName(`TSFILE: ${file}`); + } + } + + function getOldProgram({ options, builderPrograms, readFileWithCache }: SolutionBuilderState, proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (options.force) return undefined; + const value = builderPrograms.get(proj); + if (value) return value; + return readBuilderProgram(parsed.options, readFileWithCache) as any as T; + } + + function afterProgramCreate({ host, watch, builderPrograms }: SolutionBuilderState, proj: ResolvedConfigFilePath, program: T) { + if (host.afterProgramEmitAndDiagnostics) { + host.afterProgramEmitAndDiagnostics(program); + } + if (watch) { + program.releaseProgram(); + builderPrograms.set(proj, program); + } + } + + function 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; + } + + function buildSingleProject(state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, proj); + return BuildResultFlags.Success; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, proj); + + const { host, projectStatus, diagnostics, compilerHost, moduleResolutionCache, } = state; + const configFile = parseConfigFile(state, proj, resolvedPath); + if (!configFile) { + // Failed to read the config file + reportParseConfigFileDiagnostic(state, resolvedPath); + projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); + return BuildResultFlags.ConfigFileErrors; + } + + if (configFile.fileNames.length === 0) { + reportAndStoreErrors(state, resolvedPath, configFile.errors); + // Nothing to build - must be a solution file, basically + return BuildResultFlags.None; + } + + state.projectCompilerOptions = configFile.options; + // Update module resolution cache if needed + if (moduleResolutionCache) { + const projPath = toPath(state, proj); + if (moduleResolutionCache.directoryToModuleNameMap.redirectsMap.size === 0) { + // The own map will be for projectCompilerOptions + Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size === 0); + moduleResolutionCache.directoryToModuleNameMap.redirectsMap.set(projPath, moduleResolutionCache.directoryToModuleNameMap.ownMap); + moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.set(projPath, moduleResolutionCache.moduleNameToDirectoryMap.ownMap); + } + else { + // Set correct own map + Debug.assert(moduleResolutionCache.moduleNameToDirectoryMap.redirectsMap.size > 0); + + const ref: ResolvedProjectReference = { + sourceFile: configFile.options.configFile!, + commandLine: configFile + }; + moduleResolutionCache.directoryToModuleNameMap.setOwnMap(moduleResolutionCache.directoryToModuleNameMap.getOrCreateMapOfCacheRedirects(ref)); + moduleResolutionCache.moduleNameToDirectoryMap.setOwnMap(moduleResolutionCache.moduleNameToDirectoryMap.getOrCreateMapOfCacheRedirects(ref)); + } + moduleResolutionCache.directoryToModuleNameMap.setOwnOptions(configFile.options); + moduleResolutionCache.moduleNameToDirectoryMap.setOwnOptions(configFile.options); + } + + // Create program + const program = host.createProgram( + configFile.fileNames, + configFile.options, + compilerHost, + getOldProgram(state, resolvedPath, configFile), + configFile.errors, + configFile.projectReferences + ); + + // Don't emit anything in the presence of syntactic errors or options diagnostics + const syntaxDiagnostics = [ + ...program.getConfigFileParsingDiagnostics(), + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; + if (syntaxDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + program, + syntaxDiagnostics, + BuildResultFlags.SyntaxErrors, + "Syntactic" + ); + } + + // Same as above but now for semantic diagnostics + const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); + if (semanticDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + program, + semanticDiagnostics, + BuildResultFlags.TypeErrors, + "Semantic" + ); + } + + // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly + program.backupState(); + let declDiagnostics: Diagnostic[] | undefined; + const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); + const outputFiles: OutputFile[] = []; + emitFilesAndReportErrors( + program, + reportDeclarationDiagnostics, + /*writeFileName*/ undefined, + /*reportSummary*/ undefined, + (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), + cancellationToken + ); + // Don't emit .d.ts if there are decl file errors + if (declDiagnostics) { + program.restoreState(); + return buildErrors( + state, + resolvedPath, + program, + declDiagnostics, + BuildResultFlags.DeclarationEmitErrors, + "Declaration file" + ); + } + + // Actual Emit + let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; + let newestDeclarationFileContentChangedTime = minimumDate; + let anyDtsChanged = false; + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + let priorChangeTime: Date | undefined; + if (!anyDtsChanged && isDeclarationFile(name)) { + // Check for unchanged .d.ts files + if (host.fileExists(name) && state.readFileWithCache(name) === text) { + priorChangeTime = host.getModifiedTime(name); + } + else { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + anyDtsChanged = true; + } + } + + emittedOutputs.set(toPath(state, name), name); + writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); + } + }); + + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + program, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); + } + + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, configFile, name)); + listFiles(program, state.writeFileName); + } + + // Update time stamps for rest of the outputs + newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + diagnostics.delete(resolvedPath); + projectStatus.set(resolvedPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, + oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) + }); + afterProgramCreate(state, resolvedPath, program); + state.projectCompilerOptions = state.baseCompilerOptions; + return resultFlags; + } + + function updateBundle(state: SolutionBuilderState, proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); + return BuildResultFlags.Success; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, proj); + + // Update js, and source map + const { projectStatus, diagnostics, compilerHost } = state; + const config = Debug.assertDefined(parseConfigFile(state, proj, resolvedPath)); + state.projectCompilerOptions = config.options; + const outputFiles = emitUsingBuildInfo( + config, + compilerHost, + ref => { + const refName = resolveProjectName(state, ref.path); + return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName)); + }); + if (isString(outputFiles)) { + reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(state, outputFiles)); + return buildSingleProject(state, proj, resolvedPath, cancellationToken); + } + + // Actual Emit + Debug.assert(!!outputFiles.length); + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = createMap() as FileMap; + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + emittedOutputs.set(toPath(state, name), name); + writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + }); + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + return buildErrors( + state, + resolvedPath, + /*program*/ undefined, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + ); + } + + if (state.writeFileName) { + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); + } + + // Update timestamps for dts + const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, minimumDate, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + diagnostics.delete(resolvedPath); + projectStatus.set(resolvedPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + oldestOutputFileName: outputFiles[0].name + }); + state.projectCompilerOptions = state.baseCompilerOptions; + return BuildResultFlags.DeclarationOutputUnchanged; + } + + function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { + // Check tsconfig time + const tsconfigTime = state.host.getModifiedTime(configFile) || missingFileModifiedTime; + if (oldestOutputFileTime < tsconfigTime) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: configFile + }; + } + } + + function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; + const { host } = state; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!host.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; + } + + const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; + } + } + + // Container if no files are specified in the project + if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) { + return { + type: UpToDateStatusType.ContainerOnly + }; + } + + // 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 outputTime = host.getModifiedTime(output) || missingFileModifiedTime; + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; + } + + // 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; + } + + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; + } + + // Keep track of when the most recent time a .d.ts file was changed. + // In addition to file timestamps, we also keep track of when a .d.ts file + // had its file touched but not had its contents changed - this allows us + // to skip a downstream typecheck + if (isDeclarationFile(output)) { + const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); + } + } + + let pseudoUpToDate = false; + let usesPrepend = false; + let upstreamChangedProject: string | undefined; + if (project.projectReferences) { + state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.ComputingUpstream }); + for (const ref of project.projectReferences) { + usesPrepend = usesPrepend || !!(ref.prepend); + const resolvedRef = resolveProjectReferencePath(ref); + const resolvedRefPath = toResolvedConfigFilePath(state, resolvedRef); + const refStatus = getUpToDateStatus(state, parseConfigFile(state, resolvedRef, resolvedRefPath), resolvedRefPath); + + // 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 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 (missingOutputFileName !== undefined) { + return { + type: UpToDateStatusType.OutputMissing, + missingOutputFileName + }; + } + + if (isOutOfDateWithInputs) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: newestInputFileName + }; + } + else { + // Check tsconfig time + const configStatus = checkConfigFileUpToDateStatus(state, project.options.configFilePath!, oldestOutputFileTime, oldestOutputFileName); + if (configStatus) return configStatus; + + // Check extended config time + const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName)); + if (extendedConfigStatus) return extendedConfigStatus; + } + + if (!state.buildInfoChecked.has(resolvedPath)) { + state.buildInfoChecked.set(resolvedPath, true); + const buildInfoPath = getOutputPathForBuildInfo(project.options); + if (buildInfoPath) { + const value = state.readFileWithCache(buildInfoPath); + const buildInfo = value && getBuildInfo(value); + if (buildInfo && buildInfo.version !== version) { + return { + type: UpToDateStatusType.TsVersionOutputOfDate, + version: buildInfo.version + }; + } + } + } + + if (usesPrepend && pseudoUpToDate) { + return { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: upstreamChangedProject! + }; + } + + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName + }; + } + + function getUpToDateStatus(state: SolutionBuilderState, project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + } + + const prior = state.projectStatus.get(resolvedPath); + if (prior !== undefined) { + return prior; + } + + const actual = getUpToDateStatusWorker(state, project, resolvedPath); + state.projectStatus.set(resolvedPath, actual); + return actual; + } + + 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; + } + + if (reportVerbose) { + reportVerbose = false; + reportStatus(state, verboseMessage, proj.options.configFilePath!); + } + + if (isDeclarationFile(file)) { + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); + } + + host.setModifiedTime(file, now); + listEmittedFile(state, proj, file); + } + } + + 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 needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { + if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; + return config.fileNames.length === 0 || + !!config.errors.length || + !isIncrementalCompilation(config.options); + } + + function buildInvalidatedProject(state: SolutionBuilderState, { project, projectPath, reloadLevel, projectIndex, buildOrder }: InvalidatedProject, cancellationToken?: CancellationToken) { + const { options, projectPendingBuild } = state; + const config = parseConfigFile(state, project, projectPath); + if (!config) { + reportParseConfigFileDiagnostic(state, projectPath); + projectPendingBuild.delete(projectPath); + return; + } + + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + watchConfigFile(state, project, projectPath); + watchWildCardDirectories(state, project, projectPath, config); + watchInputFiles(state, project, projectPath, config); + } + else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { + // Update file names + const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost); + updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); + config.fileNames = result.fileNames; + watchInputFiles(state, project, projectPath, config); + } + + const status = getUpToDateStatus(state, config, projectPath); + verboseReportProjectStatus(state, project, status); + if (!options.force) { + if (status.type === UpToDateStatusType.UpToDate) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportStatus(state, Diagnostics.Project_0_is_up_to_date, project); + } + return; + } + + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + // Fake that files have been built by updating output file stamps + updateOutputTimestamps(state, config, projectPath); + return; + } + } + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + if (options.verbose) reportStatus(state, Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); + return; + } + + if (status.type === UpToDateStatusType.ContainerOnly) { + reportAndStoreErrors(state, projectPath, config.errors); + projectPendingBuild.delete(projectPath); + // Do nothing + return; + } + + const buildResult = needsBuild(state, status, config) ? + buildSingleProject(state, project, projectPath, cancellationToken) : // Actual build + updateBundle(state, project, projectPath, cancellationToken); // Fake that files have been built by manipulating prepend and existing output + projectPendingBuild.delete(projectPath); + // Only composite projects can be referenced by other projects + if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { + queueReferencingProjects(state, project, projectPath, projectIndex, buildOrder, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); + } + } + + function queueReferencingProjects( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + buildOrder: readonly ResolvedConfigFileName[], + declarationOutputChanged: boolean + ) { + // Always use build order to queue projects + for (let index = projectIndex + 1; index < buildOrder.length; index++) { + const nextProject = buildOrder[index]; + const nextProjectPath = toResolvedConfigFilePath(state, nextProject); + if (state.projectPendingBuild.has(nextProjectPath)) continue; + + const nextProjectConfig = parseConfigFile(state, nextProject, nextProjectPath); + if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; + for (const ref of nextProjectConfig.projectReferences) { + const resolvedRefPath = resolveProjectName(state, ref.path); + if (toResolvedConfigFilePath(state, resolvedRefPath) !== projectPath) continue; + // If the project is referenced with prepend, always build downstream projects, + // If declaration output is changed, build the project + // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps + const status = state.projectStatus.get(nextProjectPath); + if (status) { + switch (status.type) { + case UpToDateStatusType.UpToDate: + if (!declarationOutputChanged) { + if (ref.prepend) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: status.oldestOutputFileName, + newerProjectName: project + }); + } + else { + status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; + } + break; + } + + // falls through + case UpToDateStatusType.UpToDateWithUpstreamTypes: + case UpToDateStatusType.OutOfDateWithPrepend: + if (declarationOutputChanged) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, + newerProjectName: project + }); + } + break; + + case UpToDateStatusType.UpstreamBlocked: + if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) { + clearProjectStatus(state, nextProjectPath); + } + break; + } + } + addProjToQueue(state, nextProjectPath, ConfigFileProgramReloadLevel.None); + break; + } + } + } + + function buildNextProject(state: SolutionBuilderState, cancellationToken?: CancellationToken): SolutionBuilderResult | undefined { + setupInitialBuild(state, cancellationToken); + const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); + if (!invalidatedProject) return undefined; + + buildInvalidatedProject(state, invalidatedProject, cancellationToken); + return { + project: invalidatedProject.project, + result: state.diagnostics.has(invalidatedProject.projectPath) ? + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success + }; + } + + function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken): ExitStatus { + const buildOrder = getBuildOrderFor(state, project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + setupInitialBuild(state, cancellationToken); + + let successfulProjects = 0; + let errorProjects = 0; + while (true) { + const invalidatedProject = getNextInvalidatedProject(state, buildOrder); + if (!invalidatedProject) break; + buildInvalidatedProject(state, invalidatedProject, cancellationToken); + if (state.diagnostics.has(invalidatedProject.projectPath)) { + errorProjects++; + } + else { + successfulProjects++; + } + } + + disableCache(state); + reportErrorSummary(state); + startWatching(state); + + return errorProjects ? + successfulProjects ? + ExitStatus.DiagnosticsPresent_OutputsGenerated : + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; + } + + function clean(state: SolutionBuilderState, project?: string) { + const buildOrder = getBuildOrderFor(state, project); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + const { options, host } = state; + const filesToDelete = options.dry ? [] as string[] : undefined; + for (const proj of buildOrder) { + const resolvedPath = toResolvedConfigFilePath(state, proj); + const parsed = parseConfigFile(state, proj, resolvedPath); + if (parsed === undefined) { + // File has gone missing; fine to ignore here + reportParseConfigFileDiagnostic(state, resolvedPath); + continue; + } + const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); + for (const output of outputs) { + if (host.fileExists(output)) { + if (filesToDelete) { + filesToDelete.push(output); + } + else { + host.deleteFile(output); + invalidateProject(state, resolvedPath, ConfigFileProgramReloadLevel.None); + } + } + } + } + + if (filesToDelete) { + reportStatus(state, Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); + } + + return ExitStatus.Success; + } + + function invalidateProject(state: SolutionBuilderState, resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + state.configFileCache.delete(resolved); + state.buildOrder = undefined; + } + state.needsSummary = true; + clearProjectStatus(state, resolved); + addProjToQueue(state, resolved, reloadLevel); + enableCache(state); + } + + function 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 buildNextInvalidatedProject(state: SolutionBuilderState) { + state.timerToBuildInvalidatedProject = undefined; + if (state.reportFileChangeDetected) { + state.reportFileChangeDetected = false; + state.projectErrorsReported.clear(); + reportWatchStatus(state, Diagnostics.File_change_detected_Starting_incremental_compilation); + } + const invalidatedProject = getNextInvalidatedProject(state, getBuildOrder(state)); + if (invalidatedProject) { + buildInvalidatedProject(state, invalidatedProject); + if (state.projectPendingBuild.size) { + // Schedule next project for build + if (state.watch && !state.timerToBuildInvalidatedProject) { + scheduleBuildInvalidatedProject(state); + } + } + else { + disableCache(state); + reportErrorSummary(state); + } + } + } + + 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 + )); + } + + 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; + } + + // 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; + } + + // 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; + } + + // 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; + } + + if (isOutputFile(state, fileOrDirectory, parsed)) { + state.writeLog(`${fileOrDirectory} is output file`); + return; + } + + invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial); + }, + flags, + WatchType.WildcardDirectory, + resolved + ) + ); + } + + 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, + } + ); + } + + function startWatching(state: SolutionBuilderState) { + if (!state.watchAllProjectsPending) return; + state.watchAllProjectsPending = false; + for (const resolved of getBuildOrder(state)) { + const resolvedPath = toResolvedConfigFilePath(state, resolved); + // Watch this file + watchConfigFile(state, resolved, resolvedPath); + + 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); + } + } + } + /** * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references */ function createSolutionBuilderWorker(watch: false, host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions): SolutionBuilder { - const host = hostOrHostWithWatch as SolutionBuilderHost; - const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost; - const currentDirectory = host.getCurrentDirectory(); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - const parseConfigFileHost = parseConfigHostFromCompilerHostLike(host); - - // State of the solution - const options = defaultOptions; - const baseCompilerOptions = getCompilerOptionsOfBuildOptions(options); - const resolvedConfigFilePaths = createMap(); - type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; - const configFileCache = createMap() as ConfigFileMap; - /** Map from config file name to up-to-date status */ - const projectStatus = createMap() as ConfigFileMap; - let buildOrder: readonly ResolvedConfigFileName[] | undefined; - const writeFileName = host.trace ? (s: string) => host.trace!(s) : undefined; - let readFileWithCache = (f: string) => host.readFile(f); - let projectCompilerOptions = baseCompilerOptions; - const compilerHost = createCompilerHostFromProgramHost(host, () => projectCompilerOptions); - setGetSourceFileAsHashVersioned(compilerHost, host); - compilerHost.getParsedCommandLine = fileName => parseConfigFile(fileName as ResolvedConfigFileName, toResolvedConfigFilePath(fileName as ResolvedConfigFileName)); - - compilerHost.resolveModuleNames = maybeBind(host, host.resolveModuleNames); - compilerHost.resolveTypeReferenceDirectives = maybeBind(host, host.resolveTypeReferenceDirectives); - const moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined; - if (!compilerHost.resolveModuleNames) { - const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference).resolvedModule!; - compilerHost.resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference) => - loadWithLocalCache(Debug.assertEachDefined(moduleNames), containingFile, redirectedReference, loader); - } - let cacheState: { - originalReadFile: CompilerHost["readFile"]; - originalFileExists: CompilerHost["fileExists"]; - originalDirectoryExists: CompilerHost["directoryExists"]; - originalCreateDirectory: CompilerHost["createDirectory"]; - originalWriteFile: CompilerHost["writeFile"] | undefined; - originalReadFileWithCache: CompilerHost["readFile"]; - originalGetSourceFile: CompilerHost["getSourceFile"]; - } | undefined; - - const buildInfoChecked = createMap() as ConfigFileMap; - const extendedConfigCache = createMap(); - - // Watch state - const builderPrograms = createMap() as ConfigFileMap; - const diagnostics = createMap() as ConfigFileMap>; - const projectPendingBuild = createMap() as ConfigFileMap; - const projectErrorsReported = createMap() as ConfigFileMap; - let timerToBuildInvalidatedProject: any; - let reportFileChangeDetected = false; - const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); - - // Watches for the solution - const allWatchedWildcardDirectories = createMap() as ConfigFileMap>; - const allWatchedInputFiles = createMap() as ConfigFileMap>; - const allWatchedConfigFiles = createMap() as ConfigFileMap; - - let allProjectBuildPending = true; - let needsSummary = true; - let watchAllProjectsPending = watch; - + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: ReadonlyArray, options: BuildOptions): SolutionBuilder { + const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options); return { - build, - clean, - buildNextProject, - getBuildOrder, - getUpToDateStatusOfProject, - invalidateProject, - buildNextInvalidatedProject, + build: (project, cancellationToken) => build(state, project, cancellationToken), + clean: project => clean(state, project), + buildNextProject: cancellationToken => buildNextProject(state, cancellationToken), + getBuildOrder: () => getBuildOrder(state), + getUpToDateStatusOfProject: project => { + const configFileName = resolveProjectName(state, project); + const configFilePath = toResolvedConfigFilePath(state, configFileName); + return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath); + }, + invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None), + buildNextInvalidatedProject: () => buildNextInvalidatedProject(state), }; + } - function toPath(fileName: string) { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); - } + function relName(state: SolutionBuilderState, path: string): string { + return convertToRelativePath(path, state.currentDirectory, f => state.getCanonicalFileName(f)); + } - function toResolvedConfigFilePath(fileName: ResolvedConfigFileName): ResolvedConfigFilePath { - const path = resolvedConfigFilePaths.get(fileName); - if (path !== undefined) return path; + function reportStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: string[]) { + state.host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); + } - const resolvedPath = toPath(fileName) as ResolvedConfigFilePath; - resolvedConfigFilePaths.set(fileName, resolvedPath); - return resolvedPath; - } - - function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { - return !!(entry as ParsedCommandLine).options; - } - - function parseConfigFile(configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { - const value = configFileCache.get(configFilePath); - if (value) { - return isParsedCommandLine(value) ? value : undefined; - } - - let diagnostic: Diagnostic | undefined; - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - const parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache); - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; - configFileCache.set(configFilePath, parsed || diagnostic!); - return parsed; - } - - function reportStatus(message: DiagnosticMessage, ...args: string[]) { - host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); - } - - function reportWatchStatus(message: DiagnosticMessage, ...args: (string | number | undefined)[]) { - if (hostWithWatch.onWatchStatusChange) { - hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), host.getNewLine(), baseCompilerOptions); - } - } - - function startWatching() { - for (const resolved of getBuildOrder()) { - const resolvedPath = toResolvedConfigFilePath(resolved); - // Watch this file - watchConfigFile(resolved, resolvedPath); - - const cfg = parseConfigFile(resolved, resolvedPath); - if (cfg) { - // Update watchers for wildcard directories - watchWildCardDirectories(resolved, resolvedPath, cfg); - - // Watch input files - watchInputFiles(resolved, resolvedPath, cfg); - } - } - - } - - function watchConfigFile(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath) { - if (watch && !allWatchedConfigFiles.has(resolvedPath)) { - allWatchedConfigFiles.set(resolvedPath, watchFile( - hostWithWatch, - resolved, - () => { - invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.Full); - }, - PollingInterval.High, - WatchType.ConfigFile, - resolved - )); - } - } - - function watchWildCardDirectories(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!watch) return; - updateWatchingWildcardDirectories( - getOrCreateValueMapFromConfigFileMap(allWatchedWildcardDirectories, resolvedPath), - createMapFromTemplate(parsed.configFileSpecs!.wildcardDirectories), - (dir, flags) => { - return watchDirectory( - hostWithWatch, - dir, - fileOrDirectory => { - const fileOrDirectoryPath = toPath(fileOrDirectory); - if (fileOrDirectoryPath !== toPath(dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, parsed.options)) { - writeLog(`Project: ${resolved} Detected file add/remove of non supported extension: ${fileOrDirectory}`); - return; - } - - if (isOutputFile(fileOrDirectory, parsed)) { - writeLog(`${fileOrDirectory} is output file`); - return; - } - - invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.Partial); - }, - flags, - WatchType.WildcardDirectory, - resolved - ); - } - ); - } - - function watchInputFiles(resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!watch) return; - mutateMap( - getOrCreateValueMapFromConfigFileMap(allWatchedInputFiles, resolvedPath), - arrayToMap(parsed.fileNames, toPath), - { - createNewValue: (path, input) => watchFilePath( - hostWithWatch, - input, - () => invalidateProjectAndScheduleBuilds(resolvedPath, ConfigFileProgramReloadLevel.None), - PollingInterval.Low, - path as Path, - WatchType.SourceFile, - resolved - ), - onDeleteValue: closeFileWatcher, - } - ); - } - - 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; - } - - // 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; - } - - // If --outDir, check if file is in that directory - if (configFile.options.outDir && containsPath(configFile.options.outDir, fileName, currentDirectory, !host.useCaseSensitiveFileNames())) { - return true; - } - - return !forEach(configFile.fileNames, inputFile => isSameFile(fileName, inputFile)); - } - - function isSameFile(file1: string, file2: string) { - return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; - } - - function invalidateProjectAndScheduleBuilds(resolvedPath: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { - reportFileChangeDetected = true; - invalidateResolvedProject(resolvedPath, reloadLevel); - scheduleBuildInvalidatedProject(); - } - - function getUpToDateStatusOfProject(project: string): UpToDateStatus { - const configFileName = resolveProjectName(project); - const configFilePath = toResolvedConfigFilePath(configFileName); - return getUpToDateStatus(parseConfigFile(configFileName, configFilePath), configFilePath); - } - - function getBuildOrder() { - return buildOrder || (buildOrder = createBuildOrder(resolveProjectNames(rootNames))); - } - - function getUpToDateStatus(project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { - if (project === undefined) { - return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; - } - - const prior = projectStatus.get(resolvedPath); - if (prior !== undefined) { - return prior; - } - - const actual = getUpToDateStatusWorker(project, resolvedPath); - projectStatus.set(resolvedPath, actual); - return actual; - } - - function getUpToDateStatusWorker(project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { - let newestInputFileName: string = undefined!; - let newestInputFileTime = minimumDate; - // Get timestamps of input files - for (const inputFile of project.fileNames) { - if (!host.fileExists(inputFile)) { - return { - type: UpToDateStatusType.Unbuildable, - reason: `${inputFile} does not exist` - }; - } - - const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; - if (inputTime > newestInputFileTime) { - newestInputFileName = inputFile; - newestInputFileTime = inputTime; - } - } - - // Container if no files are specified in the project - if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) { - return { - type: UpToDateStatusType.ContainerOnly - }; - } - - // 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 outputTime = host.getModifiedTime(output) || missingFileModifiedTime; - if (outputTime < oldestOutputFileTime) { - oldestOutputFileTime = outputTime; - oldestOutputFileName = output; - } - - // 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; - } - - if (outputTime > newestOutputFileTime) { - newestOutputFileTime = outputTime; - newestOutputFileName = output; - } - - // Keep track of when the most recent time a .d.ts file was changed. - // In addition to file timestamps, we also keep track of when a .d.ts file - // had its file touched but not had its contents changed - this allows us - // to skip a downstream typecheck - if (isDeclarationFile(output)) { - const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); - } - } - - let pseudoUpToDate = false; - let usesPrepend = false; - let upstreamChangedProject: string | undefined; - if (project.projectReferences) { - projectStatus.set(resolvedPath, { type: UpToDateStatusType.ComputingUpstream }); - for (const ref of project.projectReferences) { - usesPrepend = usesPrepend || !!(ref.prepend); - const resolvedRef = resolveProjectReferencePath(ref); - const resolvedRefPath = toResolvedConfigFilePath(resolvedRef); - const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef, resolvedRefPath), resolvedRefPath); - - // Its a circular reference ignore the status of this project - if (refStatus.type === UpToDateStatusType.ComputingUpstream) { - continue; - } - - // An upstream project is blocked - if (refStatus.type === UpToDateStatusType.Unbuildable) { - return { - type: UpToDateStatusType.UpstreamBlocked, - upstreamProjectName: ref.path - }; - } - - // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) - if (refStatus.type !== UpToDateStatusType.UpToDate) { - return { - type: UpToDateStatusType.UpstreamOutOfDate, - upstreamProjectName: ref.path - }; - } - - // Check oldest output file name only if there is no missing output file name - if (!missingOutputFileName) { - // If the upstream project's newest file is older than our oldest output, we - // can't be out of date because of it - if (refStatus.newestInputFileTime && refStatus.newestInputFileTime <= oldestOutputFileTime) { - continue; - } - - // If the upstream project has only change .d.ts files, and we've built - // *after* those files, then we're "psuedo up to date" and eligible for a fast rebuild - if (refStatus.newestDeclarationFileContentChangedTime && refStatus.newestDeclarationFileContentChangedTime <= oldestOutputFileTime) { - pseudoUpToDate = true; - upstreamChangedProject = ref.path; - continue; - } - - // We have an output older than an upstream output - we are out of date - Debug.assert(oldestOutputFileName !== undefined, "Should have an oldest output filename here"); - return { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: ref.path - }; - } - } - } - - if (missingOutputFileName !== undefined) { - return { - type: UpToDateStatusType.OutputMissing, - missingOutputFileName - }; - } - - if (isOutOfDateWithInputs) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: newestInputFileName - }; - } - else { - // Check tsconfig time - const configStatus = checkConfigFileUpToDateStatus(project.options.configFilePath!, oldestOutputFileTime, oldestOutputFileName); - if (configStatus) return configStatus; - - // Check extended config time - const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(configFile, oldestOutputFileTime, oldestOutputFileName)); - if (extendedConfigStatus) return extendedConfigStatus; - } - - if (!buildInfoChecked.has(resolvedPath)) { - buildInfoChecked.set(resolvedPath, true); - const buildInfoPath = getOutputPathForBuildInfo(project.options); - if (buildInfoPath) { - const value = readFileWithCache(buildInfoPath); - const buildInfo = value && getBuildInfo(value); - if (buildInfo && buildInfo.version !== version) { - return { - type: UpToDateStatusType.TsVersionOutputOfDate, - version: buildInfo.version - }; - } - } - } - - if (usesPrepend && pseudoUpToDate) { - return { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: upstreamChangedProject! - }; - } - - // 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) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: configFile - }; - } - } - - function invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel) { - invalidateResolvedProject(toResolvedConfigFilePath(resolveProjectName(configFileName)), reloadLevel || ConfigFileProgramReloadLevel.None); - } - - function invalidateResolvedProject(resolved: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - configFileCache.delete(resolved); - buildOrder = undefined; - } - needsSummary = true; - clearProjectStatus(resolved); - addProjToQueue(resolved, reloadLevel); - enableCache(); - } - - function clearProjectStatus(resolved: ResolvedConfigFilePath) { - projectStatus.delete(resolved); - diagnostics.delete(resolved); - } - - /** - * return true if new addition - */ - function addProjToQueue(proj: ResolvedConfigFilePath, reloadLevel: ConfigFileProgramReloadLevel) { - const value = projectPendingBuild.get(proj); - if (value === undefined) { - projectPendingBuild.set(proj, reloadLevel); - } - else if (value < reloadLevel) { - projectPendingBuild.set(proj, reloadLevel); - } - } - - function getNextInvalidatedProject(buildOrder: readonly ResolvedConfigFileName[]): InvalidatedProject | undefined { - return hasPendingInvalidatedProjects() ? - forEach(buildOrder, (project, projectIndex) => { - const projectPath = toResolvedConfigFilePath(project); - const reloadLevel = projectPendingBuild.get(projectPath); - if (reloadLevel !== undefined) { - return { project, projectPath, reloadLevel, projectIndex }; - } - }) : - undefined; - } - - function hasPendingInvalidatedProjects() { - return !!projectPendingBuild.size; - } - - function scheduleBuildInvalidatedProject() { - if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) { - return; - } - if (timerToBuildInvalidatedProject) { - hostWithWatch.clearTimeout(timerToBuildInvalidatedProject); - } - timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildNextInvalidatedProject, 250); - } - - function buildNextInvalidatedProject() { - timerToBuildInvalidatedProject = undefined; - if (reportFileChangeDetected) { - reportFileChangeDetected = false; - projectErrorsReported.clear(); - reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); - } - const invalidatedProject = getNextInvalidatedProject(getBuildOrder()); - if (invalidatedProject) { - buildInvalidatedProject(invalidatedProject); - if (hasPendingInvalidatedProjects()) { - if (watch && !timerToBuildInvalidatedProject) { - scheduleBuildInvalidatedProject(); - } - } - else { - disableCache(); - reportErrorSummary(); - } - } - } - - function reportErrorSummary() { - if (watch || host.reportErrorSummary) { - needsSummary = false; - // Report errors from the other projects - getBuildOrder().forEach(project => { - const projectPath = toResolvedConfigFilePath(project); - if (!projectErrorsReported.has(projectPath)) { - reportErrors(diagnostics.get(projectPath) || emptyArray); - } - }); - let totalErrors = 0; - diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); - if (watch) { - reportWatchStatus(getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); - } - else { - host.reportErrorSummary!(totalErrors); - } - } - } - - function buildInvalidatedProject({ project, projectPath, reloadLevel, projectIndex }: InvalidatedProject, cancellationToken?: CancellationToken) { - const config = parseConfigFile(project, projectPath); - if (!config) { - reportParseConfigFileDiagnostic(projectPath); - projectPendingBuild.delete(projectPath); - return; - } - - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(project, projectPath); - watchWildCardDirectories(project, projectPath, config); - watchInputFiles(project, projectPath, config); - } - else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { - // Update file names - const result = getFileNamesFromConfigSpecs(config.configFileSpecs!, getDirectoryPath(project), config.options, parseConfigFileHost); - updateErrorForNoInputFiles(result, project, config.configFileSpecs!, config.errors, canJsonReportNoInutFiles(config.raw)); - config.fileNames = result.fileNames; - watchInputFiles(project, projectPath, config); - } - - const status = getUpToDateStatus(config, projectPath); - verboseReportProjectStatus(project, status); - if (status.type === UpToDateStatusType.UpToDate && !options.force) { - reportAndStoreErrors(projectPath, config.errors); - // Up to date, skip - if (options.dry) { - // In a dry build, inform the user of this fact - reportStatus(Diagnostics.Project_0_is_up_to_date, project); - } - projectPendingBuild.delete(projectPath); - return; - } - - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !options.force) { - reportAndStoreErrors(projectPath, config.errors); - // Fake that files have been built by updating output file stamps - updateOutputTimestamps(config, projectPath); - projectPendingBuild.delete(projectPath); - return; - } - - if (status.type === UpToDateStatusType.UpstreamBlocked) { - reportAndStoreErrors(projectPath, config.errors); - if (options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, project, status.upstreamProjectName); - projectPendingBuild.delete(projectPath); - return; - } - - if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(projectPath, config.errors); - // Do nothing - projectPendingBuild.delete(projectPath); - return; - } - - const buildResult = needsBuild(status, config) ? - buildSingleProject(project, projectPath, cancellationToken) : // Actual build - updateBundle(project, projectPath, cancellationToken); // Fake that files have been built by manipulating prepend and existing output - projectPendingBuild.delete(projectPath); - // Only composite projects can be referenced by other projects - if (!(buildResult & BuildResultFlags.AnyErrors) && config.options.composite) { - queueReferencingProjects(project, projectPath, projectIndex, !(buildResult & BuildResultFlags.DeclarationOutputUnchanged)); - } - } - - function queueReferencingProjects(project: ResolvedConfigFileName, projectPath: ResolvedConfigFilePath, projectIndex: number, declarationOutputChanged: boolean) { - // Always use build order to queue projects - const buildOrder = getBuildOrder(); - for (let index = projectIndex + 1; index < buildOrder.length; index++) { - const nextProject = buildOrder[index]; - const nextProjectPath = toResolvedConfigFilePath(nextProject); - if (projectPendingBuild.has(nextProjectPath)) continue; - - const nextProjectConfig = parseConfigFile(nextProject, nextProjectPath); - if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; - for (const ref of nextProjectConfig.projectReferences) { - const resolvedRefPath = resolveProjectName(ref.path); - if (toResolvedConfigFilePath(resolvedRefPath) !== projectPath) continue; - // If the project is referenced with prepend, always build downstream projects, - // If declaration output is changed, build the project - // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps - const status = projectStatus.get(nextProjectPath); - if (status) { - switch (status.type) { - case UpToDateStatusType.UpToDate: - if (!declarationOutputChanged) { - if (ref.prepend) { - projectStatus.set(nextProjectPath, { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: status.oldestOutputFileName, - newerProjectName: project - }); - } - else { - status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; - } - break; - } - - // falls through - case UpToDateStatusType.UpToDateWithUpstreamTypes: - case UpToDateStatusType.OutOfDateWithPrepend: - if (declarationOutputChanged) { - projectStatus.set(nextProjectPath, { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, - newerProjectName: project - }); - } - break; - - case UpToDateStatusType.UpstreamBlocked: - if (toResolvedConfigFilePath(resolveProjectName(status.upstreamProjectName)) === projectPath) { - clearProjectStatus(nextProjectPath); - } - break; - } - } - addProjToQueue(nextProjectPath, ConfigFileProgramReloadLevel.None); - break; - } - } - } - - function createBuildOrder(roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] { - const temporaryMarks = createMap() as ConfigFileMap; - const permanentMarks = createMap() as ConfigFileMap; - const circularityReportStack: string[] = []; - let buildOrder: ResolvedConfigFileName[] | undefined; - for (const root of roots) { - visit(root); - } - - return buildOrder || emptyArray; - - function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) { - const projPath = toResolvedConfigFilePath(configFileName); - // Already visited - if (permanentMarks.has(projPath)) return; - // Circular - if (temporaryMarks.has(projPath)) { - if (!inCircularContext) { - // TODO:: Do we report this as error? - reportStatus(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); - } - return; - } - - temporaryMarks.set(projPath, true); - circularityReportStack.push(configFileName); - const parsed = parseConfigFile(configFileName, projPath); - if (parsed && parsed.projectReferences) { - for (const ref of parsed.projectReferences) { - const resolvedRefPath = resolveProjectName(ref.path); - visit(resolvedRefPath, inCircularContext || ref.circular); - } - } - - circularityReportStack.pop(); - permanentMarks.set(projPath, true); - (buildOrder || (buildOrder = [])).push(configFileName); - } - } - - function buildSingleProject(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj); - return BuildResultFlags.Success; - } - - if (options.verbose) reportStatus(Diagnostics.Building_project_0, proj); - - let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; - - const configFile = parseConfigFile(proj, resolvedPath); - if (!configFile) { - // Failed to read the config file - resultFlags |= BuildResultFlags.ConfigFileErrors; - reportParseConfigFileDiagnostic(resolvedPath); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); - return resultFlags; - } - if (configFile.fileNames.length === 0) { - reportAndStoreErrors(resolvedPath, configFile.errors); - // Nothing to build - must be a solution file, basically - return BuildResultFlags.None; - } - - // 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); - - 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); - } - - const program = host.createProgram( - configFile.fileNames, - configFile.options, - compilerHost, - getOldProgram(resolvedPath, configFile), - configFile.errors, - configFile.projectReferences - ); - - // Don't emit anything in the presence of syntactic errors or options diagnostics - const syntaxDiagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken)]; - if (syntaxDiagnostics.length) { - return buildErrors(syntaxDiagnostics, BuildResultFlags.SyntaxErrors, "Syntactic"); - } - - // Same as above but now for semantic diagnostics - const semanticDiagnostics = program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); - if (semanticDiagnostics.length) { - return buildErrors(semanticDiagnostics, BuildResultFlags.TypeErrors, "Semantic"); - } - - // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly - program.backupState(); - let newestDeclarationFileContentChangedTime = minimumDate; - let anyDtsChanged = false; - let declDiagnostics: Diagnostic[] | undefined; - const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); - const outputFiles: OutputFile[] = []; - emitFilesAndReportErrors( - program, - reportDeclarationDiagnostics, - /*writeFileName*/ undefined, - /*reportSummary*/ undefined, - (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), - cancellationToken - ); - // Don't emit .d.ts if there are decl file errors - if (declDiagnostics) { - program.restoreState(); - return buildErrors(declDiagnostics, BuildResultFlags.DeclarationEmitErrors, "Declaration file"); - } - - // Actual Emit - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - let priorChangeTime: Date | undefined; - if (!anyDtsChanged && isDeclarationFile(name)) { - // Check for unchanged .d.ts files - if (host.fileExists(name) && readFileWithCache(name) === text) { - priorChangeTime = host.getModifiedTime(name); - } - else { - resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - anyDtsChanged = true; - } - } - - emittedOutputs.set(toPath(name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - if (priorChangeTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - } - }); - - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - return buildErrors(emitDiagnostics, BuildResultFlags.EmitErrors, "Emit"); - } - - if (writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(configFile, name)); - listFiles(program, writeFileName); - } - - // Update time stamps for rest of the outputs - newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(configFile, newestDeclarationFileContentChangedTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: anyDtsChanged ? maximumDate : newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(configFile, !host.useCaseSensitiveFileNames()) - }; - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, status); - afterProgramCreate(resolvedPath, program); - projectCompilerOptions = baseCompilerOptions; - return resultFlags; - - function buildErrors(diagnostics: ReadonlyArray, errorFlags: BuildResultFlags, errorType: string) { - resultFlags |= errorFlags; - reportAndStoreErrors(resolvedPath, diagnostics); - // List files if any other build error using program (emit errors already report files) - if (writeFileName) listFiles(program, writeFileName); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); - afterProgramCreate(resolvedPath, program); - projectCompilerOptions = baseCompilerOptions; - return resultFlags; - } - } - - function listEmittedFile(proj: ParsedCommandLine, file: string) { - if (writeFileName && proj.options.listEmittedFiles) { - writeFileName(`TSFILE: ${file}`); - } - } - - function afterProgramCreate(proj: ResolvedConfigFilePath, program: T) { - if (host.afterProgramEmitAndDiagnostics) { - host.afterProgramEmitAndDiagnostics(program); - } - if (watch) { - program.releaseProgram(); - builderPrograms.set(proj, program); - } - } - - function getOldProgram(proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (options.force) return undefined; - const value = builderPrograms.get(proj); - if (value) return value; - return readBuilderProgram(parsed.options, readFileWithCache) as any as T; - } - - function updateBundle(proj: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, cancellationToken: CancellationToken | undefined): BuildResultFlags { - if (options.dry) { - reportStatus(Diagnostics.A_non_dry_build_would_update_output_of_project_0, proj); - return BuildResultFlags.Success; - } - - if (options.verbose) reportStatus(Diagnostics.Updating_output_of_project_0, proj); - - // Update js, and source map - const config = Debug.assertDefined(parseConfigFile(proj, resolvedPath)); - projectCompilerOptions = config.options; - const outputFiles = emitUsingBuildInfo( - config, - compilerHost, - ref => { - const refName = resolveProjectName(ref.path); - return parseConfigFile(refName, toResolvedConfigFilePath(refName)); - }); - if (isString(outputFiles)) { - reportStatus(Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, proj, relName(outputFiles)); - return buildSingleProject(proj, resolvedPath, cancellationToken); - } - - // Actual Emit - Debug.assert(!!outputFiles.length); - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = createMap() as FileMap; - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - emittedOutputs.set(toPath(name), name); - writeFile(compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - }); - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - reportAndStoreErrors(resolvedPath, emitDiagnostics); - projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: "Emit errors" }); - projectCompilerOptions = baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged | BuildResultFlags.EmitErrors; - } - - if (writeFileName) { - emittedOutputs.forEach(name => listEmittedFile(config, name)); - } - - // Update timestamps for dts - const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(config, minimumDate, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - oldestOutputFileName: outputFiles[0].name - }; - - diagnostics.delete(resolvedPath); - projectStatus.set(resolvedPath, status); - projectCompilerOptions = baseCompilerOptions; - return BuildResultFlags.DeclarationOutputUnchanged; - } - - function updateOutputTimestamps(proj: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath) { - if (options.dry) { - return reportStatus(Diagnostics.A_non_dry_build_would_update_timestamps_for_output_of_project_0, proj.options.configFilePath!); - } - const priorNewestUpdateTime = updateOutputTimestampsWorker(proj, minimumDate, Diagnostics.Updating_output_timestamps_of_project_0); - const status: Status.UpToDate = { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: priorNewestUpdateTime, - oldestOutputFileName: getFirstProjectOutput(proj, !host.useCaseSensitiveFileNames()) - }; - projectStatus.set(resolvedPath, status); - } - - function updateOutputTimestampsWorker(proj: ParsedCommandLine, priorNewestUpdateTime: Date, verboseMessage: DiagnosticMessage, skipOutputs?: FileMap) { - const outputs = getAllProjectOutputs(proj, !host.useCaseSensitiveFileNames()); - if (!skipOutputs || outputs.length !== skipOutputs.size) { - if (options.verbose) { - reportStatus(verboseMessage, proj.options.configFilePath!); - } - const now = host.now ? host.now() : new Date(); - for (const file of outputs) { - if (skipOutputs && skipOutputs.has(toPath(file))) { - continue; - } - - if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); - } - - host.setModifiedTime(file, now); - listEmittedFile(proj, file); - } - } - - return priorNewestUpdateTime; - } - - function clean(project?: string) { - const buildOrder = getBuildOrderFor(project); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - - const filesToDelete = options.dry ? [] as string[] : undefined; - for (const proj of buildOrder) { - const resolvedPath = toResolvedConfigFilePath(proj); - const parsed = parseConfigFile(proj, resolvedPath); - if (parsed === undefined) { - // File has gone missing; fine to ignore here - reportParseConfigFileDiagnostic(resolvedPath); - continue; - } - const outputs = getAllProjectOutputs(parsed, !host.useCaseSensitiveFileNames()); - for (const output of outputs) { - if (host.fileExists(output)) { - if (filesToDelete) { - filesToDelete.push(output); - } - else { - host.deleteFile(output); - invalidateResolvedProject(resolvedPath, ConfigFileProgramReloadLevel.None); - } - } - } - } - - if (filesToDelete) { - reportStatus(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); - } - - return ExitStatus.Success; - } - - function resolveProjectName(name: string): ResolvedConfigFileName { - return resolveConfigFileProjectName(resolvePath(host.getCurrentDirectory(), name)); - } - - function resolveProjectNames(configFileNames: ReadonlyArray): ResolvedConfigFileName[] { - return configFileNames.map(resolveProjectName); - } - - function enableCache() { - if (cacheState) { - disableCache(); - } - - const originalReadFileWithCache = readFileWithCache; - const originalGetSourceFile = compilerHost.getSourceFile; - - const { originalReadFile, originalFileExists, originalDirectoryExists, - originalCreateDirectory, originalWriteFile, getSourceFileWithCache, - readFileWithCache: newReadFileWithCache - } = changeCompilerHostLikeToUseCache(host, toPath, (...args) => originalGetSourceFile.call(compilerHost, ...args)); - readFileWithCache = newReadFileWithCache; - compilerHost.getSourceFile = getSourceFileWithCache!; - - cacheState = { - originalReadFile, - originalFileExists, - originalDirectoryExists, - originalCreateDirectory, - originalWriteFile, - originalReadFileWithCache, - originalGetSourceFile, - }; - } - - function disableCache() { - if (!cacheState) return; - - host.readFile = cacheState.originalReadFile; - host.fileExists = cacheState.originalFileExists; - host.directoryExists = cacheState.originalDirectoryExists; - host.createDirectory = cacheState.originalCreateDirectory; - host.writeFile = cacheState.originalWriteFile; - compilerHost.getSourceFile = cacheState.originalGetSourceFile; - readFileWithCache = cacheState.originalReadFileWithCache; - extendedConfigCache.clear(); - if (moduleResolutionCache) { - moduleResolutionCache.directoryToModuleNameMap.clear(); - moduleResolutionCache.moduleNameToDirectoryMap.clear(); - } - cacheState = undefined; - } - - function getBuildOrderFor(project: string | undefined) { - const resolvedProject = project && resolveProjectName(project); - if (resolvedProject) { - const projectPath = toResolvedConfigFilePath(resolvedProject); - const projectIndex = findIndex( - getBuildOrder(), - configFileName => toResolvedConfigFilePath(configFileName) === projectPath - ); - if (projectIndex === -1) return undefined; - } - return resolvedProject ? createBuildOrder([resolvedProject]) : getBuildOrder(); - } - - function setupInitialBuild(cancellationToken: CancellationToken | undefined) { - // Set initial build if not already built - if (allProjectBuildPending) { - allProjectBuildPending = false; - if (options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } - enableCache(); - const buildOrder = getBuildOrder(); - reportBuildQueue(buildOrder); - buildOrder.forEach(configFileName => - projectPendingBuild.set(toResolvedConfigFilePath(configFileName), ConfigFileProgramReloadLevel.None)); - - if (cancellationToken) { - cancellationToken.throwIfCancellationRequested(); - } - } - } - - function buildNextProject(cancellationToken?: CancellationToken): SolutionBuilderResult | undefined { - setupInitialBuild(cancellationToken); - const invalidatedProject = getNextInvalidatedProject(getBuildOrder()); - if (!invalidatedProject) return undefined; - - buildInvalidatedProject(invalidatedProject, cancellationToken); - return { - project: invalidatedProject.project, - result: diagnostics.has(invalidatedProject.projectPath) ? - ExitStatus.DiagnosticsPresent_OutputsSkipped : - ExitStatus.Success - }; - } - - function build(project?: string, cancellationToken?: CancellationToken): ExitStatus { - const buildOrder = getBuildOrderFor(project); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - - setupInitialBuild(cancellationToken); - - let successfulProjects = 0; - let errorProjects = 0; - while (true) { - const invalidatedProject = getNextInvalidatedProject(buildOrder); - if (!invalidatedProject) { - if (needsSummary) { - disableCache(); - reportErrorSummary(); - } - if (watchAllProjectsPending) { - watchAllProjectsPending = false; - startWatching(); - } - break; - } - - buildInvalidatedProject(invalidatedProject, cancellationToken); - if (diagnostics.has(invalidatedProject.projectPath)) { - errorProjects++; - } - else { - successfulProjects++; - } - } - - return errorProjects ? - successfulProjects ? - ExitStatus.DiagnosticsPresent_OutputsGenerated : - ExitStatus.DiagnosticsPresent_OutputsSkipped : - ExitStatus.Success; - } - - function needsBuild(status: UpToDateStatus, config: ParsedCommandLine) { - if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; - return config.fileNames.length === 0 || - !!config.errors.length || - !isIncrementalCompilation(config.options); - } - - function reportParseConfigFileDiagnostic(proj: ResolvedConfigFilePath) { - reportAndStoreErrors(proj, [configFileCache.get(proj) as Diagnostic]); - } - - function reportAndStoreErrors(proj: ResolvedConfigFilePath, errors: ReadonlyArray) { - reportErrors(errors); - projectErrorsReported.set(proj, true); - if (errors.length) { - diagnostics.set(proj, errors); - } - } - - function reportErrors(errors: ReadonlyArray) { - errors.forEach(err => host.reportDiagnostic(err)); - } - - /** - * Report the build ordering inferred from the current project graph if we're in verbose mode - */ - function reportBuildQueue(buildQueue: readonly ResolvedConfigFileName[]) { - if (options.verbose) { - reportStatus(Diagnostics.Projects_in_this_build_Colon_0, buildQueue.map(s => "\r\n * " + relName(s)).join("")); - } - } - - function relName(path: string): string { - return convertToRelativePath(path, host.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); - } - - /** - * 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 reportWatchStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { + if (state.hostWithWatch.onWatchStatusChange) { + state.hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions); } } - function formatUpToDateStatus(configFileName: string, status: UpToDateStatus, relName: (fileName: string) => string, formatMessage: (message: DiagnosticMessage, ...args: string[]) => T) { + function reportErrors({ host }: SolutionBuilderState, errors: ReadonlyArray) { + errors.forEach(err => host.reportDiagnostic(err)); + } + + function reportAndStoreErrors(state: SolutionBuilderState, proj: ResolvedConfigFilePath, errors: ReadonlyArray) { + reportErrors(state, errors); + state.projectErrorsReported.set(proj, true); + if (errors.length) { + state.diagnostics.set(proj, errors); + } + } + + function reportParseConfigFileDiagnostic(state: SolutionBuilderState, proj: ResolvedConfigFilePath) { + reportAndStoreErrors(state, proj, [state.configFileCache.get(proj) as Diagnostic]); + } + + function reportErrorSummary(state: SolutionBuilderState) { + if (!state.needsSummary || (!state.watch && !state.host.reportErrorSummary)) return; + state.needsSummary = false; + const { diagnostics } = state; + // Report errors from the other projects + getBuildOrder(state).forEach(project => { + const projectPath = toResolvedConfigFilePath(state, project); + if (!state.projectErrorsReported.has(projectPath)) { + reportErrors(state, diagnostics.get(projectPath) || emptyArray); + } + }); + let totalErrors = 0; + diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); + if (state.watch) { + reportWatchStatus(state, getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); + } + else { + state.host.reportErrorSummary!(totalErrors); + } + } + + /** + * Report the build ordering inferred from the current project graph if we're in verbose mode + */ + function reportBuildQueue(state: SolutionBuilderState, buildQueue: readonly ResolvedConfigFileName[]) { + if (state.options.verbose) { + reportStatus(state, Diagnostics.Projects_in_this_build_Colon_0, buildQueue.map(s => "\r\n * " + relName(state, s)).join("")); + } + } + + function reportUpToDateStatus(state: SolutionBuilderState, configFileName: string, status: UpToDateStatus) { switch (status.type) { case UpToDateStatusType.OutOfDateWithSelf: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - relName(configFileName), - relName(status.outOfDateOutputFileName), - relName(status.newerInputFileName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(state, configFileName), + relName(state, status.outOfDateOutputFileName), + relName(state, status.newerInputFileName) + ); case UpToDateStatusType.OutOfDateWithUpstream: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, - relName(configFileName), - relName(status.outOfDateOutputFileName), - relName(status.newerProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, + relName(state, configFileName), + relName(state, status.outOfDateOutputFileName), + relName(state, status.newerProjectName) + ); case UpToDateStatusType.OutputMissing: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, - relName(configFileName), - relName(status.missingOutputFileName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, + relName(state, configFileName), + relName(state, status.missingOutputFileName) + ); case UpToDateStatusType.UpToDate: if (status.newestInputFileTime !== undefined) { - return formatMessage(Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, - relName(configFileName), - relName(status.newestInputFileName || ""), - relName(status.oldestOutputFileName || "")); + return reportStatus( + state, + Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, + relName(state, configFileName), + relName(state, status.newestInputFileName || ""), + relName(state, status.oldestOutputFileName || "") + ); } // Don't report anything for "up to date because it was already built" -- too verbose break; case UpToDateStatusType.OutOfDateWithPrepend: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed, - relName(configFileName), - relName(status.newerProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed, + relName(state, configFileName), + relName(state, status.newerProjectName) + ); case UpToDateStatusType.UpToDateWithUpstreamTypes: - return formatMessage(Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, - relName(configFileName)); + return reportStatus( + state, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + relName(state, configFileName) + ); case UpToDateStatusType.UpstreamOutOfDate: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, - relName(configFileName), - relName(status.upstreamProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_its_dependency_1_is_out_of_date, + relName(state, configFileName), + relName(state, status.upstreamProjectName) + ); case UpToDateStatusType.UpstreamBlocked: - return formatMessage(Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, - relName(configFileName), - relName(status.upstreamProjectName)); + return reportStatus( + state, + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + relName(state, configFileName), + relName(state, status.upstreamProjectName) + ); case UpToDateStatusType.Unbuildable: - return formatMessage(Diagnostics.Failed_to_parse_file_0_Colon_1, - relName(configFileName), - status.reason); + return reportStatus( + state, + Diagnostics.Failed_to_parse_file_0_Colon_1, + relName(state, configFileName), + status.reason + ); case UpToDateStatusType.TsVersionOutputOfDate: - return formatMessage(Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, - relName(configFileName), + return reportStatus( + state, + Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, + relName(state, configFileName), status.version, - version); + version + ); case UpToDateStatusType.ContainerOnly: - // Don't report status on "solution" projects + // Don't report status on "solution" projects case UpToDateStatusType.ComputingUpstream: // Should never leak from getUptoDateStatusWorker break; @@ -1616,4 +1760,13 @@ namespace ts { assertType(status); } } + + /** + * Report the up-to-date status of a project if we're in verbose mode + */ + function verboseReportProjectStatus(state: SolutionBuilderState, configFileName: string, status: UpToDateStatus) { + if (state.options.verbose) { + reportUpToDateStatus(state, configFileName, status); + } + } } diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 7f9f2260d6d..056e894bf53 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -437,7 +437,7 @@ export class cNew {}`); function verifyInvalidation(expectedToWriteTests: boolean) { // Rebuild this project tick(); - builder.invalidateProject("/src/logic"); + builder.invalidateProject("/src/logic/tsconfig.json" as ResolvedConfigFilePath); builder.buildNextInvalidatedProject(); // The file should be updated assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 70fe602c8a2..f67b575e979 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -721,7 +721,7 @@ let x: string = 10;`); host.writeFile(logic[1].path, `${logic[1].content} function foo() { }`); - solutionBuilder.invalidateProject(`${project}/${SubProject.logic}`); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); // not ideal, but currently because of d.ts but no new file is written @@ -734,7 +734,7 @@ function foo() { host.writeFile(logic[1].path, `${logic[1].content} export function gfoo() { }`); - solutionBuilder.invalidateProject(logic[0].path); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); }, expectedProgramFiles); }); @@ -745,7 +745,7 @@ export function gfoo() { compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, references: [{ path: "../core" }] })); - solutionBuilder.invalidateProject(logic[0].path, ConfigFileProgramReloadLevel.Full); + solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath, ConfigFileProgramReloadLevel.Full); solutionBuilder.buildNextInvalidatedProject(); }, [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]); }); @@ -965,7 +965,7 @@ export function gfoo() { host.writeFile(bTs.path, `${bTs.content} export function gfoo() { }`); - solutionBuilder.invalidateProject(bTsconfig.path); + solutionBuilder.invalidateProject(bTsconfig.path.toLowerCase() as ResolvedConfigFilePath); solutionBuilder.buildNextInvalidatedProject(); }, emptyArray,