diff --git a/src/compiler/tsbuildPublic.ts b/src/compiler/tsbuildPublic.ts index a6f71ec4d4b18..1845589d745dc 100644 --- a/src/compiler/tsbuildPublic.ts +++ b/src/compiler/tsbuildPublic.ts @@ -1,2146 +1,2147 @@ -namespace ts { - const minimumDate = new Date(-8640000000000000); - const maximumDate = new Date(8640000000000000); - - export interface BuildOptions { - dry?: boolean; - force?: boolean; - verbose?: boolean; - - /*@internal*/ clean?: boolean; - /*@internal*/ watch?: boolean; - /*@internal*/ help?: boolean; - - /*@internal*/ preserveWatchOutput?: boolean; - /*@internal*/ listEmittedFiles?: boolean; - /*@internal*/ listFiles?: boolean; - /*@internal*/ explainFiles?: boolean; - /*@internal*/ pretty?: boolean; - incremental?: boolean; - assumeChangesOnlyAffectDirectDependencies?: boolean; - - traceResolution?: boolean; - /* @internal */ diagnostics?: boolean; - /* @internal */ extendedDiagnostics?: boolean; - /* @internal */ locale?: string; - /* @internal */ generateCpuProfile?: string; - /* @internal */ generateTrace?: string; - - [option: string]: CompilerOptionsValue | undefined; - } - - enum BuildResultFlags { - None = 0, - - /** - * No errors of any kind occurred during build - */ - Success = 1 << 0, - /** - * None of the .d.ts files emitted by this build were - * different from the existing files on disk - */ - DeclarationOutputUnchanged = 1 << 1, - - ConfigFileErrors = 1 << 2, - SyntaxErrors = 1 << 3, - TypeErrors = 1 << 4, - DeclarationEmitErrors = 1 << 5, - EmitErrors = 1 << 6, - - AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors - } - - /*@internal*/ - export type ResolvedConfigFilePath = ResolvedConfigFileName & Path; - - function getOrCreateValueFromConfigFileMap(configFileMap: ESMap, resolved: ResolvedConfigFilePath, createT: () => T): T { - const existingValue = configFileMap.get(resolved); - let newValue: T | undefined; - if (!existingValue) { - newValue = createT(); - configFileMap.set(resolved, newValue); - } - return existingValue || newValue!; - } - - function getOrCreateValueMapFromConfigFileMap(configFileMap: ESMap>, resolved: ResolvedConfigFilePath): ESMap { - return getOrCreateValueFromConfigFileMap>(configFileMap, resolved, () => new Map()); - } - - function newer(date1: Date, date2: Date): Date { - return date2 > date1 ? date2 : date1; - } - - function isDeclarationFile(fileName: string) { - return fileExtensionIs(fileName, Extension.Dts); - } - - export type ReportEmitErrorSummary = (errorCount: number) => void; - - export interface SolutionBuilderHostBase extends ProgramHost { - createDirectory?(path: string): void; - /** - * Should provide create directory and writeFile if done of invalidatedProjects is not invoked with - * writeFileCallback - */ - writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; - getCustomTransformers?: (project: string) => CustomTransformers | undefined; - - getModifiedTime(fileName: string): Date | undefined; - setModifiedTime(fileName: string, date: Date): void; - deleteFile(fileName: string): void; - getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; - - reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here - reportSolutionBuilderStatus: DiagnosticReporter; - - // TODO: To do better with watch mode and normal build mode api that creates program and emits files - // This currently helps enable --diagnostics and --extendedDiagnostics - afterProgramEmitAndDiagnostics?(program: T): void; - /*@internal*/ afterEmitBundle?(config: ParsedCommandLine): void; - - // For testing - /*@internal*/ now?(): Date; - } - - export interface SolutionBuilderHost extends SolutionBuilderHostBase { - reportErrorSummary?: ReportEmitErrorSummary; - } - - export interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { - } - - /*@internal*/ - export type BuildOrder = readonly ResolvedConfigFileName[]; - /*@internal*/ - export interface CircularBuildOrder { - buildOrder: BuildOrder; - circularDiagnostics: readonly Diagnostic[]; - } - /*@internal*/ - export type AnyBuildOrder = BuildOrder | CircularBuildOrder; - - /*@internal*/ - export function isCircularBuildOrder(buildOrder: AnyBuildOrder): buildOrder is CircularBuildOrder { - return !!buildOrder && !!(buildOrder as CircularBuildOrder).buildOrder; - } - - /*@internal*/ - export function getBuildOrderFromAnyBuildOrder(anyBuildOrder: AnyBuildOrder): BuildOrder { - return isCircularBuildOrder(anyBuildOrder) ? anyBuildOrder.buildOrder : anyBuildOrder; - } - - export interface SolutionBuilder { - build(project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; - clean(project?: string): ExitStatus; - buildReferences(project: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; - cleanReferences(project?: string): ExitStatus; - getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; - - // Currently used for testing but can be made public if needed: - /*@internal*/ getBuildOrder(): AnyBuildOrder; - - // Testing only - /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; - /*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void; - /*@internal*/ buildNextInvalidatedProject(): void; - /*@internal*/ getAllParsedConfigs(): readonly ParsedCommandLine[]; - /*@internal*/ close(): void; - } - - /** - * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic - */ - export function createBuilderStatusReporter(system: System, pretty?: boolean): DiagnosticReporter { - return diagnostic => { - let output = pretty ? `[${formatColorAndReset(getLocaleTimeString(system), ForegroundColorEscapeSequences.Grey)}] ` : `${getLocaleTimeString(system)} - `; - output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine}`; - system.write(output); - }; - } - - function createSolutionBuilderHostBase(system: System, createProgram: CreateProgram | undefined, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter) { - const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase; - host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined; - host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop; - host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; - host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); - host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system); - host.now = maybeBind(system, system.now); // For testing - return host; - } - - export function createSolutionBuilderHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary) { - const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderHost; - host.reportErrorSummary = reportErrorSummary; - return host; - } - - export function createSolutionBuilderWithWatchHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter) { - const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderWithWatchHost; - const watchHost = createWatchHost(system, reportWatchStatus); - copyProperties(host, watchHost); - return host; - } - - function getCompilerOptionsOfBuildOptions(buildOptions: BuildOptions): CompilerOptions { - const result = {} as CompilerOptions; - commonOptionsWithBuild.forEach(option => { - if (hasProperty(buildOptions, option.name)) result[option.name] = buildOptions[option.name]; - }); - return result; - } - - export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder { - return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions); - } - - export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { - return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions, baseWatchOptions); - } - - 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 extends WatchFactory { - readonly host: SolutionBuilderHost; - readonly hostWithWatch: SolutionBuilderWithWatchHost; - readonly currentDirectory: string; - readonly getCanonicalFileName: GetCanonicalFileName; - readonly parseConfigFileHost: ParseConfigFileHost; - readonly write: ((s: string) => void) | undefined; - - // State of solution - readonly options: BuildOptions; - readonly baseCompilerOptions: CompilerOptions; - readonly rootNames: readonly string[]; - readonly baseWatchOptions: WatchOptions | undefined; - - readonly resolvedConfigFilePaths: ESMap; - readonly configFileCache: ESMap; - /** Map from config file name to up-to-date status */ - readonly projectStatus: ESMap; - readonly buildInfoChecked: ESMap; - readonly extendedConfigCache: ESMap; - - readonly builderPrograms: ESMap; - readonly diagnostics: ESMap; - readonly projectPendingBuild: ESMap; - readonly projectErrorsReported: ESMap; - - readonly compilerHost: CompilerHost; - readonly moduleResolutionCache: ModuleResolutionCache | undefined; - readonly typeReferenceDirectiveResolutionCache: TypeReferenceDirectiveResolutionCache | undefined; - - // Mutable state - buildOrder: AnyBuildOrder | undefined; - readFileWithCache: (f: string) => string | undefined; - projectCompilerOptions: CompilerOptions; - cache: SolutionBuilderStateCache | undefined; - allProjectBuildPending: boolean; - needsSummary: boolean; - watchAllProjectsPending: boolean; - currentInvalidatedProject: InvalidatedProject | undefined; - - // Watch state - readonly watch: boolean; - readonly allWatchedWildcardDirectories: ESMap>; - readonly allWatchedInputFiles: ESMap>; - readonly allWatchedConfigFiles: ESMap; - readonly allWatchedExtendedConfigFiles: ESMap>; - readonly allWatchedPackageJsonFiles: ESMap>; - readonly lastCachedPackageJsonLookups: ESMap; - - timerToBuildInvalidatedProject: any; - reportFileChangeDetected: boolean; - writeLog: (s: string) => void; - } - - function createSolutionBuilderState(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions: WatchOptions | undefined): 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; - const typeReferenceDirectiveResolutionCache = !compilerHost.resolveTypeReferenceDirectives ? createTypeReferenceDirectiveResolutionCache(currentDirectory, getCanonicalFileName, /*options*/ undefined, moduleResolutionCache?.getPackageJsonInfoCache()) : 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.checkEachDefined(moduleNames), containingFile, redirectedReference, loader); - } - if (!compilerHost.resolveTypeReferenceDirectives) { - const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(moduleName, containingFile, state.projectCompilerOptions, compilerHost, redirectedReference, state.typeReferenceDirectiveResolutionCache).resolvedTypeReferenceDirective!; - compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile, redirectedReference) => - loadWithLocalCache(Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, loader); - } - - const { watchFile, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); - - const state: SolutionBuilderState = { - host, - hostWithWatch, - currentDirectory, - getCanonicalFileName, - parseConfigFileHost: parseConfigHostFromCompilerHostLike(host), - write: maybeBind(host, host.trace), - - // State of solution - options, - baseCompilerOptions, - rootNames, - baseWatchOptions, - - resolvedConfigFilePaths: new Map(), - configFileCache: new Map(), - projectStatus: new Map(), - buildInfoChecked: new Map(), - extendedConfigCache: new Map(), - - builderPrograms: new Map(), - diagnostics: new Map(), - projectPendingBuild: new Map(), - projectErrorsReported: new Map(), - - compilerHost, - moduleResolutionCache, - typeReferenceDirectiveResolutionCache, - - // Mutable state - buildOrder: undefined, - readFileWithCache: f => host.readFile(f), - projectCompilerOptions: baseCompilerOptions, - cache: undefined, - allProjectBuildPending: true, - needsSummary: true, - watchAllProjectsPending: watch, - currentInvalidatedProject: undefined, - - // Watch state - watch, - allWatchedWildcardDirectories: new Map(), - allWatchedInputFiles: new Map(), - allWatchedConfigFiles: new Map(), - allWatchedExtendedConfigFiles: new Map(), - allWatchedPackageJsonFiles: new Map(), - lastCachedPackageJsonLookups: new Map(), - - timerToBuildInvalidatedProject: undefined, - reportFileChangeDetected: false, - watchFile, - 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 getCachedParsedConfigFile(state: SolutionBuilderState, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { - const value = state.configFileCache.get(configFilePath); - return value && isParsedCommandLine(value) ? value : undefined; - } - - 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, baseWatchOptions, extendedConfigCache, host } = state; - let parsed: ParsedCommandLine | undefined; - if (host.getParsedCommandLine) { - parsed = host.getParsedCommandLine(configFileName); - if (!parsed) diagnostic = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - } - else { - parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; - parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache, baseWatchOptions); - 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[]): AnyBuildOrder { - const temporaryMarks = new Map(); - const permanentMarks = new Map(); - const circularityReportStack: string[] = []; - let buildOrder: ResolvedConfigFileName[] | undefined; - let circularDiagnostics: Diagnostic[] | undefined; - for (const root of roots) { - visit(root); - } - - return circularDiagnostics ? - { buildOrder: buildOrder || emptyArray, circularDiagnostics } : - 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) { - (circularDiagnostics || (circularDiagnostics = [])).push( - createCompilerDiagnostic( - 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 || createStateBuildOrder(state); - } - - function createStateBuildOrder(state: SolutionBuilderState) { - const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f))); - - // Clear all to ResolvedConfigFilePaths cache to start fresh - state.resolvedConfigFilePaths.clear(); - - // TODO(rbuckton): Should be a `Set`, but that requires changing the code below that uses `mutateMapSkippingNewValues` - const currentProjects = new Map( - getBuildOrderFromAnyBuildOrder(buildOrder).map( - resolved => [toResolvedConfigFilePath(state, resolved), true as true]) - ); - - const noopOnDelete = { onDeleteValue: noop }; - // Config file cache - mutateMapSkippingNewValues(state.configFileCache, currentProjects, noopOnDelete); - mutateMapSkippingNewValues(state.projectStatus, currentProjects, noopOnDelete); - mutateMapSkippingNewValues(state.buildInfoChecked, currentProjects, noopOnDelete); - mutateMapSkippingNewValues(state.builderPrograms, currentProjects, noopOnDelete); - mutateMapSkippingNewValues(state.diagnostics, currentProjects, noopOnDelete); - mutateMapSkippingNewValues(state.projectPendingBuild, currentProjects, noopOnDelete); - mutateMapSkippingNewValues(state.projectErrorsReported, currentProjects, noopOnDelete); - - // Remove watches for the program no longer in the solution - if (state.watch) { - mutateMapSkippingNewValues( - state.allWatchedConfigFiles, - currentProjects, - { onDeleteValue: closeFileWatcher } - ); - - state.allWatchedExtendedConfigFiles.forEach(watcher => { - watcher.projects.forEach(project => { - if (!currentProjects.has(project)) { - watcher.projects.delete(project); - } - }); - watcher.close(); - }); - - mutateMapSkippingNewValues( - state.allWatchedWildcardDirectories, - currentProjects, - { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcherOf) } - ); - - mutateMapSkippingNewValues( - state.allWatchedInputFiles, - currentProjects, - { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) } - ); - - mutateMapSkippingNewValues( - state.allWatchedPackageJsonFiles, - currentProjects, - { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) } - ); - } - return state.buildOrder = buildOrder; - } - - function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined): AnyBuildOrder | undefined { - const resolvedProject = project && resolveProjectName(state, project); - const buildOrderFromState = getBuildOrder(state); - if (isCircularBuildOrder(buildOrderFromState)) return buildOrderFromState; - if (resolvedProject) { - const projectPath = toResolvedConfigFilePath(state, resolvedProject); - const projectIndex = findIndex( - buildOrderFromState, - configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath - ); - if (projectIndex === -1) return undefined; - } - const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) as BuildOrder : buildOrderFromState; - Debug.assert(!isCircularBuildOrder(buildOrder)); - Debug.assert(!onlyReferences || resolvedProject !== undefined); - Debug.assert(!onlyReferences || buildOrder[buildOrder.length - 1] === resolvedProject); - return onlyReferences ? buildOrder.slice(0, buildOrder.length - 1) : buildOrder; - } - - function enableCache(state: SolutionBuilderState) { - 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, typeReferenceDirectiveResolutionCache } = 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(); - moduleResolutionCache?.clear(); - typeReferenceDirectiveResolutionCache?.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 = getBuildOrderFromAnyBuildOrder(getBuildOrder(state)); - buildOrder.forEach(configFileName => - state.projectPendingBuild.set( - toResolvedConfigFilePath(state, configFileName), - ConfigFileProgramReloadLevel.None - ) - ); - - if (cancellationToken) { - cancellationToken.throwIfCancellationRequested(); - } - } - - export enum InvalidatedProjectKind { - Build, - UpdateBundle, - UpdateOutputFileStamps - } - - export interface InvalidatedProjectBase { - readonly kind: InvalidatedProjectKind; - readonly project: ResolvedConfigFileName; - /*@internal*/ readonly projectPath: ResolvedConfigFilePath; - /*@internal*/ readonly buildOrder: readonly ResolvedConfigFileName[]; - /** - * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly - */ - done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus; - getCompilerOptions(): CompilerOptions; - getCurrentDirectory(): string; - } - - export interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { - readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; - updateOutputFileStatmps(): void; - } - - export interface BuildInvalidedProject extends InvalidatedProjectBase { - readonly kind: InvalidatedProjectKind.Build; - /* - * Emitting with this builder program without the api provided for this project - * can result in build system going into invalid state as files written reflect the state of the project - */ - getBuilderProgram(): T | undefined; - getProgram(): Program | undefined; - getSourceFile(fileName: string): SourceFile | undefined; - getSourceFiles(): readonly SourceFile[]; - getOptionsDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; - getGlobalDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; - getConfigFileParsingDiagnostics(): readonly Diagnostic[]; - getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; - getAllDependencies(sourceFile: SourceFile): readonly string[]; - getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; - getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult; - /* - * Calling emit directly with targetSourceFile and emitOnlyDtsFiles set to true is not advised since - * emit in build system is responsible in updating status of the project - * If called with targetSourceFile and emitOnlyDtsFiles set to true, the emit just passes to underlying builder and - * wont reflect the status of file as being emitted in the builder - * (if that emit of that source file is required it would be emitted again when making sure invalidated project is completed) - * This emit is not considered actual emit (and hence uptodate status is not reflected if - */ - emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined; - // TODO(shkamat):: investigate later if we can emit even when there are declaration diagnostics - // emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; - } - - export interface UpdateBundleProject extends InvalidatedProjectBase { - readonly kind: InvalidatedProjectKind.UpdateBundle; - emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; - } - - export type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; - - function doneInvalidatedProject( - state: SolutionBuilderState, - projectPath: ResolvedConfigFilePath - ) { - state.projectPendingBuild.delete(projectPath); - state.currentInvalidatedProject = undefined; - return state.diagnostics.has(projectPath) ? - ExitStatus.DiagnosticsPresent_OutputsSkipped : - ExitStatus.Success; - } - - function createUpdateOutputFileStampsProject( - state: SolutionBuilderState, - project: ResolvedConfigFileName, - projectPath: ResolvedConfigFilePath, - config: ParsedCommandLine, - buildOrder: readonly ResolvedConfigFileName[] - ): UpdateOutputFileStampsProject { - let updateOutputFileStampsPending = true; - return { - kind: InvalidatedProjectKind.UpdateOutputFileStamps, - project, - projectPath, - buildOrder, - getCompilerOptions: () => config.options, - getCurrentDirectory: () => state.currentDirectory, - updateOutputFileStatmps: () => { - updateOutputTimestamps(state, config, projectPath); - updateOutputFileStampsPending = false; - }, - done: () => { - if (updateOutputFileStampsPending) { - updateOutputTimestamps(state, config, projectPath); - } - return doneInvalidatedProject(state, projectPath); - } - }; - } - - enum BuildStep { - CreateProgram, - SyntaxDiagnostics, - SemanticDiagnostics, - Emit, - EmitBundle, - EmitBuildInfo, - BuildInvalidatedProjectOfBundle, - QueueReferencingProjects, - Done - } - - function createBuildOrUpdateInvalidedProject( - kind: InvalidatedProjectKind.Build | InvalidatedProjectKind.UpdateBundle, - state: SolutionBuilderState, - project: ResolvedConfigFileName, - projectPath: ResolvedConfigFilePath, - projectIndex: number, - config: ParsedCommandLine, - buildOrder: readonly ResolvedConfigFileName[], - ): BuildInvalidedProject | UpdateBundleProject { - let step = kind === InvalidatedProjectKind.Build ? BuildStep.CreateProgram : BuildStep.EmitBundle; - let program: T | undefined; - let buildResult: BuildResultFlags | undefined; - let invalidatedProjectOfBundle: BuildInvalidedProject | undefined; - - return kind === InvalidatedProjectKind.Build ? - { - kind, - project, - projectPath, - buildOrder, - getCompilerOptions: () => config.options, - getCurrentDirectory: () => state.currentDirectory, - getBuilderProgram: () => withProgramOrUndefined(identity), - getProgram: () => - withProgramOrUndefined( - program => program.getProgramOrUndefined() - ), - getSourceFile: fileName => - withProgramOrUndefined( - program => program.getSourceFile(fileName) - ), - getSourceFiles: () => - withProgramOrEmptyArray( - program => program.getSourceFiles() - ), - getOptionsDiagnostics: cancellationToken => - withProgramOrEmptyArray( - program => program.getOptionsDiagnostics(cancellationToken) - ), - getGlobalDiagnostics: cancellationToken => - withProgramOrEmptyArray( - program => program.getGlobalDiagnostics(cancellationToken) - ), - getConfigFileParsingDiagnostics: () => - withProgramOrEmptyArray( - program => program.getConfigFileParsingDiagnostics() - ), - getSyntacticDiagnostics: (sourceFile, cancellationToken) => - withProgramOrEmptyArray( - program => program.getSyntacticDiagnostics(sourceFile, cancellationToken) - ), - getAllDependencies: sourceFile => - withProgramOrEmptyArray( - program => program.getAllDependencies(sourceFile) - ), - getSemanticDiagnostics: (sourceFile, cancellationToken) => - withProgramOrEmptyArray( - program => program.getSemanticDiagnostics(sourceFile, cancellationToken) - ), - getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) => - withProgramOrUndefined( - program => - ((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) && - (program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile) - ), - emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => { - if (targetSourceFile || emitOnlyDtsFiles) { - return withProgramOrUndefined( - program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers || state.host.getCustomTransformers?.(project)) - ); - } - executeSteps(BuildStep.SemanticDiagnostics, cancellationToken); - if (step === BuildStep.EmitBuildInfo) { - return emitBuildInfo(writeFile, cancellationToken); - } - if (step !== BuildStep.Emit) return undefined; - return emit(writeFile, cancellationToken, customTransformers); - }, - done - } : - { - kind, - project, - projectPath, - buildOrder, - getCompilerOptions: () => config.options, - getCurrentDirectory: () => state.currentDirectory, - emit: (writeFile: WriteFileCallback | undefined, customTransformers: CustomTransformers | undefined) => { - if (step !== BuildStep.EmitBundle) return invalidatedProjectOfBundle; - return emitBundle(writeFile, customTransformers); - }, - done, - }; - - function done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { - executeSteps(BuildStep.Done, cancellationToken, writeFile, customTransformers); - return doneInvalidatedProject(state, projectPath); - } - - function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { - executeSteps(BuildStep.CreateProgram); - return program && action(program); - } - - function withProgramOrEmptyArray(action: (program: T) => readonly U[]): readonly U[] { - return withProgramOrUndefined(action) || emptyArray; - } - - function createProgram() { - Debug.assert(program === undefined); - - if (state.options.dry) { - reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, project); - buildResult = BuildResultFlags.Success; - step = BuildStep.QueueReferencingProjects; - return; - } - - if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, project); - - if (config.fileNames.length === 0) { - reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); - // Nothing to build - must be a solution file, basically - buildResult = BuildResultFlags.None; - step = BuildStep.QueueReferencingProjects; - return; - } - - const { host, compilerHost } = state; - state.projectCompilerOptions = config.options; - // Update module resolution cache if needed - state.moduleResolutionCache?.update(config.options); - state.typeReferenceDirectiveResolutionCache?.update(config.options); - - // Create program - program = host.createProgram( - config.fileNames, - config.options, - compilerHost, - getOldProgram(state, projectPath, config), - getConfigFileParsingDiagnostics(config), - config.projectReferences - ); - if (state.watch) { - state.lastCachedPackageJsonLookups.set(projectPath, state.moduleResolutionCache && map( - state.moduleResolutionCache.getPackageJsonInfoCache().entries(), - ([path, data]) => ([state.host.realpath && data ? toPath(state, state.host.realpath(path)) : path, data] as const) - )); - - state.builderPrograms.set(projectPath, program); - } - step++; - } - - function handleDiagnostics(diagnostics: readonly Diagnostic[], errorFlags: BuildResultFlags, errorType: string) { - if (diagnostics.length) { - ({ buildResult, step } = buildErrors( - state, - projectPath, - program, - config, - diagnostics, - errorFlags, - errorType - )); - } - else { - step++; - } - } - - function getSyntaxDiagnostics(cancellationToken?: CancellationToken) { - Debug.assertIsDefined(program); - handleDiagnostics( - [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken) - ], - BuildResultFlags.SyntaxErrors, - "Syntactic" - ); - } - - function getSemanticDiagnostics(cancellationToken?: CancellationToken) { - handleDiagnostics( - Debug.checkDefined(program).getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken), - BuildResultFlags.TypeErrors, - "Semantic" - ); - } - - function emit(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitResult { - Debug.assertIsDefined(program); - Debug.assert(step === BuildStep.Emit); - // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly - program.backupState(); - let declDiagnostics: Diagnostic[] | undefined; - const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); - const outputFiles: OutputFile[] = []; - const { emitResult } = emitFilesAndReportErrors( - program, - reportDeclarationDiagnostics, - /*write*/ undefined, - /*reportSummary*/ undefined, - (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), - cancellationToken, - /*emitOnlyDts*/ false, - customTransformers || state.host.getCustomTransformers?.(project) - ); - // Don't emit .d.ts if there are decl file errors - if (declDiagnostics) { - program.restoreState(); - ({ buildResult, step } = buildErrors( - state, - projectPath, - program, - config, - declDiagnostics, - BuildResultFlags.DeclarationEmitErrors, - "Declaration file" - )); - return { - emitSkipped: true, - diagnostics: emitResult.diagnostics - }; - } - - // Actual Emit - const { host, compilerHost } = state; - let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; - let newestDeclarationFileContentChangedTime = minimumDate; - let anyDtsChanged = false; - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = new Map(); - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - let priorChangeTime: Date | undefined; - if (!anyDtsChanged && isDeclarationFile(name)) { - // Check for unchanged .d.ts files - if (host.fileExists(name) && state.readFileWithCache(name) === text) { - priorChangeTime = host.getModifiedTime(name); - } - else { - resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - anyDtsChanged = true; - } - } - - emittedOutputs.set(toPath(state, name), name); - writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - if (priorChangeTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); - } - }); - - finishEmit( - emitterDiagnostics, - emittedOutputs, - newestDeclarationFileContentChangedTime, - /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ anyDtsChanged, - outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()), - resultFlags - ); - return emitResult; - } - - function emitBuildInfo(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult { - Debug.assertIsDefined(program); - Debug.assert(step === BuildStep.EmitBuildInfo); - const emitResult = program.emitBuildInfo(writeFileCallback, cancellationToken); - if (emitResult.diagnostics.length) { - reportErrors(state, emitResult.diagnostics); - state.diagnostics.set(projectPath, [...state.diagnostics.get(projectPath)!, ...emitResult.diagnostics]); - buildResult = BuildResultFlags.EmitErrors & buildResult!; - } - - if (emitResult.emittedFiles && state.write) { - emitResult.emittedFiles.forEach(name => listEmittedFile(state, config, name)); - } - afterProgramDone(state, program, config); - step = BuildStep.QueueReferencingProjects; - return emitResult; - } - - function finishEmit( - emitterDiagnostics: DiagnosticCollection, - emittedOutputs: ESMap, - priorNewestUpdateTime: Date, - newestDeclarationFileContentChangedTimeIsMaximumDate: boolean, - oldestOutputFileName: string, - resultFlags: BuildResultFlags - ) { - const emitDiagnostics = emitterDiagnostics.getDiagnostics(); - if (emitDiagnostics.length) { - ({ buildResult, step } = buildErrors( - state, - projectPath, - program, - config, - emitDiagnostics, - BuildResultFlags.EmitErrors, - "Emit" - )); - return emitDiagnostics; - } - - if (state.write) { - emittedOutputs.forEach(name => listEmittedFile(state, config, name)); - } - - // Update time stamps for rest of the outputs - const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, priorNewestUpdateTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); - state.diagnostics.delete(projectPath); - state.projectStatus.set(projectPath, { - type: UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime: newestDeclarationFileContentChangedTimeIsMaximumDate ? - maximumDate : - newestDeclarationFileContentChangedTime, - oldestOutputFileName - }); - afterProgramDone(state, program, config); - step = BuildStep.QueueReferencingProjects; - buildResult = resultFlags; - return emitDiagnostics; - } - - function emitBundle(writeFileCallback?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined { - Debug.assert(kind === InvalidatedProjectKind.UpdateBundle); - if (state.options.dry) { - reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, project); - buildResult = BuildResultFlags.Success; - step = BuildStep.QueueReferencingProjects; - return undefined; - } - - if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, project); - - // Update js, and source map - const { compilerHost } = state; - state.projectCompilerOptions = config.options; - const outputFiles = emitUsingBuildInfo( - config, - compilerHost, - ref => { - const refName = resolveProjectName(state, ref.path); - return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName)); - }, - customTransformers || state.host.getCustomTransformers?.(project) - ); - - if (isString(outputFiles)) { - reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, project, relName(state, outputFiles)); - step = BuildStep.BuildInvalidatedProjectOfBundle; - return invalidatedProjectOfBundle = createBuildOrUpdateInvalidedProject( - InvalidatedProjectKind.Build, - state, - project, - projectPath, - projectIndex, - config, - buildOrder - ) as BuildInvalidedProject; - } - - // Actual Emit - Debug.assert(!!outputFiles.length); - const emitterDiagnostics = createDiagnosticCollection(); - const emittedOutputs = new Map(); - outputFiles.forEach(({ name, text, writeByteOrderMark }) => { - emittedOutputs.set(toPath(state, name), name); - writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); - }); - - const emitDiagnostics = finishEmit( - emitterDiagnostics, - emittedOutputs, - minimumDate, - /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ false, - outputFiles[0].name, - BuildResultFlags.DeclarationOutputUnchanged - ); - return { emitSkipped: false, diagnostics: emitDiagnostics }; - } - - function executeSteps(till: BuildStep, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { - while (step <= till && step < BuildStep.Done) { - const currentStep = step; - switch (step) { - case BuildStep.CreateProgram: - createProgram(); - break; - - case BuildStep.SyntaxDiagnostics: - getSyntaxDiagnostics(cancellationToken); - break; - - case BuildStep.SemanticDiagnostics: - getSemanticDiagnostics(cancellationToken); - break; - - case BuildStep.Emit: - emit(writeFile, cancellationToken, customTransformers); - break; - - case BuildStep.EmitBuildInfo: - emitBuildInfo(writeFile, cancellationToken); - break; - - case BuildStep.EmitBundle: - emitBundle(writeFile, customTransformers); - break; - - case BuildStep.BuildInvalidatedProjectOfBundle: - Debug.checkDefined(invalidatedProjectOfBundle).done(cancellationToken, writeFile, customTransformers); - step = BuildStep.Done; - break; - - case BuildStep.QueueReferencingProjects: - queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, Debug.checkDefined(buildResult)); - step++; - break; - - // Should never be done - case BuildStep.Done: - default: - assertType(step); - - } - Debug.assert(step > currentStep); - } - } - } - - function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { - if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; - return config.fileNames.length === 0 || - !!getConfigFileParsingDiagnostics(config).length || - !isIncrementalCompilation(config.options); - } - - function getNextInvalidatedProject( - state: SolutionBuilderState, - buildOrder: AnyBuildOrder, - reportQueue: boolean - ): InvalidatedProject | undefined { - if (!state.projectPendingBuild.size) return undefined; - if (isCircularBuildOrder(buildOrder)) return undefined; - if (state.currentInvalidatedProject) { - // Only if same buildOrder the currentInvalidated project can be sent again - return arrayIsEqualTo(state.currentInvalidatedProject.buildOrder, buildOrder) ? - state.currentInvalidatedProject : - undefined; - } - - const { options, projectPendingBuild } = state; - for (let projectIndex = 0; projectIndex < buildOrder.length; projectIndex++) { - const project = buildOrder[projectIndex]; - const projectPath = toResolvedConfigFilePath(state, project); - const reloadLevel = state.projectPendingBuild.get(projectPath); - if (reloadLevel === undefined) continue; - - if (reportQueue) { - reportQueue = false; - reportBuildQueue(state, buildOrder); - } - - const config = parseConfigFile(state, project, projectPath); - if (!config) { - reportParseConfigFileDiagnostic(state, projectPath); - projectPendingBuild.delete(projectPath); - continue; - } - - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - watchConfigFile(state, project, projectPath, config); - watchExtendedConfigFiles(state, projectPath, config); - watchWildCardDirectories(state, project, projectPath, config); - watchInputFiles(state, project, projectPath, config); - watchPackageJsonFiles(state, project, projectPath, config); - } - else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { - // Update file names - config.fileNames = getFileNamesFromConfigSpecs(config.options.configFile!.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost); - updateErrorForNoInputFiles(config.fileNames, project, config.options.configFile!.configFileSpecs!, config.errors, canJsonReportNoInputFiles(config.raw)); - watchInputFiles(state, project, projectPath, config); - watchPackageJsonFiles(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, getConfigFileParsingDiagnostics(config)); - projectPendingBuild.delete(projectPath); - // Up to date, skip - if (options.dry) { - // In a dry build, inform the user of this fact - reportStatus(state, Diagnostics.Project_0_is_up_to_date, project); - } - continue; - } - - if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { - reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); - return createUpdateOutputFileStampsProject( - state, - project, - projectPath, - config, - buildOrder - ); - } - } - - if (status.type === UpToDateStatusType.UpstreamBlocked) { - reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); - projectPendingBuild.delete(projectPath); - if (options.verbose) { - reportStatus( - state, - status.upstreamProjectBlocked ? - Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_was_not_built : - Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, - project, - status.upstreamProjectName - ); - } - continue; - } - - if (status.type === UpToDateStatusType.ContainerOnly) { - reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); - projectPendingBuild.delete(projectPath); - // Do nothing - continue; - } - - return createBuildOrUpdateInvalidedProject( - needsBuild(state, status, config) ? - InvalidatedProjectKind.Build : - InvalidatedProjectKind.UpdateBundle, - state, - project, - projectPath, - projectIndex, - config, - buildOrder, - ); - } - - return undefined; - } - - function listEmittedFile({ write }: SolutionBuilderState, proj: ParsedCommandLine, file: string) { - if (write && proj.options.listEmittedFiles) { - write(`TSFILE: ${file}`); - } - } - - function getOldProgram({ options, builderPrograms, compilerHost }: SolutionBuilderState, proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (options.force) return undefined; - const value = builderPrograms.get(proj); - if (value) return value; - return readBuilderProgram(parsed.options, compilerHost) as any as T; - } - - function afterProgramDone( - state: SolutionBuilderState, - program: T | undefined, - config: ParsedCommandLine - ) { - if (program) { - if (program && state.write) listFiles(program, state.write); - if (state.host.afterProgramEmitAndDiagnostics) { - state.host.afterProgramEmitAndDiagnostics(program); - } - program.releaseProgram(); - } - else if (state.host.afterEmitBundle) { - state.host.afterEmitBundle(config); - } - state.projectCompilerOptions = state.baseCompilerOptions; - } - - function buildErrors( - state: SolutionBuilderState, - resolvedPath: ResolvedConfigFilePath, - program: T | undefined, - config: ParsedCommandLine, - diagnostics: readonly Diagnostic[], - buildResult: BuildResultFlags, - errorType: string, - ) { - const canEmitBuildInfo = !(buildResult & BuildResultFlags.SyntaxErrors) && program && !outFile(program.getCompilerOptions()); - - reportAndStoreErrors(state, resolvedPath, diagnostics); - state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); - if (canEmitBuildInfo) return { buildResult, step: BuildStep.EmitBuildInfo }; - afterProgramDone(state, program, config); - return { buildResult, step: BuildStep.QueueReferencingProjects }; - } - - function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { - // Check tsconfig time - const tsconfigTime = getModifiedTime(state.host, configFile); - if (oldestOutputFileTime < tsconfigTime) { - return { - type: UpToDateStatusType.OutOfDateWithSelf, - outOfDateOutputFileName: oldestOutputFileName, - newerInputFileName: configFile - }; - } - } - - function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { - const force = !!state.options.force; - let newestInputFileName: string = undefined!; - let newestInputFileTime = minimumDate; - const { host } = state; - // Get timestamps of input files - for (const inputFile of project.fileNames) { - if (!host.fileExists(inputFile)) { - return { - type: UpToDateStatusType.Unbuildable, - reason: `${inputFile} does not exist` - }; - } - - if (!force) { - const inputTime = getModifiedTime(host, inputFile); host.getModifiedTime(inputFile); - if (inputTime > newestInputFileTime) { - newestInputFileName = inputFile; - newestInputFileTime = inputTime; - } - } - } - - // Container if no files are specified in the project - if (!project.fileNames.length && !canJsonReportNoInputFiles(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; - if (!force) { - 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 = getModifiedTime(host, output); - 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 = getModifiedTime(host, output); - 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 || - refStatus.type === UpToDateStatusType.ContainerOnly) { // Container only ignore this project - continue; - } - - // An upstream project is blocked - if (refStatus.type === UpToDateStatusType.Unbuildable || - refStatus.type === UpToDateStatusType.UpstreamBlocked) { - return { - type: UpToDateStatusType.UpstreamBlocked, - upstreamProjectName: ref.path, - upstreamProjectBlocked: refStatus.type === UpToDateStatusType.UpstreamBlocked - }; - } - - // 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 - // (a check we will have skipped if this is a forced build) - if (!force && !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; - - // Check package file time - const dependentPackageFileStatus = forEach( - state.lastCachedPackageJsonLookups.get(resolvedPath) || emptyArray, - ([path]) => checkConfigFileUpToDateStatus(state, path, oldestOutputFileTime, oldestOutputFileName) - ); - if (dependentPackageFileStatus) return dependentPackageFileStatus; - } - - if (!force && !state.buildInfoChecked.has(resolvedPath)) { - state.buildInfoChecked.set(resolvedPath, true); - const buildInfoPath = getTsBuildInfoEmitOutputFilePath(project.options); - if (buildInfoPath) { - const value = state.readFileWithCache(buildInfoPath); - const buildInfo = value && getBuildInfo(value); - if (buildInfo && (buildInfo.bundle || buildInfo.program) && buildInfo.version !== version) { - return { - type: UpToDateStatusType.TsVersionOutputOfDate, - version: buildInfo.version - }; - } - } - } - - 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?: ESMap) { - if (proj.options.noEmit) return priorNewestUpdateTime; - 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, getModifiedTime(host, file)); - } - - host.setModifiedTime(file, now); - } - } - - 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 queueReferencingProjects( - state: SolutionBuilderState, - project: ResolvedConfigFileName, - projectPath: ResolvedConfigFilePath, - projectIndex: number, - config: ParsedCommandLine, - buildOrder: readonly ResolvedConfigFileName[], - buildResult: BuildResultFlags - ) { - // Queue only if there are no errors - if (buildResult & BuildResultFlags.AnyErrors) return; - // Only composite projects can be referenced by other projects - if (!config.options.composite) return; - // Always use build order to queue projects - for (let index = projectIndex + 1; index < buildOrder.length; index++) { - const nextProject = buildOrder[index]; - const nextProjectPath = toResolvedConfigFilePath(state, nextProject); - if (state.projectPendingBuild.has(nextProjectPath)) continue; - - const nextProjectConfig = parseConfigFile(state, nextProject, nextProjectPath); - if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; - for (const ref of nextProjectConfig.projectReferences) { - const resolvedRefPath = resolveProjectName(state, ref.path); - if (toResolvedConfigFilePath(state, resolvedRefPath) !== projectPath) continue; - // If the project is referenced with prepend, always build downstream projects, - // If declaration output is changed, build the project - // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps - const status = state.projectStatus.get(nextProjectPath); - if (status) { - switch (status.type) { - case UpToDateStatusType.UpToDate: - if (buildResult & BuildResultFlags.DeclarationOutputUnchanged) { - if (ref.prepend) { - state.projectStatus.set(nextProjectPath, { - type: UpToDateStatusType.OutOfDateWithPrepend, - outOfDateOutputFileName: status.oldestOutputFileName, - newerProjectName: project - }); - } - else { - status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; - } - break; - } - // falls through - - case UpToDateStatusType.UpToDateWithUpstreamTypes: - case UpToDateStatusType.OutOfDateWithPrepend: - if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { - state.projectStatus.set(nextProjectPath, { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, - newerProjectName: project - }); - } - break; - - case UpToDateStatusType.UpstreamBlocked: - if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) { - clearProjectStatus(state, nextProjectPath); - } - break; - } - } - addProjToQueue(state, nextProjectPath, ConfigFileProgramReloadLevel.None); - break; - } - } - } - - function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers, onlyReferences?: boolean): ExitStatus { - const buildOrder = getBuildOrderFor(state, project, onlyReferences); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - - setupInitialBuild(state, cancellationToken); - - let reportQueue = true; - let successfulProjects = 0; - while (true) { - const invalidatedProject = getNextInvalidatedProject(state, buildOrder, reportQueue); - if (!invalidatedProject) break; - reportQueue = false; - invalidatedProject.done(cancellationToken, writeFile, getCustomTransformers?.(invalidatedProject.project)); - if (!state.diagnostics.has(invalidatedProject.projectPath)) successfulProjects++; - } - - disableCache(state); - reportErrorSummary(state, buildOrder); - startWatching(state, buildOrder); - - return isCircularBuildOrder(buildOrder) - ? ExitStatus.ProjectReferenceCycle_OutputsSkipped - : !buildOrder.some(p => state.diagnostics.has(toResolvedConfigFilePath(state, p))) - ? ExitStatus.Success - : successfulProjects - ? ExitStatus.DiagnosticsPresent_OutputsGenerated - : ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - - function clean(state: SolutionBuilderState, project?: string, onlyReferences?: boolean) { - const buildOrder = getBuildOrderFor(state, project, onlyReferences); - if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; - - if (isCircularBuildOrder(buildOrder)) { - reportErrors(state, buildOrder.circularDiagnostics); - return ExitStatus.ProjectReferenceCycle_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()); - if (!outputs.length) continue; - const inputFileNames = new Set(parsed.fileNames.map(f => toPath(state, f))); - for (const output of outputs) { - // If output name is same as input file name, do not delete and ignore the error - if (inputFileNames.has(toPath(state, output))) continue; - 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 host implements getParsedCommandLine, we cant get list of files from parseConfigFileHost - if (state.host.getParsedCommandLine && reloadLevel === ConfigFileProgramReloadLevel.Partial) { - reloadLevel = ConfigFileProgramReloadLevel.Full; - } - if (reloadLevel === ConfigFileProgramReloadLevel.Full) { - state.configFileCache.delete(resolved); - state.buildOrder = undefined; - } - 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 buildOrder = getBuildOrder(state); - const invalidatedProject = getNextInvalidatedProject(state, buildOrder, /*reportQueue*/ false); - if (invalidatedProject) { - invalidatedProject.done(); - if (state.projectPendingBuild.size) { - // Schedule next project for build - if (state.watch && !state.timerToBuildInvalidatedProject) { - scheduleBuildInvalidatedProject(state); - } - return; - } - } - disableCache(state); - reportErrorSummary(state, buildOrder); - } - - function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) { - if (!state.watch || state.allWatchedConfigFiles.has(resolvedPath)) return; - state.allWatchedConfigFiles.set(resolvedPath, state.watchFile( - resolved, - () => { - invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full); - }, - PollingInterval.High, - parsed?.watchOptions, - WatchType.ConfigFile, - resolved - )); - } - - function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) { - updateSharedExtendedConfigFileWatcher( - resolvedPath, - parsed?.options, - state.allWatchedExtendedConfigFiles, - (extendedConfigFileName, extendedConfigFilePath) => state.watchFile( - extendedConfigFileName, - () => state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath)?.projects.forEach(projectConfigFilePath => - invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full) - ), - PollingInterval.High, - parsed?.watchOptions, - WatchType.ExtendedConfigFile, - ), - fileName => toPath(state, fileName), - ); - } - - function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!state.watch) return; - updateWatchingWildcardDirectories( - getOrCreateValueMapFromConfigFileMap(state.allWatchedWildcardDirectories, resolvedPath), - new Map(getEntries(parsed.wildcardDirectories!)), - (dir, flags) => state.watchDirectory( - dir, - fileOrDirectory => { - if (isIgnoredFileFromWildCardWatching({ - watchedDirPath: toPath(state, dir), - fileOrDirectory, - fileOrDirectoryPath: toPath(state, fileOrDirectory), - configFileName: resolved, - currentDirectory: state.currentDirectory, - options: parsed.options, - program: state.builderPrograms.get(resolvedPath) || getCachedParsedConfigFile(state, resolvedPath)?.fileNames, - useCaseSensitiveFileNames: state.parseConfigFileHost.useCaseSensitiveFileNames, - writeLog: s => state.writeLog(s), - toPath: fileName => toPath(state, fileName) - })) return; - - invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial); - }, - flags, - parsed?.watchOptions, - 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.watchFile( - input, - () => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None), - PollingInterval.Low, - parsed?.watchOptions, - WatchType.SourceFile, - resolved - ), - onDeleteValue: closeFileWatcher, - } - ); - } - - function watchPackageJsonFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { - if (!state.watch || !state.lastCachedPackageJsonLookups) return; - mutateMap( - getOrCreateValueMapFromConfigFileMap(state.allWatchedPackageJsonFiles, resolvedPath), - new Map(state.lastCachedPackageJsonLookups.get(resolvedPath)), - { - createNewValue: (path, _input) => state.watchFile( - path, - () => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full), - PollingInterval.High, - parsed?.watchOptions, - WatchType.PackageJson, - resolved - ), - onDeleteValue: closeFileWatcher, - } - ); - } - - function startWatching(state: SolutionBuilderState, buildOrder: AnyBuildOrder) { - if (!state.watchAllProjectsPending) return; - state.watchAllProjectsPending = false; - for (const resolved of getBuildOrderFromAnyBuildOrder(buildOrder)) { - const resolvedPath = toResolvedConfigFilePath(state, resolved); - const cfg = parseConfigFile(state, resolved, resolvedPath); - // Watch this file - watchConfigFile(state, resolved, resolvedPath, cfg); - watchExtendedConfigFiles(state, resolvedPath, cfg); - if (cfg) { - // Update watchers for wildcard directories - watchWildCardDirectories(state, resolved, resolvedPath, cfg); - - // Watch input files - watchInputFiles(state, resolved, resolvedPath, cfg); - - // Watch package json files - watchPackageJsonFiles(state, resolved, resolvedPath, cfg); - } - } - } - - function stopWatching(state: SolutionBuilderState) { - clearMap(state.allWatchedConfigFiles, closeFileWatcher); - clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf); - clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf)); - clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher)); - clearMap(state.allWatchedPackageJsonFiles, watchedPacageJsonFiles => clearMap(watchedPacageJsonFiles, closeFileWatcher)); - } - - /** - * 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: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder; - function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { - const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions); - return { - build: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers), - clean: project => clean(state, project), - buildReferences: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers, /*onlyReferences*/ true), - cleanReferences: project => clean(state, project, /*onlyReferences*/ true), - getNextInvalidatedProject: cancellationToken => { - setupInitialBuild(state, cancellationToken); - return getNextInvalidatedProject(state, getBuildOrder(state), /*reportQueue*/ false); - }, - getBuildOrder: () => getBuildOrder(state), - getUpToDateStatusOfProject: project => { - const configFileName = resolveProjectName(state, project); - const configFilePath = toResolvedConfigFilePath(state, configFileName); - return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath); - }, - invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None), - buildNextInvalidatedProject: () => buildNextInvalidatedProject(state), - getAllParsedConfigs: () => arrayFrom(mapDefinedIterator( - state.configFileCache.values(), - config => isParsedCommandLine(config) ? config : undefined - )), - close: () => stopWatching(state), - }; - } - - function relName(state: SolutionBuilderState, path: string): string { - return convertToRelativePath(path, state.currentDirectory, f => state.getCanonicalFileName(f)); - } - - function reportStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: string[]) { - state.host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); - } - - function reportWatchStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { - state.hostWithWatch.onWatchStatusChange?.(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions); - } - - function reportErrors({ host }: SolutionBuilderState, errors: readonly Diagnostic[]) { - errors.forEach(err => host.reportDiagnostic(err)); - } - - function reportAndStoreErrors(state: SolutionBuilderState, proj: ResolvedConfigFilePath, errors: readonly Diagnostic[]) { - 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, buildOrder: AnyBuildOrder) { - if (!state.needsSummary) return; - state.needsSummary = false; - const canReportSummary = state.watch || !!state.host.reportErrorSummary; - const { diagnostics } = state; - let totalErrors = 0; - if (isCircularBuildOrder(buildOrder)) { - reportBuildQueue(state, buildOrder.buildOrder); - reportErrors(state, buildOrder.circularDiagnostics); - if (canReportSummary) totalErrors += getErrorCountForSummary(buildOrder.circularDiagnostics); - } - else { - // Report errors from the other projects - buildOrder.forEach(project => { - const projectPath = toResolvedConfigFilePath(state, project); - if (!state.projectErrorsReported.has(projectPath)) { - reportErrors(state, diagnostics.get(projectPath) || emptyArray); - } - }); - if (canReportSummary) diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); - } - - if (state.watch) { - reportWatchStatus(state, getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); - } - else if (state.host.reportErrorSummary) { - 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) { - if (state.options.force && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes)) { - return reportStatus( - state, - Diagnostics.Project_0_is_being_forcibly_rebuilt, - relName(state, configFileName) - ); - } - - switch (status.type) { - case UpToDateStatusType.OutOfDateWithSelf: - 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 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 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 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 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 reportStatus( - state, - Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, - relName(state, configFileName) - ); - case UpToDateStatusType.UpstreamOutOfDate: - 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 reportStatus( - state, - status.upstreamProjectBlocked ? - Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_was_not_built : - Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, - relName(state, configFileName), - relName(state, status.upstreamProjectName) - ); - case UpToDateStatusType.Unbuildable: - return reportStatus( - state, - Diagnostics.Failed_to_parse_file_0_Colon_1, - relName(state, configFileName), - status.reason - ); - case UpToDateStatusType.TsVersionOutputOfDate: - 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 - ); - case UpToDateStatusType.ContainerOnly: - // Don't report status on "solution" projects - // falls through - case UpToDateStatusType.ComputingUpstream: - // Should never leak from getUptoDateStatusWorker - break; - default: - 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); - } - } -} +namespace ts { + const minimumDate = new Date(-8640000000000000); + const maximumDate = new Date(8640000000000000); + + export interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + + /*@internal*/ clean?: boolean; + /*@internal*/ watch?: boolean; + /*@internal*/ help?: boolean; + + /*@internal*/ preserveWatchOutput?: boolean; + /*@internal*/ listEmittedFiles?: boolean; + /*@internal*/ listFiles?: boolean; + /*@internal*/ explainFiles?: boolean; + /*@internal*/ pretty?: boolean; + incremental?: boolean; + assumeChangesOnlyAffectDirectDependencies?: boolean; + + traceResolution?: boolean; + /* @internal */ diagnostics?: boolean; + /* @internal */ extendedDiagnostics?: boolean; + /* @internal */ locale?: string; + /* @internal */ generateCpuProfile?: string; + /* @internal */ generateTrace?: string; + + [option: string]: CompilerOptionsValue | undefined; + } + + enum BuildResultFlags { + None = 0, + + /** + * No errors of any kind occurred during build + */ + Success = 1 << 0, + /** + * None of the .d.ts files emitted by this build were + * different from the existing files on disk + */ + DeclarationOutputUnchanged = 1 << 1, + + ConfigFileErrors = 1 << 2, + SyntaxErrors = 1 << 3, + TypeErrors = 1 << 4, + DeclarationEmitErrors = 1 << 5, + EmitErrors = 1 << 6, + + AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors + } + + /*@internal*/ + export type ResolvedConfigFilePath = ResolvedConfigFileName & Path; + + function getOrCreateValueFromConfigFileMap(configFileMap: ESMap, resolved: ResolvedConfigFilePath, createT: () => T): T { + const existingValue = configFileMap.get(resolved); + let newValue: T | undefined; + if (!existingValue) { + newValue = createT(); + configFileMap.set(resolved, newValue); + } + return existingValue || newValue!; + } + + function getOrCreateValueMapFromConfigFileMap(configFileMap: ESMap>, resolved: ResolvedConfigFilePath): ESMap { + return getOrCreateValueFromConfigFileMap>(configFileMap, resolved, () => new Map()); + } + + function newer(date1: Date, date2: Date): Date { + return date2 > date1 ? date2 : date1; + } + + function isDeclarationFile(fileName: string) { + return fileExtensionIs(fileName, Extension.Dts); + } + + export type ReportEmitErrorSummary = (errorCount: number) => void; + + export interface SolutionBuilderHostBase extends ProgramHost { + createDirectory?(path: string): void; + /** + * Should provide create directory and writeFile if done of invalidatedProjects is not invoked with + * writeFileCallback + */ + writeFile?(path: string, data: string, writeByteOrderMark?: boolean): void; + getCustomTransformers?: (project: string) => CustomTransformers | undefined; + + getModifiedTime(fileName: string): Date | undefined; + setModifiedTime(fileName: string, date: Date): void; + deleteFile(fileName: string): void; + getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; + + reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here + reportSolutionBuilderStatus: DiagnosticReporter; + + // TODO: To do better with watch mode and normal build mode api that creates program and emits files + // This currently helps enable --diagnostics and --extendedDiagnostics + afterProgramEmitAndDiagnostics?(program: T): void; + /*@internal*/ afterEmitBundle?(config: ParsedCommandLine): void; + + // For testing + /*@internal*/ now?(): Date; + } + + export interface SolutionBuilderHost extends SolutionBuilderHostBase { + reportErrorSummary?: ReportEmitErrorSummary; + } + + export interface SolutionBuilderWithWatchHost extends SolutionBuilderHostBase, WatchHost { + } + + /*@internal*/ + export type BuildOrder = readonly ResolvedConfigFileName[]; + /*@internal*/ + export interface CircularBuildOrder { + buildOrder: BuildOrder; + circularDiagnostics: readonly Diagnostic[]; + } + /*@internal*/ + export type AnyBuildOrder = BuildOrder | CircularBuildOrder; + + /*@internal*/ + export function isCircularBuildOrder(buildOrder: AnyBuildOrder): buildOrder is CircularBuildOrder { + return !!buildOrder && !!(buildOrder as CircularBuildOrder).buildOrder; + } + + /*@internal*/ + export function getBuildOrderFromAnyBuildOrder(anyBuildOrder: AnyBuildOrder): BuildOrder { + return isCircularBuildOrder(anyBuildOrder) ? anyBuildOrder.buildOrder : anyBuildOrder; + } + + export interface SolutionBuilder { + build(project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; + clean(project?: string): ExitStatus; + buildReferences(project: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers): ExitStatus; + cleanReferences(project?: string): ExitStatus; + getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject | undefined; + + // Currently used for testing but can be made public if needed: + /*@internal*/ getBuildOrder(): AnyBuildOrder; + + // Testing only + /*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus; + /*@internal*/ invalidateProject(configFilePath: ResolvedConfigFilePath, reloadLevel?: ConfigFileProgramReloadLevel): void; + /*@internal*/ buildNextInvalidatedProject(): void; + /*@internal*/ getAllParsedConfigs(): readonly ParsedCommandLine[]; + /*@internal*/ close(): void; + } + + /** + * Create a function that reports watch status by writing to the system and handles the formating of the diagnostic + */ + export function createBuilderStatusReporter(system: System, pretty?: boolean): DiagnosticReporter { + return diagnostic => { + let output = pretty ? `[${formatColorAndReset(getLocaleTimeString(system), ForegroundColorEscapeSequences.Grey)}] ` : `${getLocaleTimeString(system)} - `; + output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine}`; + system.write(output); + }; + } + + function createSolutionBuilderHostBase(system: System, createProgram: CreateProgram | undefined, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter) { + const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase; + host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined; + host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop; + host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; + host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); + host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system); + host.now = maybeBind(system, system.now); // For testing + return host; + } + + export function createSolutionBuilderHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportErrorSummary?: ReportEmitErrorSummary) { + const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderHost; + host.reportErrorSummary = reportErrorSummary; + return host; + } + + export function createSolutionBuilderWithWatchHost(system = sys, createProgram?: CreateProgram, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter) { + const host = createSolutionBuilderHostBase(system, createProgram, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderWithWatchHost; + const watchHost = createWatchHost(system, reportWatchStatus); + copyProperties(host, watchHost); + return host; + } + + function getCompilerOptionsOfBuildOptions(buildOptions: BuildOptions): CompilerOptions { + const result = {} as CompilerOptions; + commonOptionsWithBuild.forEach(option => { + if (hasProperty(buildOptions, option.name)) result[option.name] = buildOptions[option.name]; + }); + return result; + } + + export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder { + return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions); + } + + export function createSolutionBuilderWithWatch(host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { + return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions, baseWatchOptions); + } + + 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 extends WatchFactory { + readonly host: SolutionBuilderHost; + readonly hostWithWatch: SolutionBuilderWithWatchHost; + readonly currentDirectory: string; + readonly getCanonicalFileName: GetCanonicalFileName; + readonly parseConfigFileHost: ParseConfigFileHost; + readonly write: ((s: string) => void) | undefined; + + // State of solution + readonly options: BuildOptions; + readonly baseCompilerOptions: CompilerOptions; + readonly rootNames: readonly string[]; + readonly baseWatchOptions: WatchOptions | undefined; + + readonly resolvedConfigFilePaths: ESMap; + readonly configFileCache: ESMap; + /** Map from config file name to up-to-date status */ + readonly projectStatus: ESMap; + readonly buildInfoChecked: ESMap; + readonly extendedConfigCache: ESMap; + + readonly builderPrograms: ESMap; + readonly diagnostics: ESMap; + readonly projectPendingBuild: ESMap; + readonly projectErrorsReported: ESMap; + + readonly compilerHost: CompilerHost; + readonly moduleResolutionCache: ModuleResolutionCache | undefined; + readonly typeReferenceDirectiveResolutionCache: TypeReferenceDirectiveResolutionCache | undefined; + + // Mutable state + buildOrder: AnyBuildOrder | undefined; + readFileWithCache: (f: string) => string | undefined; + projectCompilerOptions: CompilerOptions; + cache: SolutionBuilderStateCache | undefined; + allProjectBuildPending: boolean; + needsSummary: boolean; + watchAllProjectsPending: boolean; + currentInvalidatedProject: InvalidatedProject | undefined; + + // Watch state + readonly watch: boolean; + readonly allWatchedWildcardDirectories: ESMap>; + readonly allWatchedInputFiles: ESMap>; + readonly allWatchedConfigFiles: ESMap; + readonly allWatchedExtendedConfigFiles: ESMap>; + readonly allWatchedPackageJsonFiles: ESMap>; + readonly lastCachedPackageJsonLookups: ESMap; + + timerToBuildInvalidatedProject: any; + reportFileChangeDetected: boolean; + writeLog: (s: string) => void; + } + + function createSolutionBuilderState(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions: WatchOptions | undefined): 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; + const typeReferenceDirectiveResolutionCache = !compilerHost.resolveTypeReferenceDirectives ? createTypeReferenceDirectiveResolutionCache(currentDirectory, getCanonicalFileName, /*options*/ undefined, moduleResolutionCache?.getPackageJsonInfoCache()) : 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.checkEachDefined(moduleNames), containingFile, redirectedReference, loader); + } + if (!compilerHost.resolveTypeReferenceDirectives) { + const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(moduleName, containingFile, state.projectCompilerOptions, compilerHost, redirectedReference, state.typeReferenceDirectiveResolutionCache).resolvedTypeReferenceDirective!; + compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile, redirectedReference) => + loadWithLocalCache(Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, loader); + } + + const { watchFile, watchDirectory, writeLog } = createWatchFactory(hostWithWatch, options); + + const state: SolutionBuilderState = { + host, + hostWithWatch, + currentDirectory, + getCanonicalFileName, + parseConfigFileHost: parseConfigHostFromCompilerHostLike(host), + write: maybeBind(host, host.trace), + + // State of solution + options, + baseCompilerOptions, + rootNames, + baseWatchOptions, + + resolvedConfigFilePaths: new Map(), + configFileCache: new Map(), + projectStatus: new Map(), + buildInfoChecked: new Map(), + extendedConfigCache: new Map(), + + builderPrograms: new Map(), + diagnostics: new Map(), + projectPendingBuild: new Map(), + projectErrorsReported: new Map(), + + compilerHost, + moduleResolutionCache, + typeReferenceDirectiveResolutionCache, + + // Mutable state + buildOrder: undefined, + readFileWithCache: f => host.readFile(f), + projectCompilerOptions: baseCompilerOptions, + cache: undefined, + allProjectBuildPending: true, + needsSummary: true, + watchAllProjectsPending: watch, + currentInvalidatedProject: undefined, + + // Watch state + watch, + allWatchedWildcardDirectories: new Map(), + allWatchedInputFiles: new Map(), + allWatchedConfigFiles: new Map(), + allWatchedExtendedConfigFiles: new Map(), + allWatchedPackageJsonFiles: new Map(), + lastCachedPackageJsonLookups: new Map(), + + timerToBuildInvalidatedProject: undefined, + reportFileChangeDetected: false, + watchFile, + 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 getCachedParsedConfigFile(state: SolutionBuilderState, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined { + const value = state.configFileCache.get(configFilePath); + return value && isParsedCommandLine(value) ? value : undefined; + } + + 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, baseWatchOptions, extendedConfigCache, host } = state; + let parsed: ParsedCommandLine | undefined; + if (host.getParsedCommandLine) { + parsed = host.getParsedCommandLine(configFileName); + if (!parsed) diagnostic = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + } + else { + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; + parsed = getParsedCommandLineOfConfigFile(configFileName, baseCompilerOptions, parseConfigFileHost, extendedConfigCache, baseWatchOptions); + 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[]): AnyBuildOrder { + const temporaryMarks = new Map(); + const permanentMarks = new Map(); + const circularityReportStack: string[] = []; + let buildOrder: ResolvedConfigFileName[] | undefined; + let circularDiagnostics: Diagnostic[] | undefined; + for (const root of roots) { + visit(root); + } + + return circularDiagnostics ? + { buildOrder: buildOrder || emptyArray, circularDiagnostics } : + 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) { + (circularDiagnostics || (circularDiagnostics = [])).push( + createCompilerDiagnostic( + 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 || createStateBuildOrder(state); + } + + function createStateBuildOrder(state: SolutionBuilderState) { + const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f))); + + // Clear all to ResolvedConfigFilePaths cache to start fresh + state.resolvedConfigFilePaths.clear(); + + // TODO(rbuckton): Should be a `Set`, but that requires changing the code below that uses `mutateMapSkippingNewValues` + const currentProjects = new Map( + getBuildOrderFromAnyBuildOrder(buildOrder).map( + resolved => [toResolvedConfigFilePath(state, resolved), true as true]) + ); + + const noopOnDelete = { onDeleteValue: noop }; + // Config file cache + mutateMapSkippingNewValues(state.configFileCache, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectStatus, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.buildInfoChecked, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.builderPrograms, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.diagnostics, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectPendingBuild, currentProjects, noopOnDelete); + mutateMapSkippingNewValues(state.projectErrorsReported, currentProjects, noopOnDelete); + + // Remove watches for the program no longer in the solution + if (state.watch) { + mutateMapSkippingNewValues( + state.allWatchedConfigFiles, + currentProjects, + { onDeleteValue: closeFileWatcher } + ); + + state.allWatchedExtendedConfigFiles.forEach(watcher => { + watcher.projects.forEach(project => { + if (!currentProjects.has(project)) { + watcher.projects.delete(project); + } + }); + watcher.close(); + }); + + mutateMapSkippingNewValues( + state.allWatchedWildcardDirectories, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcherOf) } + ); + + mutateMapSkippingNewValues( + state.allWatchedInputFiles, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) } + ); + + mutateMapSkippingNewValues( + state.allWatchedPackageJsonFiles, + currentProjects, + { onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) } + ); + } + return state.buildOrder = buildOrder; + } + + function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined): AnyBuildOrder | undefined { + const resolvedProject = project && resolveProjectName(state, project); + const buildOrderFromState = getBuildOrder(state); + if (isCircularBuildOrder(buildOrderFromState)) return buildOrderFromState; + if (resolvedProject) { + const projectPath = toResolvedConfigFilePath(state, resolvedProject); + const projectIndex = findIndex( + buildOrderFromState, + configFileName => toResolvedConfigFilePath(state, configFileName) === projectPath + ); + if (projectIndex === -1) return undefined; + } + const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) as BuildOrder : buildOrderFromState; + Debug.assert(!isCircularBuildOrder(buildOrder)); + Debug.assert(!onlyReferences || resolvedProject !== undefined); + Debug.assert(!onlyReferences || buildOrder[buildOrder.length - 1] === resolvedProject); + return onlyReferences ? buildOrder.slice(0, buildOrder.length - 1) : buildOrder; + } + + function enableCache(state: SolutionBuilderState) { + 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, typeReferenceDirectiveResolutionCache } = 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(); + moduleResolutionCache?.clear(); + typeReferenceDirectiveResolutionCache?.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 = getBuildOrderFromAnyBuildOrder(getBuildOrder(state)); + buildOrder.forEach(configFileName => + state.projectPendingBuild.set( + toResolvedConfigFilePath(state, configFileName), + ConfigFileProgramReloadLevel.None + ) + ); + + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); + } + } + + export enum InvalidatedProjectKind { + Build, + UpdateBundle, + UpdateOutputFileStamps + } + + export interface InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind; + readonly project: ResolvedConfigFileName; + /*@internal*/ readonly projectPath: ResolvedConfigFilePath; + /*@internal*/ readonly buildOrder: readonly ResolvedConfigFileName[]; + /** + * To dispose this project and ensure that all the necessary actions are taken and state is updated accordingly + */ + done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): ExitStatus; + getCompilerOptions(): CompilerOptions; + getCurrentDirectory(): string; + } + + export interface UpdateOutputFileStampsProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateOutputFileStamps; + updateOutputFileStatmps(): void; + } + + export interface BuildInvalidedProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.Build; + /* + * Emitting with this builder program without the api provided for this project + * can result in build system going into invalid state as files written reflect the state of the project + */ + getBuilderProgram(): T | undefined; + getProgram(): Program | undefined; + getSourceFile(fileName: string): SourceFile | undefined; + getSourceFiles(): readonly SourceFile[]; + getOptionsDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; + getGlobalDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; + getConfigFileParsingDiagnostics(): readonly Diagnostic[]; + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; + getAllDependencies(sourceFile: SourceFile): readonly string[]; + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult; + /* + * Calling emit directly with targetSourceFile and emitOnlyDtsFiles set to true is not advised since + * emit in build system is responsible in updating status of the project + * If called with targetSourceFile and emitOnlyDtsFiles set to true, the emit just passes to underlying builder and + * wont reflect the status of file as being emitted in the builder + * (if that emit of that source file is required it would be emitted again when making sure invalidated project is completed) + * This emit is not considered actual emit (and hence uptodate status is not reflected if + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult | undefined; + // TODO(shkamat):: investigate later if we can emit even when there are declaration diagnostics + // emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; + } + + export interface UpdateBundleProject extends InvalidatedProjectBase { + readonly kind: InvalidatedProjectKind.UpdateBundle; + emit(writeFile?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined; + } + + export type InvalidatedProject = UpdateOutputFileStampsProject | BuildInvalidedProject | UpdateBundleProject; + + function doneInvalidatedProject( + state: SolutionBuilderState, + projectPath: ResolvedConfigFilePath + ) { + state.projectPendingBuild.delete(projectPath); + state.currentInvalidatedProject = undefined; + return state.diagnostics.has(projectPath) ? + ExitStatus.DiagnosticsPresent_OutputsSkipped : + ExitStatus.Success; + } + + function createUpdateOutputFileStampsProject( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[] + ): UpdateOutputFileStampsProject { + let updateOutputFileStampsPending = true; + return { + kind: InvalidatedProjectKind.UpdateOutputFileStamps, + project, + projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, + updateOutputFileStatmps: () => { + updateOutputTimestamps(state, config, projectPath); + updateOutputFileStampsPending = false; + }, + done: () => { + if (updateOutputFileStampsPending) { + updateOutputTimestamps(state, config, projectPath); + } + return doneInvalidatedProject(state, projectPath); + } + }; + } + + enum BuildStep { + CreateProgram, + SyntaxDiagnostics, + SemanticDiagnostics, + Emit, + EmitBundle, + EmitBuildInfo, + BuildInvalidatedProjectOfBundle, + QueueReferencingProjects, + Done + } + + function createBuildOrUpdateInvalidedProject( + kind: InvalidatedProjectKind.Build | InvalidatedProjectKind.UpdateBundle, + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[], + ): BuildInvalidedProject | UpdateBundleProject { + let step = kind === InvalidatedProjectKind.Build ? BuildStep.CreateProgram : BuildStep.EmitBundle; + let program: T | undefined; + let buildResult: BuildResultFlags | undefined; + let invalidatedProjectOfBundle: BuildInvalidedProject | undefined; + + return kind === InvalidatedProjectKind.Build ? + { + kind, + project, + projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, + getBuilderProgram: () => withProgramOrUndefined(identity), + getProgram: () => + withProgramOrUndefined( + program => program.getProgramOrUndefined() + ), + getSourceFile: fileName => + withProgramOrUndefined( + program => program.getSourceFile(fileName) + ), + getSourceFiles: () => + withProgramOrEmptyArray( + program => program.getSourceFiles() + ), + getOptionsDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getOptionsDiagnostics(cancellationToken) + ), + getGlobalDiagnostics: cancellationToken => + withProgramOrEmptyArray( + program => program.getGlobalDiagnostics(cancellationToken) + ), + getConfigFileParsingDiagnostics: () => + withProgramOrEmptyArray( + program => program.getConfigFileParsingDiagnostics() + ), + getSyntacticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSyntacticDiagnostics(sourceFile, cancellationToken) + ), + getAllDependencies: sourceFile => + withProgramOrEmptyArray( + program => program.getAllDependencies(sourceFile) + ), + getSemanticDiagnostics: (sourceFile, cancellationToken) => + withProgramOrEmptyArray( + program => program.getSemanticDiagnostics(sourceFile, cancellationToken) + ), + getSemanticDiagnosticsOfNextAffectedFile: (cancellationToken, ignoreSourceFile) => + withProgramOrUndefined( + program => + ((program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile) && + (program as any as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile(cancellationToken, ignoreSourceFile) + ), + emit: (targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers) => { + if (targetSourceFile || emitOnlyDtsFiles) { + return withProgramOrUndefined( + program => program.emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers || state.host.getCustomTransformers?.(project)) + ); + } + executeSteps(BuildStep.SemanticDiagnostics, cancellationToken); + if (step === BuildStep.EmitBuildInfo) { + return emitBuildInfo(writeFile, cancellationToken); + } + if (step !== BuildStep.Emit) return undefined; + return emit(writeFile, cancellationToken, customTransformers); + }, + done + } : + { + kind, + project, + projectPath, + buildOrder, + getCompilerOptions: () => config.options, + getCurrentDirectory: () => state.currentDirectory, + emit: (writeFile: WriteFileCallback | undefined, customTransformers: CustomTransformers | undefined) => { + if (step !== BuildStep.EmitBundle) return invalidatedProjectOfBundle; + return emitBundle(writeFile, customTransformers); + }, + done, + }; + + function done(cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { + executeSteps(BuildStep.Done, cancellationToken, writeFile, customTransformers); + return doneInvalidatedProject(state, projectPath); + } + + function withProgramOrUndefined(action: (program: T) => U | undefined): U | undefined { + executeSteps(BuildStep.CreateProgram); + return program && action(program); + } + + function withProgramOrEmptyArray(action: (program: T) => readonly U[]): readonly U[] { + return withProgramOrUndefined(action) || emptyArray; + } + + function createProgram() { + Debug.assert(program === undefined); + + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_build_project_0, project); + buildResult = BuildResultFlags.Success; + step = BuildStep.QueueReferencingProjects; + return; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Building_project_0, project); + + if (config.fileNames.length === 0) { + reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); + // Nothing to build - must be a solution file, basically + buildResult = BuildResultFlags.None; + step = BuildStep.QueueReferencingProjects; + return; + } + + const { host, compilerHost } = state; + state.projectCompilerOptions = config.options; + // Update module resolution cache if needed + state.moduleResolutionCache?.update(config.options); + state.typeReferenceDirectiveResolutionCache?.update(config.options); + + // Create program + program = host.createProgram( + config.fileNames, + config.options, + compilerHost, + getOldProgram(state, projectPath, config), + getConfigFileParsingDiagnostics(config), + config.projectReferences + ); + + if (state.watch) { + state.lastCachedPackageJsonLookups.set(projectPath, state.moduleResolutionCache && map( + state.moduleResolutionCache.getPackageJsonInfoCache().entries(), + ([path, data]) => ([state.host.realpath && data ? toPath(state, state.host.realpath(path)) : path, data] as const) + )); + + state.builderPrograms.set(projectPath, program); + } + step++; + } + + function handleDiagnostics(diagnostics: readonly Diagnostic[], errorFlags: BuildResultFlags, errorType: string) { + if (diagnostics.length) { + ({ buildResult, step } = buildErrors( + state, + projectPath, + program, + config, + diagnostics, + errorFlags, + errorType + )); + } + else { + step++; + } + } + + function getSyntaxDiagnostics(cancellationToken?: CancellationToken) { + Debug.assertIsDefined(program); + handleDiagnostics( + [ + ...program.getConfigFileParsingDiagnostics(), + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken) + ], + BuildResultFlags.SyntaxErrors, + "Syntactic" + ); + } + + function getSemanticDiagnostics(cancellationToken?: CancellationToken) { + handleDiagnostics( + Debug.checkDefined(program).getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken), + BuildResultFlags.TypeErrors, + "Semantic" + ); + } + + function emit(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitResult { + Debug.assertIsDefined(program); + Debug.assert(step === BuildStep.Emit); + // Before emitting lets backup state, so we can revert it back if there are declaration errors to handle emit and declaration errors correctly + program.backupState(); + let declDiagnostics: Diagnostic[] | undefined; + const reportDeclarationDiagnostics = (d: Diagnostic) => (declDiagnostics || (declDiagnostics = [])).push(d); + const outputFiles: OutputFile[] = []; + const { emitResult } = emitFilesAndReportErrors( + program, + reportDeclarationDiagnostics, + /*write*/ undefined, + /*reportSummary*/ undefined, + (name, text, writeByteOrderMark) => outputFiles.push({ name, text, writeByteOrderMark }), + cancellationToken, + /*emitOnlyDts*/ false, + customTransformers || state.host.getCustomTransformers?.(project) + ); + // Don't emit .d.ts if there are decl file errors + if (declDiagnostics) { + program.restoreState(); + ({ buildResult, step } = buildErrors( + state, + projectPath, + program, + config, + declDiagnostics, + BuildResultFlags.DeclarationEmitErrors, + "Declaration file" + )); + return { + emitSkipped: true, + diagnostics: emitResult.diagnostics + }; + } + + // Actual Emit + const { host, compilerHost } = state; + let resultFlags = BuildResultFlags.DeclarationOutputUnchanged; + let newestDeclarationFileContentChangedTime = minimumDate; + let anyDtsChanged = false; + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = new Map(); + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + let priorChangeTime: Date | undefined; + if (!anyDtsChanged && isDeclarationFile(name)) { + // Check for unchanged .d.ts files + if (host.fileExists(name) && state.readFileWithCache(name) === text) { + priorChangeTime = host.getModifiedTime(name); + } + else { + resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; + anyDtsChanged = true; + } + } + + emittedOutputs.set(toPath(state, name), name); + writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + if (priorChangeTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); + } + }); + + finishEmit( + emitterDiagnostics, + emittedOutputs, + newestDeclarationFileContentChangedTime, + /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ anyDtsChanged, + outputFiles.length ? outputFiles[0].name : getFirstProjectOutput(config, !host.useCaseSensitiveFileNames()), + resultFlags + ); + return emitResult; + } + + function emitBuildInfo(writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult { + Debug.assertIsDefined(program); + Debug.assert(step === BuildStep.EmitBuildInfo); + const emitResult = program.emitBuildInfo(writeFileCallback, cancellationToken); + if (emitResult.diagnostics.length) { + reportErrors(state, emitResult.diagnostics); + state.diagnostics.set(projectPath, [...state.diagnostics.get(projectPath)!, ...emitResult.diagnostics]); + buildResult = BuildResultFlags.EmitErrors & buildResult!; + } + + if (emitResult.emittedFiles && state.write) { + emitResult.emittedFiles.forEach(name => listEmittedFile(state, config, name)); + } + afterProgramDone(state, program, config); + step = BuildStep.QueueReferencingProjects; + return emitResult; + } + + function finishEmit( + emitterDiagnostics: DiagnosticCollection, + emittedOutputs: ESMap, + priorNewestUpdateTime: Date, + newestDeclarationFileContentChangedTimeIsMaximumDate: boolean, + oldestOutputFileName: string, + resultFlags: BuildResultFlags + ) { + const emitDiagnostics = emitterDiagnostics.getDiagnostics(); + if (emitDiagnostics.length) { + ({ buildResult, step } = buildErrors( + state, + projectPath, + program, + config, + emitDiagnostics, + BuildResultFlags.EmitErrors, + "Emit" + )); + return emitDiagnostics; + } + + if (state.write) { + emittedOutputs.forEach(name => listEmittedFile(state, config, name)); + } + + // Update time stamps for rest of the outputs + const newestDeclarationFileContentChangedTime = updateOutputTimestampsWorker(state, config, priorNewestUpdateTime, Diagnostics.Updating_unchanged_output_timestamps_of_project_0, emittedOutputs); + state.diagnostics.delete(projectPath); + state.projectStatus.set(projectPath, { + type: UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime: newestDeclarationFileContentChangedTimeIsMaximumDate ? + maximumDate : + newestDeclarationFileContentChangedTime, + oldestOutputFileName + }); + afterProgramDone(state, program, config); + step = BuildStep.QueueReferencingProjects; + buildResult = resultFlags; + return emitDiagnostics; + } + + function emitBundle(writeFileCallback?: WriteFileCallback, customTransformers?: CustomTransformers): EmitResult | BuildInvalidedProject | undefined { + Debug.assert(kind === InvalidatedProjectKind.UpdateBundle); + if (state.options.dry) { + reportStatus(state, Diagnostics.A_non_dry_build_would_update_output_of_project_0, project); + buildResult = BuildResultFlags.Success; + step = BuildStep.QueueReferencingProjects; + return undefined; + } + + if (state.options.verbose) reportStatus(state, Diagnostics.Updating_output_of_project_0, project); + + // Update js, and source map + const { compilerHost } = state; + state.projectCompilerOptions = config.options; + const outputFiles = emitUsingBuildInfo( + config, + compilerHost, + ref => { + const refName = resolveProjectName(state, ref.path); + return parseConfigFile(state, refName, toResolvedConfigFilePath(state, refName)); + }, + customTransformers || state.host.getCustomTransformers?.(project) + ); + + if (isString(outputFiles)) { + reportStatus(state, Diagnostics.Cannot_update_output_of_project_0_because_there_was_error_reading_file_1, project, relName(state, outputFiles)); + step = BuildStep.BuildInvalidatedProjectOfBundle; + return invalidatedProjectOfBundle = createBuildOrUpdateInvalidedProject( + InvalidatedProjectKind.Build, + state, + project, + projectPath, + projectIndex, + config, + buildOrder + ) as BuildInvalidedProject; + } + + // Actual Emit + Debug.assert(!!outputFiles.length); + const emitterDiagnostics = createDiagnosticCollection(); + const emittedOutputs = new Map(); + outputFiles.forEach(({ name, text, writeByteOrderMark }) => { + emittedOutputs.set(toPath(state, name), name); + writeFile(writeFileCallback ? { writeFile: writeFileCallback } : compilerHost, emitterDiagnostics, name, text, writeByteOrderMark); + }); + + const emitDiagnostics = finishEmit( + emitterDiagnostics, + emittedOutputs, + minimumDate, + /*newestDeclarationFileContentChangedTimeIsMaximumDate*/ false, + outputFiles[0].name, + BuildResultFlags.DeclarationOutputUnchanged + ); + return { emitSkipped: false, diagnostics: emitDiagnostics }; + } + + function executeSteps(till: BuildStep, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, customTransformers?: CustomTransformers) { + while (step <= till && step < BuildStep.Done) { + const currentStep = step; + switch (step) { + case BuildStep.CreateProgram: + createProgram(); + break; + + case BuildStep.SyntaxDiagnostics: + getSyntaxDiagnostics(cancellationToken); + break; + + case BuildStep.SemanticDiagnostics: + getSemanticDiagnostics(cancellationToken); + break; + + case BuildStep.Emit: + emit(writeFile, cancellationToken, customTransformers); + break; + + case BuildStep.EmitBuildInfo: + emitBuildInfo(writeFile, cancellationToken); + break; + + case BuildStep.EmitBundle: + emitBundle(writeFile, customTransformers); + break; + + case BuildStep.BuildInvalidatedProjectOfBundle: + Debug.checkDefined(invalidatedProjectOfBundle).done(cancellationToken, writeFile, customTransformers); + step = BuildStep.Done; + break; + + case BuildStep.QueueReferencingProjects: + queueReferencingProjects(state, project, projectPath, projectIndex, config, buildOrder, Debug.checkDefined(buildResult)); + step++; + break; + + // Should never be done + case BuildStep.Done: + default: + assertType(step); + + } + Debug.assert(step > currentStep); + } + } + } + + function needsBuild({ options }: SolutionBuilderState, status: UpToDateStatus, config: ParsedCommandLine) { + if (status.type !== UpToDateStatusType.OutOfDateWithPrepend || options.force) return true; + return config.fileNames.length === 0 || + !!getConfigFileParsingDiagnostics(config).length || + !isIncrementalCompilation(config.options); + } + + function getNextInvalidatedProject( + state: SolutionBuilderState, + buildOrder: AnyBuildOrder, + reportQueue: boolean + ): InvalidatedProject | undefined { + if (!state.projectPendingBuild.size) return undefined; + if (isCircularBuildOrder(buildOrder)) return undefined; + if (state.currentInvalidatedProject) { + // Only if same buildOrder the currentInvalidated project can be sent again + return arrayIsEqualTo(state.currentInvalidatedProject.buildOrder, buildOrder) ? + state.currentInvalidatedProject : + undefined; + } + + const { options, projectPendingBuild } = state; + for (let projectIndex = 0; projectIndex < buildOrder.length; projectIndex++) { + const project = buildOrder[projectIndex]; + const projectPath = toResolvedConfigFilePath(state, project); + const reloadLevel = state.projectPendingBuild.get(projectPath); + if (reloadLevel === undefined) continue; + + if (reportQueue) { + reportQueue = false; + reportBuildQueue(state, buildOrder); + } + + const config = parseConfigFile(state, project, projectPath); + if (!config) { + reportParseConfigFileDiagnostic(state, projectPath); + projectPendingBuild.delete(projectPath); + continue; + } + + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + watchConfigFile(state, project, projectPath, config); + watchExtendedConfigFiles(state, projectPath, config); + watchWildCardDirectories(state, project, projectPath, config); + watchInputFiles(state, project, projectPath, config); + watchPackageJsonFiles(state, project, projectPath, config); + } + else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { + // Update file names + config.fileNames = getFileNamesFromConfigSpecs(config.options.configFile!.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost); + updateErrorForNoInputFiles(config.fileNames, project, config.options.configFile!.configFileSpecs!, config.errors, canJsonReportNoInputFiles(config.raw)); + watchInputFiles(state, project, projectPath, config); + watchPackageJsonFiles(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, getConfigFileParsingDiagnostics(config)); + projectPendingBuild.delete(projectPath); + // Up to date, skip + if (options.dry) { + // In a dry build, inform the user of this fact + reportStatus(state, Diagnostics.Project_0_is_up_to_date, project); + } + continue; + } + + if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes) { + reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); + return createUpdateOutputFileStampsProject( + state, + project, + projectPath, + config, + buildOrder + ); + } + } + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); + projectPendingBuild.delete(projectPath); + if (options.verbose) { + reportStatus( + state, + status.upstreamProjectBlocked ? + Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_was_not_built : + Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, + project, + status.upstreamProjectName + ); + } + continue; + } + + if (status.type === UpToDateStatusType.ContainerOnly) { + reportAndStoreErrors(state, projectPath, getConfigFileParsingDiagnostics(config)); + projectPendingBuild.delete(projectPath); + // Do nothing + continue; + } + + return createBuildOrUpdateInvalidedProject( + needsBuild(state, status, config) ? + InvalidatedProjectKind.Build : + InvalidatedProjectKind.UpdateBundle, + state, + project, + projectPath, + projectIndex, + config, + buildOrder, + ); + } + + return undefined; + } + + function listEmittedFile({ write }: SolutionBuilderState, proj: ParsedCommandLine, file: string) { + if (write && proj.options.listEmittedFiles) { + write(`TSFILE: ${file}`); + } + } + + function getOldProgram({ options, builderPrograms, compilerHost }: SolutionBuilderState, proj: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (options.force) return undefined; + const value = builderPrograms.get(proj); + if (value) return value; + return readBuilderProgram(parsed.options, compilerHost) as any as T; + } + + function afterProgramDone( + state: SolutionBuilderState, + program: T | undefined, + config: ParsedCommandLine + ) { + if (program) { + if (program && state.write) listFiles(program, state.write); + if (state.host.afterProgramEmitAndDiagnostics) { + state.host.afterProgramEmitAndDiagnostics(program); + } + program.releaseProgram(); + } + else if (state.host.afterEmitBundle) { + state.host.afterEmitBundle(config); + } + state.projectCompilerOptions = state.baseCompilerOptions; + } + + function buildErrors( + state: SolutionBuilderState, + resolvedPath: ResolvedConfigFilePath, + program: T | undefined, + config: ParsedCommandLine, + diagnostics: readonly Diagnostic[], + buildResult: BuildResultFlags, + errorType: string, + ) { + const canEmitBuildInfo = !(buildResult & BuildResultFlags.SyntaxErrors) && program && !outFile(program.getCompilerOptions()); + + reportAndStoreErrors(state, resolvedPath, diagnostics); + state.projectStatus.set(resolvedPath, { type: UpToDateStatusType.Unbuildable, reason: `${errorType} errors` }); + if (canEmitBuildInfo) return { buildResult, step: BuildStep.EmitBuildInfo }; + afterProgramDone(state, program, config); + return { buildResult, step: BuildStep.QueueReferencingProjects }; + } + + function checkConfigFileUpToDateStatus(state: SolutionBuilderState, configFile: string, oldestOutputFileTime: Date, oldestOutputFileName: string): Status.OutOfDateWithSelf | undefined { + // Check tsconfig time + const tsconfigTime = getModifiedTime(state.host, configFile); + if (oldestOutputFileTime < tsconfigTime) { + return { + type: UpToDateStatusType.OutOfDateWithSelf, + outOfDateOutputFileName: oldestOutputFileName, + newerInputFileName: configFile + }; + } + } + + function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus { + const force = !!state.options.force; + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; + const { host } = state; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!host.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; + } + + if (!force) { + const inputTime = getModifiedTime(host, inputFile); host.getModifiedTime(inputFile); + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; + } + } + } + + // Container if no files are specified in the project + if (!project.fileNames.length && !canJsonReportNoInputFiles(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; + if (!force) { + 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 = getModifiedTime(host, output); + 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 = getModifiedTime(host, output); + 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 || + refStatus.type === UpToDateStatusType.ContainerOnly) { // Container only ignore this project + continue; + } + + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable || + refStatus.type === UpToDateStatusType.UpstreamBlocked) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path, + upstreamProjectBlocked: refStatus.type === UpToDateStatusType.UpstreamBlocked + }; + } + + // 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 + // (a check we will have skipped if this is a forced build) + if (!force && !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; + + // Check package file time + const dependentPackageFileStatus = forEach( + state.lastCachedPackageJsonLookups.get(resolvedPath) || emptyArray, + ([path]) => checkConfigFileUpToDateStatus(state, path, oldestOutputFileTime, oldestOutputFileName) + ); + if (dependentPackageFileStatus) return dependentPackageFileStatus; + } + + if (!force && !state.buildInfoChecked.has(resolvedPath)) { + state.buildInfoChecked.set(resolvedPath, true); + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(project.options); + if (buildInfoPath) { + const value = state.readFileWithCache(buildInfoPath); + const buildInfo = value && getBuildInfo(value); + if (buildInfo && (buildInfo.bundle || buildInfo.program) && buildInfo.version !== version) { + return { + type: UpToDateStatusType.TsVersionOutputOfDate, + version: buildInfo.version + }; + } + } + } + + 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?: ESMap) { + if (proj.options.noEmit) return priorNewestUpdateTime; + 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, getModifiedTime(host, file)); + } + + host.setModifiedTime(file, now); + } + } + + 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 queueReferencingProjects( + state: SolutionBuilderState, + project: ResolvedConfigFileName, + projectPath: ResolvedConfigFilePath, + projectIndex: number, + config: ParsedCommandLine, + buildOrder: readonly ResolvedConfigFileName[], + buildResult: BuildResultFlags + ) { + // Queue only if there are no errors + if (buildResult & BuildResultFlags.AnyErrors) return; + // Only composite projects can be referenced by other projects + if (!config.options.composite) return; + // Always use build order to queue projects + for (let index = projectIndex + 1; index < buildOrder.length; index++) { + const nextProject = buildOrder[index]; + const nextProjectPath = toResolvedConfigFilePath(state, nextProject); + if (state.projectPendingBuild.has(nextProjectPath)) continue; + + const nextProjectConfig = parseConfigFile(state, nextProject, nextProjectPath); + if (!nextProjectConfig || !nextProjectConfig.projectReferences) continue; + for (const ref of nextProjectConfig.projectReferences) { + const resolvedRefPath = resolveProjectName(state, ref.path); + if (toResolvedConfigFilePath(state, resolvedRefPath) !== projectPath) continue; + // If the project is referenced with prepend, always build downstream projects, + // If declaration output is changed, build the project + // otherwise mark the project UpToDateWithUpstreamTypes so it updates output time stamps + const status = state.projectStatus.get(nextProjectPath); + if (status) { + switch (status.type) { + case UpToDateStatusType.UpToDate: + if (buildResult & BuildResultFlags.DeclarationOutputUnchanged) { + if (ref.prepend) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithPrepend, + outOfDateOutputFileName: status.oldestOutputFileName, + newerProjectName: project + }); + } + else { + status.type = UpToDateStatusType.UpToDateWithUpstreamTypes; + } + break; + } + // falls through + + case UpToDateStatusType.UpToDateWithUpstreamTypes: + case UpToDateStatusType.OutOfDateWithPrepend: + if (!(buildResult & BuildResultFlags.DeclarationOutputUnchanged)) { + state.projectStatus.set(nextProjectPath, { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: status.type === UpToDateStatusType.OutOfDateWithPrepend ? status.outOfDateOutputFileName : status.oldestOutputFileName, + newerProjectName: project + }); + } + break; + + case UpToDateStatusType.UpstreamBlocked: + if (toResolvedConfigFilePath(state, resolveProjectName(state, status.upstreamProjectName)) === projectPath) { + clearProjectStatus(state, nextProjectPath); + } + break; + } + } + addProjToQueue(state, nextProjectPath, ConfigFileProgramReloadLevel.None); + break; + } + } + } + + function build(state: SolutionBuilderState, project?: string, cancellationToken?: CancellationToken, writeFile?: WriteFileCallback, getCustomTransformers?: (project: string) => CustomTransformers, onlyReferences?: boolean): ExitStatus { + const buildOrder = getBuildOrderFor(state, project, onlyReferences); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + setupInitialBuild(state, cancellationToken); + + let reportQueue = true; + let successfulProjects = 0; + while (true) { + const invalidatedProject = getNextInvalidatedProject(state, buildOrder, reportQueue); + if (!invalidatedProject) break; + reportQueue = false; + invalidatedProject.done(cancellationToken, writeFile, getCustomTransformers?.(invalidatedProject.project)); + if (!state.diagnostics.has(invalidatedProject.projectPath)) successfulProjects++; + } + + disableCache(state); + reportErrorSummary(state, buildOrder); + startWatching(state, buildOrder); + + return isCircularBuildOrder(buildOrder) + ? ExitStatus.ProjectReferenceCycle_OutputsSkipped + : !buildOrder.some(p => state.diagnostics.has(toResolvedConfigFilePath(state, p))) + ? ExitStatus.Success + : successfulProjects + ? ExitStatus.DiagnosticsPresent_OutputsGenerated + : ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + + function clean(state: SolutionBuilderState, project?: string, onlyReferences?: boolean) { + const buildOrder = getBuildOrderFor(state, project, onlyReferences); + if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped; + + if (isCircularBuildOrder(buildOrder)) { + reportErrors(state, buildOrder.circularDiagnostics); + return ExitStatus.ProjectReferenceCycle_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()); + if (!outputs.length) continue; + const inputFileNames = new Set(parsed.fileNames.map(f => toPath(state, f))); + for (const output of outputs) { + // If output name is same as input file name, do not delete and ignore the error + if (inputFileNames.has(toPath(state, output))) continue; + 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 host implements getParsedCommandLine, we cant get list of files from parseConfigFileHost + if (state.host.getParsedCommandLine && reloadLevel === ConfigFileProgramReloadLevel.Partial) { + reloadLevel = ConfigFileProgramReloadLevel.Full; + } + if (reloadLevel === ConfigFileProgramReloadLevel.Full) { + state.configFileCache.delete(resolved); + state.buildOrder = undefined; + } + 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 buildOrder = getBuildOrder(state); + const invalidatedProject = getNextInvalidatedProject(state, buildOrder, /*reportQueue*/ false); + if (invalidatedProject) { + invalidatedProject.done(); + if (state.projectPendingBuild.size) { + // Schedule next project for build + if (state.watch && !state.timerToBuildInvalidatedProject) { + scheduleBuildInvalidatedProject(state); + } + return; + } + } + disableCache(state); + reportErrorSummary(state, buildOrder); + } + + function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) { + if (!state.watch || state.allWatchedConfigFiles.has(resolvedPath)) return; + state.allWatchedConfigFiles.set(resolvedPath, state.watchFile( + resolved, + () => { + invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full); + }, + PollingInterval.High, + parsed?.watchOptions, + WatchType.ConfigFile, + resolved + )); + } + + function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) { + updateSharedExtendedConfigFileWatcher( + resolvedPath, + parsed?.options, + state.allWatchedExtendedConfigFiles, + (extendedConfigFileName, extendedConfigFilePath) => state.watchFile( + extendedConfigFileName, + () => state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath)?.projects.forEach(projectConfigFilePath => + invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full) + ), + PollingInterval.High, + parsed?.watchOptions, + WatchType.ExtendedConfigFile, + ), + fileName => toPath(state, fileName), + ); + } + + function watchWildCardDirectories(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (!state.watch) return; + updateWatchingWildcardDirectories( + getOrCreateValueMapFromConfigFileMap(state.allWatchedWildcardDirectories, resolvedPath), + new Map(getEntries(parsed.wildcardDirectories!)), + (dir, flags) => state.watchDirectory( + dir, + fileOrDirectory => { + if (isIgnoredFileFromWildCardWatching({ + watchedDirPath: toPath(state, dir), + fileOrDirectory, + fileOrDirectoryPath: toPath(state, fileOrDirectory), + configFileName: resolved, + currentDirectory: state.currentDirectory, + options: parsed.options, + program: state.builderPrograms.get(resolvedPath) || getCachedParsedConfigFile(state, resolvedPath)?.fileNames, + useCaseSensitiveFileNames: state.parseConfigFileHost.useCaseSensitiveFileNames, + writeLog: s => state.writeLog(s), + toPath: fileName => toPath(state, fileName) + })) return; + + invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial); + }, + flags, + parsed?.watchOptions, + 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.watchFile( + input, + () => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None), + PollingInterval.Low, + parsed?.watchOptions, + WatchType.SourceFile, + resolved + ), + onDeleteValue: closeFileWatcher, + } + ); + } + + function watchPackageJsonFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) { + if (!state.watch || !state.lastCachedPackageJsonLookups) return; + mutateMap( + getOrCreateValueMapFromConfigFileMap(state.allWatchedPackageJsonFiles, resolvedPath), + new Map(state.lastCachedPackageJsonLookups.get(resolvedPath)), + { + createNewValue: (path, _input) => state.watchFile( + path, + () => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full), + PollingInterval.High, + parsed?.watchOptions, + WatchType.PackageJson, + resolved + ), + onDeleteValue: closeFileWatcher, + } + ); + } + + function startWatching(state: SolutionBuilderState, buildOrder: AnyBuildOrder) { + if (!state.watchAllProjectsPending) return; + state.watchAllProjectsPending = false; + for (const resolved of getBuildOrderFromAnyBuildOrder(buildOrder)) { + const resolvedPath = toResolvedConfigFilePath(state, resolved); + const cfg = parseConfigFile(state, resolved, resolvedPath); + // Watch this file + watchConfigFile(state, resolved, resolvedPath, cfg); + watchExtendedConfigFiles(state, resolvedPath, cfg); + if (cfg) { + // Update watchers for wildcard directories + watchWildCardDirectories(state, resolved, resolvedPath, cfg); + + // Watch input files + watchInputFiles(state, resolved, resolvedPath, cfg); + + // Watch package json files + watchPackageJsonFiles(state, resolved, resolvedPath, cfg); + } + } + } + + function stopWatching(state: SolutionBuilderState) { + clearMap(state.allWatchedConfigFiles, closeFileWatcher); + clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf); + clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf)); + clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher)); + clearMap(state.allWatchedPackageJsonFiles, watchedPacageJsonFiles => clearMap(watchedPacageJsonFiles, closeFileWatcher)); + } + + /** + * 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: readonly string[], defaultOptions: BuildOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: true, host: SolutionBuilderWithWatchHost, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder; + function createSolutionBuilderWorker(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost | SolutionBuilderWithWatchHost, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder { + const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions); + return { + build: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers), + clean: project => clean(state, project), + buildReferences: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers, /*onlyReferences*/ true), + cleanReferences: project => clean(state, project, /*onlyReferences*/ true), + getNextInvalidatedProject: cancellationToken => { + setupInitialBuild(state, cancellationToken); + return getNextInvalidatedProject(state, getBuildOrder(state), /*reportQueue*/ false); + }, + getBuildOrder: () => getBuildOrder(state), + getUpToDateStatusOfProject: project => { + const configFileName = resolveProjectName(state, project); + const configFilePath = toResolvedConfigFilePath(state, configFileName); + return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath); + }, + invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None), + buildNextInvalidatedProject: () => buildNextInvalidatedProject(state), + getAllParsedConfigs: () => arrayFrom(mapDefinedIterator( + state.configFileCache.values(), + config => isParsedCommandLine(config) ? config : undefined + )), + close: () => stopWatching(state), + }; + } + + function relName(state: SolutionBuilderState, path: string): string { + return convertToRelativePath(path, state.currentDirectory, f => state.getCanonicalFileName(f)); + } + + function reportStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: string[]) { + state.host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); + } + + function reportWatchStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { + state.hostWithWatch.onWatchStatusChange?.(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions); + } + + function reportErrors({ host }: SolutionBuilderState, errors: readonly Diagnostic[]) { + errors.forEach(err => host.reportDiagnostic(err)); + } + + function reportAndStoreErrors(state: SolutionBuilderState, proj: ResolvedConfigFilePath, errors: readonly Diagnostic[]) { + 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, buildOrder: AnyBuildOrder) { + if (!state.needsSummary) return; + state.needsSummary = false; + const canReportSummary = state.watch || !!state.host.reportErrorSummary; + const { diagnostics } = state; + let totalErrors = 0; + if (isCircularBuildOrder(buildOrder)) { + reportBuildQueue(state, buildOrder.buildOrder); + reportErrors(state, buildOrder.circularDiagnostics); + if (canReportSummary) totalErrors += getErrorCountForSummary(buildOrder.circularDiagnostics); + } + else { + // Report errors from the other projects + buildOrder.forEach(project => { + const projectPath = toResolvedConfigFilePath(state, project); + if (!state.projectErrorsReported.has(projectPath)) { + reportErrors(state, diagnostics.get(projectPath) || emptyArray); + } + }); + if (canReportSummary) diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors)); + } + + if (state.watch) { + reportWatchStatus(state, getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors); + } + else if (state.host.reportErrorSummary) { + 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) { + if (state.options.force && (status.type === UpToDateStatusType.UpToDate || status.type === UpToDateStatusType.UpToDateWithUpstreamTypes)) { + return reportStatus( + state, + Diagnostics.Project_0_is_being_forcibly_rebuilt, + relName(state, configFileName) + ); + } + + switch (status.type) { + case UpToDateStatusType.OutOfDateWithSelf: + 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 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 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 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 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 reportStatus( + state, + Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, + relName(state, configFileName) + ); + case UpToDateStatusType.UpstreamOutOfDate: + 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 reportStatus( + state, + status.upstreamProjectBlocked ? + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_was_not_built : + Diagnostics.Project_0_can_t_be_built_because_its_dependency_1_has_errors, + relName(state, configFileName), + relName(state, status.upstreamProjectName) + ); + case UpToDateStatusType.Unbuildable: + return reportStatus( + state, + Diagnostics.Failed_to_parse_file_0_Colon_1, + relName(state, configFileName), + status.reason + ); + case UpToDateStatusType.TsVersionOutputOfDate: + 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 + ); + case UpToDateStatusType.ContainerOnly: + // Don't report status on "solution" projects + // falls through + case UpToDateStatusType.ComputingUpstream: + // Should never leak from getUptoDateStatusWorker + break; + default: + 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); + } + } +}