diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index e72e9d1903e..55af0080983 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -37,8 +37,8 @@ namespace ts { projectStatus: ConfigFileMap; diagnostics?: ConfigFileMap; // TODO(shkamat): this should be really be diagnostics but thats for later time - invalidateProject(project: ResolvedConfigFileName, dependencyGraph: DependencyGraph | undefined): void; - getNextInvalidatedProject(): ResolvedConfigFileName | undefined; + invalidateProject(project: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel | undefined, dependencyGraph: DependencyGraph | undefined): void; + getNextInvalidatedProject(): { project: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel } | undefined; hasPendingInvalidatedProjects(): boolean; missingRoots: Map; } @@ -341,40 +341,6 @@ namespace ts { return opts.rootDir || getDirectoryPath(configFileName); } - function createConfigFileCache(host: CompilerHost, toPath: ToResolvedConfigFilePath) { - const cache = createFileMap(toPath); - const configParseHost = parseConfigHostFromCompilerHost(host); - - function isParsedCommandLine(value: ParsedCommandLine | "error"): value is ParsedCommandLine { - return !(value as "error").length; - } - - function parseConfigFile(configFilePath: ResolvedConfigFileName) { - const value = cache.getValue(configFilePath); - if (value) { - return isParsedCommandLine(value) ? value : undefined; - } - - const sourceFile = host.getSourceFile(configFilePath, ScriptTarget.JSON) as JsonSourceFile; - if (sourceFile === undefined) { - return undefined; - } - - const parsed = parseJsonSourceFileConfigFileContent(sourceFile, configParseHost, getDirectoryPath(configFilePath)); - parsed.options.configFilePath = configFilePath; - cache.setValue(configFilePath, parsed); - return parsed; - } - - function removeKey(configFilePath: ResolvedConfigFileName) { - cache.removeKey(configFilePath); - } - - return { - parseConfigFile, - removeKey - }; - } function newer(date1: Date, date2: Date): Date { return date2 > date1 ? date2 : date1; @@ -387,7 +353,7 @@ namespace ts { export function createBuildContext(options: BuildOptions, toPath: ToResolvedConfigFilePath): BuildContext { const invalidatedProjectQueue = [] as ResolvedConfigFileName[]; let nextIndex = 0; - const projectPendingBuild = createFileMap(toPath); + const projectPendingBuild = createFileMap(toPath); const missingRoots = createMap(); const diagnostics = options.watch ? createFileMap(toPath) : undefined; @@ -402,31 +368,39 @@ namespace ts { missingRoots }; - function invalidateProject(proj: ResolvedConfigFileName, dependencyGraph: DependencyGraph | undefined) { - if (!projectPendingBuild.hasKey(proj)) { - addProjToQueue(proj); - if (dependencyGraph) { - queueBuildForDownstreamReferences(proj, dependencyGraph); - } + function invalidateProject(proj: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel | undefined, dependencyGraph: DependencyGraph | undefined) { + if (addProjToQueue(proj, reloadLevel) && dependencyGraph) { + queueBuildForDownstreamReferences(proj, dependencyGraph); } } - function addProjToQueue(proj: ResolvedConfigFileName) { - Debug.assert(!projectPendingBuild.hasKey(proj)); - projectPendingBuild.setValue(proj, true); - invalidatedProjectQueue.push(proj); + /** + * return true if new addition + */ + function addProjToQueue(proj: ResolvedConfigFileName, reloadLevel?: ConfigFileProgramReloadLevel) { + const value = projectPendingBuild.getValue(proj); + if (value === undefined) { + projectPendingBuild.setValue(proj, reloadLevel || ConfigFileProgramReloadLevel.None); + invalidatedProjectQueue.push(proj); + return true; + } + + if (value < (reloadLevel || ConfigFileProgramReloadLevel.None)) { + projectPendingBuild.setValue(proj, reloadLevel || ConfigFileProgramReloadLevel.None); + } } function getNextInvalidatedProject() { if (nextIndex < invalidatedProjectQueue.length) { - const proj = invalidatedProjectQueue[nextIndex]; + const project = invalidatedProjectQueue[nextIndex]; nextIndex++; - projectPendingBuild.removeKey(proj); + const reloadLevel = projectPendingBuild.getValue(project)!; + projectPendingBuild.removeKey(project); if (!projectPendingBuild.getSize()) { invalidatedProjectQueue.length = 0; nextIndex = 0; } - return proj; + return { project, reloadLevel }; } } @@ -439,8 +413,7 @@ namespace ts { const deps = dependencyGraph.dependencyMap.getReferencesTo(root); for (const ref of deps) { // Can skip circular references - if (!projectPendingBuild.hasKey(ref)) { - addProjToQueue(ref); + if (addProjToQueue(ref)) { queueBuildForDownstreamReferences(ref, dependencyGraph); } } @@ -501,12 +474,15 @@ namespace ts { const hostWithWatch = host as SolutionBuilderWithWatchHost; const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - const configFileCache = createConfigFileCache(host, toPath); + const parseConfigFileHost = parseConfigHostFromCompilerHost(host); + type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic; + const configFileCache = createFileMap(toPath); let context = createBuildContext(defaultOptions, toPath); let timerToBuildInvalidatedProject: any; let reportFileChangeDetected = false; - const existingWatchersForWildcards = createMap(); + const existingWatchersForWildcards = createFileMap>(toPath); + return { buildAllProjects, getUpToDateStatus, @@ -529,6 +505,24 @@ namespace ts { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } + function isParsedCommandLine(entry: ConfigFileCacheEntry): entry is ParsedCommandLine { + return !!(entry as ParsedCommandLine).options; + } + + function parseConfigFile(configFilePath: ResolvedConfigFileName): ParsedCommandLine | undefined { + const value = configFileCache.getValue(configFilePath); + if (value) { + return isParsedCommandLine(value) ? value : undefined; + } + + let diagnostic: Diagnostic | undefined; + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = d => diagnostic = d; + const parsed = getParsedCommandLineOfConfigFile(configFilePath, {}, parseConfigFileHost); + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = noop; + configFileCache.setValue(configFilePath, parsed || diagnostic!); + return parsed; + } + function reportStatus(message: DiagnosticMessage, ...args: string[]) { host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); } @@ -559,19 +553,36 @@ namespace ts { } for (const resolved of graph.buildQueue) { - const cfg = configFileCache.parseConfigFile(resolved); + const cfg = parseConfigFile(resolved); if (cfg) { // Watch this file hostWithWatch.watchFile(resolved, () => { configFileCache.removeKey(resolved); - invalidateProjectAndScheduleBuilds(resolved); + invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Full); }); // Update watchers for wildcard directories if (cfg.configFileSpecs) { - updateWatchingWildcardDirectories(existingWatchersForWildcards, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { - return hostWithWatch.watchDirectory(dir, () => { - invalidateProjectAndScheduleBuilds(resolved); + const existingWatches = existingWatchersForWildcards.getValue(resolved); + let newWatches: Map | undefined; + if (!existingWatches) { + newWatches = createMap(); + existingWatchersForWildcards.setValue(resolved, newWatches); + } + updateWatchingWildcardDirectories(existingWatches || newWatches!, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { + return hostWithWatch.watchDirectory(dir, fileOrDirectory => { + const fileOrDirectoryPath = toPath(fileOrDirectory); + if (fileOrDirectoryPath !== toPath(dir) && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, cfg.options)) { + // writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`); + return; + } + + if (isOutputFile(fileOrDirectory, cfg)) { + // writeLog(`${fileOrDirectory} is output file`); + return; + } + + invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.Partial); }, !!(flags & WatchDirectoryFlags.Recursive)); }); } @@ -579,7 +590,7 @@ namespace ts { // Watch input files for (const input of cfg.fileNames) { hostWithWatch.watchFile(input, () => { - invalidateProjectAndScheduleBuilds(resolved); + invalidateProjectAndScheduleBuilds(resolved, ConfigFileProgramReloadLevel.None); }); } } @@ -587,9 +598,41 @@ namespace ts { } - function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) { + function isOutputFile(fileName: string, configFile: ParsedCommandLine) { + if (configFile.options.noEmit) return false; + + // ts or tsx files are not output + if (!fileExtensionIs(fileName, Extension.Dts) && + (fileExtensionIs(fileName, Extension.Ts) || fileExtensionIs(fileName, Extension.Tsx))) { + return false; + } + + // If options have --outFile or --out, check if its that + const out = configFile.options.outFile || configFile.options.out; + if (out && (isSameFile(fileName, out) || isSameFile(fileName, removeFileExtension(out) + Extension.Dts))) { + return true; + } + + // If declarationDir is specified, return if its a file in that directory + if (configFile.options.declarationDir && containsPath(configFile.options.declarationDir, fileName, currentDirectory, !host.useCaseSensitiveFileNames())) { + return true; + } + + // If --outDir, check if file is in that directory + if (configFile.options.outDir && containsPath(configFile.options.outDir, fileName, currentDirectory, !host.useCaseSensitiveFileNames())) { + return true; + } + + return !forEach(configFile.fileNames, inputFile => isSameFile(fileName, inputFile)); + } + + function isSameFile(file1: string, file2: string) { + return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + } + + function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { reportFileChangeDetected = true; - invalidateProject(resolved); + invalidateProject(resolved, reloadLevel); scheduleBuildInvalidatedProject(); } @@ -598,7 +641,7 @@ namespace ts { } function getUpToDateStatusOfFile(configFileName: ResolvedConfigFileName): UpToDateStatus { - return getUpToDateStatus(configFileCache.parseConfigFile(configFileName)); + return getUpToDateStatus(parseConfigFile(configFileName)); } function getBuildGraph(configFileNames: ReadonlyArray) { @@ -712,7 +755,7 @@ namespace ts { for (const ref of project.projectReferences) { usesPrepend = usesPrepend || !!(ref.prepend); const resolvedRef = resolveProjectReferencePath(host, ref); - const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); + const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef)); // An upstream project is blocked if (refStatus.type === UpToDateStatusType.Unbuildable) { @@ -789,7 +832,7 @@ namespace ts { }; } - function invalidateProject(configFileName: string) { + function invalidateProject(configFileName: string, reloadLevel?: ConfigFileProgramReloadLevel) { const resolved = resolveProjectName(configFileName); if (resolved === undefined) { // If this was a rootName, we need to track it as missing. @@ -800,13 +843,12 @@ namespace ts { return; } - configFileCache.removeKey(resolved); context.projectStatus.removeKey(resolved); if (context.options.watch) { context.diagnostics!.removeKey(resolved); } - context.invalidateProject(resolved, getGlobalDependencyGraph()); + context.invalidateProject(resolved, reloadLevel, getGlobalDependencyGraph()); } function scheduleBuildInvalidatedProject() { @@ -826,14 +868,16 @@ namespace ts { reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); } const buildProject = context.getNextInvalidatedProject(); - buildSomeProjects(p => p === buildProject); - if (context.hasPendingInvalidatedProjects()) { - if (!timerToBuildInvalidatedProject) { - scheduleBuildInvalidatedProject(); + if (buildProject) { + buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel); + if (context.hasPendingInvalidatedProjects()) { + if (!timerToBuildInvalidatedProject) { + scheduleBuildInvalidatedProject(); + } + } + else { + reportErrorSummary(); } - } - else { - reportErrorSummary(); } } @@ -845,29 +889,37 @@ namespace ts { } } - function buildSomeProjects(predicate: (projName: ResolvedConfigFileName) => boolean) { - const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(rootNames); - if (resolvedNames === undefined) return; + function buildSingleInvalidatedProject(project: ResolvedConfigFileName, reloadLevel: ConfigFileProgramReloadLevel) { + // TODO:: handle this in better way later - const graph = createDependencyGraph(resolvedNames)!; - for (const next of graph.buildQueue) { - if (!predicate(next)) continue; + const resolved = resolveProjectName(project); + if (!resolved) return; // ?? + const proj = parseConfigFile(resolved); + if (!proj) return; // ? + // TODO:: If full reload , update watch for wild cards + // TODO:: If full or partial reload, update watch for input files - const resolved = resolveProjectName(next); - if (!resolved) continue; // ?? - const proj = configFileCache.parseConfigFile(resolved); - if (!proj) continue; // ? - - const status = getUpToDateStatus(proj); - verboseReportProjectStatus(next, status); - - if (status.type === UpToDateStatusType.UpstreamBlocked) { - if (context.options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); - continue; + if (reloadLevel === ConfigFileProgramReloadLevel.Partial) { + // Update file names + const result = getFileNamesFromConfigSpecs(proj.configFileSpecs!, getDirectoryPath(project), proj.options, parseConfigFileHost); + if (result.fileNames.length !== 0) { + filterMutate(proj.errors, error => !isErrorNoInputFiles(error)); } - - buildSingleProject(next); + else if (!proj.configFileSpecs!.filesSpecs && !some(proj.errors, isErrorNoInputFiles)) { + proj.errors.push(getErrorForNoInputFiles(proj.configFileSpecs!, resolved)); + } + proj.fileNames = result.fileNames; } + + const status = getUpToDateStatus(proj); + verboseReportProjectStatus(project, status); + + if (status.type === UpToDateStatusType.UpstreamBlocked) { + if (context.options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); + return; + } + + buildSingleProject(project); } function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph | undefined { @@ -907,7 +959,7 @@ namespace ts { temporaryMarks[projPath] = true; circularityReportStack.push(projPath); - const parsed = configFileCache.parseConfigFile(projPath); + const parsed = parseConfigFile(projPath); if (parsed === undefined) { hadError = true; return; @@ -941,10 +993,11 @@ namespace ts { let resultFlags = BuildResultFlags.None; resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; - const configFile = configFileCache.parseConfigFile(proj); + const configFile = parseConfigFile(proj); if (!configFile) { // Failed to read the config file resultFlags |= BuildResultFlags.ConfigFileErrors; + host.reportDiagnostic(configFileCache.getValue(proj) as Diagnostic); storeErrorSummary(proj, 1); context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); return resultFlags; @@ -959,7 +1012,8 @@ namespace ts { projectReferences: configFile.projectReferences, host, rootNames: configFile.fileNames, - options: configFile.options + options: configFile.options, + configFileParsingDiagnostics: configFile.errors }; const program = createProgram(programOptions); @@ -1068,7 +1122,7 @@ namespace ts { const filesToDelete: string[] = []; for (const proj of graph.buildQueue) { - const parsed = configFileCache.parseConfigFile(proj); + const parsed = parseConfigFile(proj); if (parsed === undefined) { // File has gone missing; fine to ignore here continue; @@ -1155,7 +1209,7 @@ namespace ts { let anyFailed = false; for (const next of queue) { - const proj = configFileCache.parseConfigFile(next); + const proj = parseConfigFile(next); if (proj === undefined) { anyFailed = true; break; diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index daa1276e023..c9c57379ec2 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -26,8 +26,12 @@ namespace ts.tscWatch { type SubProjectFiles = [ReadonlyFile, ReadonlyFile] | [ReadonlyFile, ReadonlyFile, ReadonlyFile, ReadonlyFile]; const root = Harness.IO.getWorkspaceRoot(); + function projectPath(subProject: SubProject) { + return `${projectsLocation}/${project}/${subProject}`; + } + function projectFilePath(subProject: SubProject, baseFileName: string) { - return `${projectsLocation}/${project}/${subProject}/${baseFileName.toLowerCase()}`; + return `${projectPath(subProject)}/${baseFileName.toLowerCase()}`; } function projectFile(subProject: SubProject, baseFileName: string): File { @@ -92,7 +96,7 @@ namespace ts.tscWatch { createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`]); checkWatchedFiles(host, testProjectExpectedWatchedFiles); checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, emptyArray, /*recursive*/ true); // TODO: #26524 + checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true); checkOutputErrorsInitial(host, emptyArray); const outputFileStamps = getOutputFileStamps(host); for (const stamp of outputFileStamps) {