diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 98cbaca5e6f..e6f8dc01b50 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -797,10 +797,11 @@ namespace ts { } function getOptionNameMap(): OptionNameMap { - if (optionNameMapCache) { - return optionNameMapCache; - } + return optionNameMapCache || (optionNameMapCache = createOptionNameMap(optionDeclarations)); + } + /*@internal*/ + export function createOptionNameMap(optionDeclarations: ReadonlyArray): OptionNameMap { const optionNameMap = createMap(); const shortOptionNames = createMap(); forEach(optionDeclarations, option => { @@ -810,8 +811,7 @@ namespace ts { } }); - optionNameMapCache = { optionNameMap, shortOptionNames }; - return optionNameMapCache; + return { optionNameMap, shortOptionNames }; } /* @internal */ @@ -961,7 +961,12 @@ namespace ts { } /** @internal */ - export function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined { + export function getOptionFromName(optionName: string, allowShort?: boolean): CommandLineOption | undefined { + return getOptionDeclarationFromName(getOptionNameMap, optionName, allowShort); + } + + /*@internal*/ + export function getOptionDeclarationFromName(getOptionNameMap: () => OptionNameMap, optionName: string, allowShort = false): CommandLineOption | undefined { optionName = optionName.toLowerCase(); const { optionNameMap, shortOptionNames } = getOptionNameMap(); // Try to translate short option names to their full equivalents. diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 73be2b46b79..a6f216bd007 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2872,6 +2872,11 @@ "category": "Error", "code": 5071 }, + "Unknown build option '{0}'.": { + "category": "Error", + "code": 5072 + }, + "Generates a sourcemap for each corresponding '.d.ts' file.": { "category": "Message", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 482e977c9b6..c959c49b320 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -67,19 +67,24 @@ namespace ts { } export function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost { + return createCompilerHostWorker(options, setParentNodes); + } + /*@internal*/ + // TODO(shkamat): update this after reworking ts build API + export function createCompilerHostWorker(options: CompilerOptions, setParentNodes?: boolean, system = sys): CompilerHost { const existingDirectories = createMap(); function getCanonicalFileName(fileName: string): string { // if underlying system can distinguish between two files whose names differs only in cases then file name already in canonical form. // otherwise use toLowerCase as a canonical form. - return sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); + return system.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); } function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile | undefined { let text: string | undefined; try { performance.mark("beforeIORead"); - text = sys.readFile(fileName, options.charset); + text = system.readFile(fileName, options.charset); performance.mark("afterIORead"); performance.measure("I/O Read", "beforeIORead", "afterIORead"); } @@ -97,7 +102,7 @@ namespace ts { if (existingDirectories.has(directoryPath)) { return true; } - if (sys.directoryExists(directoryPath)) { + if (system.directoryExists(directoryPath)) { existingDirectories.set(directoryPath, true); return true; } @@ -108,7 +113,7 @@ namespace ts { if (directoryPath.length > getRootLength(directoryPath) && !directoryExists(directoryPath)) { const parentDirectory = getDirectoryPath(directoryPath); ensureDirectoriesExist(parentDirectory); - sys.createDirectory(directoryPath); + system.createDirectory(directoryPath); } } @@ -119,8 +124,8 @@ namespace ts { outputFingerprints = createMap(); } - const hash = sys.createHash!(data); // TODO: GH#18217 - const mtimeBefore = sys.getModifiedTime!(fileName); // TODO: GH#18217 + const hash = system.createHash!(data); // TODO: GH#18217 + const mtimeBefore = system.getModifiedTime!(fileName); // TODO: GH#18217 if (mtimeBefore) { const fingerprint = outputFingerprints.get(fileName); @@ -133,9 +138,9 @@ namespace ts { } } - sys.writeFile(fileName, data, writeByteOrderMark); + system.writeFile(fileName, data, writeByteOrderMark); - const mtimeAfter = sys.getModifiedTime!(fileName); // TODO: GH#18217 + const mtimeAfter = system.getModifiedTime!(fileName) || missingFileModifiedTime; // TODO: GH#18217 outputFingerprints.set(fileName, { hash, @@ -149,11 +154,11 @@ namespace ts { performance.mark("beforeIOWrite"); ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - if (isWatchSet(options) && sys.createHash && sys.getModifiedTime) { + if (isWatchSet(options) && system.createHash && system.getModifiedTime) { writeFileIfUpdated(fileName, data, writeByteOrderMark); } else { - sys.writeFile(fileName, data, writeByteOrderMark); + system.writeFile(fileName, data, writeByteOrderMark); } performance.mark("afterIOWrite"); @@ -167,32 +172,29 @@ namespace ts { } function getDefaultLibLocation(): string { - return getDirectoryPath(normalizePath(sys.getExecutingFilePath())); + return getDirectoryPath(normalizePath(system.getExecutingFilePath())); } - const newLine = getNewLineCharacter(options); - const realpath = sys.realpath && ((path: string) => sys.realpath!(path)); + const newLine = getNewLineCharacter(options, () => system.newLine); + const realpath = system.realpath && ((path: string) => system.realpath!(path)); return { getSourceFile, getDefaultLibLocation, getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), writeFile, - getCurrentDirectory: memoize(() => sys.getCurrentDirectory()), - useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, + getCurrentDirectory: memoize(() => system.getCurrentDirectory()), + useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, getCanonicalFileName, getNewLine: () => newLine, - fileExists: fileName => sys.fileExists(fileName), - readFile: fileName => sys.readFile(fileName), - trace: (s: string) => sys.write(s + newLine), - directoryExists: directoryName => sys.directoryExists(directoryName), - getEnvironmentVariable: name => sys.getEnvironmentVariable ? sys.getEnvironmentVariable(name) : "", - getDirectories: (path: string) => sys.getDirectories(path), + fileExists: fileName => system.fileExists(fileName), + readFile: fileName => system.readFile(fileName), + trace: (s: string) => system.write(s + newLine), + directoryExists: directoryName => system.directoryExists(directoryName), + getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", + getDirectories: (path: string) => system.getDirectories(path), realpath, - readDirectory: (path, extensions, include, exclude, depth) => sys.readDirectory(path, extensions, include, exclude, depth), - getModifiedTime: sys.getModifiedTime && (path => sys.getModifiedTime!(path)), - setModifiedTime: sys.setModifiedTime && ((path, date) => sys.setModifiedTime!(path, date)), - deleteFile: sys.deleteFile && (path => sys.deleteFile!(path)) + readDirectory: (path, extensions, include, exclude, depth) => system.readDirectory(path, extensions, include, exclude, depth) }; } diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 169da87c7db..32b10569fc6 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -34,9 +34,11 @@ namespace ts { * Map from config file name to up-to-date status */ projectStatus: FileMap; + diagnostics?: FileMap; // TODO(shkamat): this should be really be diagnostics but thats for later time - invalidatedProjects: FileMap; - queuedProjects: FileMap; + invalidateProject(project: ResolvedConfigFileName, dependencyGraph: DependencyGraph | undefined): void; + getNextInvalidatedProject(): ResolvedConfigFileName | undefined; + hasPendingInvalidatedProjects(): boolean; missingRoots: Map; } @@ -46,10 +48,14 @@ namespace ts { dependencyMap: Mapper; } - interface BuildOptions { - dry: boolean; - force: boolean; - verbose: boolean; + export interface BuildOptions { + dry?: boolean; + force?: boolean; + verbose?: boolean; + /*@internal*/ clean?: boolean; + /*@internal*/ watch?: boolean; + /*@internal*/ help?: boolean; + preserveWatchOutput?: boolean; } enum BuildResultFlags { @@ -189,6 +195,7 @@ namespace ts { hasKey(fileName: string): boolean; removeKey(fileName: string): void; getKeys(): string[]; + getSize(): number; } /** @@ -204,7 +211,8 @@ namespace ts { getValueOrUndefined, removeKey, getKeys, - hasKey + hasKey, + getSize }; function getKeys(): string[] { @@ -237,6 +245,10 @@ namespace ts { const f = normalizePath(fileName); return lookup.get(f); } + + function getSize() { + return lookup.size; + } } function createDependencyMapper() { @@ -370,165 +382,124 @@ namespace ts { } export function createBuildContext(options: BuildOptions): BuildContext { - const invalidatedProjects = createFileMap(); - const queuedProjects = createFileMap(); + const invalidatedProjectQueue = [] as ResolvedConfigFileName[]; + let nextIndex = 0; + const projectPendingBuild = createFileMap(); const missingRoots = createMap(); + const diagnostics = options.watch ? createFileMap() : undefined; return { options, projectStatus: createFileMap(), + diagnostics, unchangedOutputs: createFileMap(), - invalidatedProjects, - missingRoots, - queuedProjects + invalidateProject, + getNextInvalidatedProject, + hasPendingInvalidatedProjects, + missingRoots + }; + + function invalidateProject(proj: ResolvedConfigFileName, dependencyGraph: DependencyGraph | undefined) { + if (!projectPendingBuild.hasKey(proj)) { + addProjToQueue(proj); + if (dependencyGraph) { + queueBuildForDownstreamReferences(proj, dependencyGraph); + } + } + } + + function addProjToQueue(proj: ResolvedConfigFileName) { + Debug.assert(!projectPendingBuild.hasKey(proj)); + projectPendingBuild.setValue(proj, true); + invalidatedProjectQueue.push(proj); + } + + function getNextInvalidatedProject() { + if (nextIndex < invalidatedProjectQueue.length) { + const proj = invalidatedProjectQueue[nextIndex]; + nextIndex++; + projectPendingBuild.removeKey(proj); + if (!projectPendingBuild.getSize()) { + invalidatedProjectQueue.length = 0; + } + return proj; + } + } + + function hasPendingInvalidatedProjects() { + return !!projectPendingBuild.getSize(); + } + + // Mark all downstream projects of this one needing to be built "later" + function queueBuildForDownstreamReferences(root: ResolvedConfigFileName, dependencyGraph: DependencyGraph) { + const deps = dependencyGraph.dependencyMap.getReferencesTo(root); + for (const ref of deps) { + // Can skip circular references + if (!projectPendingBuild.hasKey(ref)) { + addProjToQueue(ref); + queueBuildForDownstreamReferences(ref, dependencyGraph); + } + } + } + } + + export interface SolutionBuilderHost extends CompilerHost { + getModifiedTime(fileName: string): Date | undefined; + setModifiedTime(fileName: string, date: Date): void; + deleteFile(fileName: string): void; + + reportDiagnostic: DiagnosticReporter; // Technically we want to move it out and allow steps of actions on Solution, but for now just merge stuff in build host here + reportSolutionBuilderStatus: DiagnosticReporter; + } + + export interface SolutionBuilderWithWatchHost extends SolutionBuilderHost, WatchHost { + } + + /** + * 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(new Date().toLocaleTimeString(), ForegroundColorEscapeSequences.Grey)}] ` : `${new Date().toLocaleTimeString()} - `; + output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine}`; + system.write(output); }; } - const buildOpts: CommandLineOption[] = [ - { - name: "verbose", - shortName: "v", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Enable_verbose_logging, - type: "boolean" - }, - { - name: "dry", - shortName: "d", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, - type: "boolean" - }, - { - name: "force", - shortName: "f", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, - type: "boolean" - }, - { - name: "clean", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Delete_the_outputs_of_all_projects, - type: "boolean" - }, - { - name: "watch", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Watch_input_files, - type: "boolean" - } - ]; + export function createSolutionBuilderHost(system = sys, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter) { + const host = createCompilerHostWorker({}, /*setParentNodes*/ undefined, system) as SolutionBuilderHost; + host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : () => undefined; + host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop; + host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop; + host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); + host.reportSolutionBuilderStatus = reportSolutionBuilderStatus || createBuilderStatusReporter(system); + return host; + } - export function performBuild(args: string[], compilerHost: CompilerHost, buildHost: BuildHost, system?: System): number | undefined { - let verbose = false; - let dry = false; - let force = false; - let clean = false; - let watch = false; - - const projects: string[] = []; - for (const arg of args) { - switch (arg.toLowerCase()) { - case "-v": - case "--verbose": - verbose = true; - continue; - case "-d": - case "--dry": - dry = true; - continue; - case "-f": - case "--force": - force = true; - continue; - case "--clean": - clean = true; - continue; - case "--watch": - case "-w": - watch = true; - continue; - - case "--?": - case "-?": - case "--help": - printHelp(buildOpts, "--build "); - return ExitStatus.Success; - } - // Not a flag, parse as filename - addProject(arg); - } - - // Nonsensical combinations - if (clean && force) { - buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force"); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - if (clean && verbose) { - buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose"); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - if (clean && watch) { - buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch"); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - if (watch && dry) { - buildHost.error(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry"); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - - if (projects.length === 0) { - // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." - addProject("."); - } - - const builder = createSolutionBuilder(compilerHost, buildHost, projects, { dry, force, verbose }, system); - if (clean) { - return builder.cleanAllProjects(); - } - - if (watch) { - builder.buildAllProjects(); - builder.startWatching(); - return undefined; - } - - return builder.buildAllProjects(); - - function addProject(projectSpecification: string) { - const fileName = resolvePath(compilerHost.getCurrentDirectory(), projectSpecification); - const refPath = resolveProjectReferencePath(compilerHost, { path: fileName }); - if (!compilerHost.fileExists(refPath)) { - return buildHost.error(Diagnostics.File_0_does_not_exist, fileName); - } - projects.push(refPath); - } + export function createSolutionBuilderWithWatchHost(system = sys, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter, reportWatchStatus?: WatchStatusReporter) { + const host = createSolutionBuilderHost(system, reportDiagnostic, reportSolutionBuilderStatus) as SolutionBuilderWithWatchHost; + const watchHost = createWatchHost(system, reportWatchStatus); + host.onWatchStatusChange = watchHost.onWatchStatusChange; + host.watchFile = watchHost.watchFile; + host.watchDirectory = watchHost.watchDirectory; + host.setTimeout = watchHost.setTimeout; + host.clearTimeout = watchHost.clearTimeout; + return host; } /** * A SolutionBuilder has an immutable set of rootNames that are the "entry point" projects, but * can dynamically add/remove other projects based on changes on the rootNames' references + * TODO: use SolutionBuilderWithWatchHost => watchedSolution + * use SolutionBuilderHost => Solution */ - export function createSolutionBuilder(compilerHost: CompilerHost, buildHost: BuildHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions, system?: System) { - if (!compilerHost.getModifiedTime || !compilerHost.setModifiedTime) { - throw new Error("Host must support timestamp APIs"); - } - - const configFileCache = createConfigFileCache(compilerHost); + export function createSolutionBuilder(host: SolutionBuilderHost, rootNames: ReadonlyArray, defaultOptions: BuildOptions) { + const hostWithWatch = host as SolutionBuilderWithWatchHost; + const configFileCache = createConfigFileCache(host); let context = createBuildContext(defaultOptions); + let timerToBuildInvalidatedProject: any; const existingWatchersForWildcards = createMap(); - - const upToDateHost: UpToDateHost = { - fileExists: fileName => compilerHost.fileExists(fileName), - getModifiedTime: fileName => compilerHost.getModifiedTime!(fileName), - getUnchangedTime: fileName => context.unchangedOutputs.getValueOrUndefined(fileName), - getLastStatus: fileName => context.projectStatus.getValueOrUndefined(fileName), - setLastStatus: (fileName, status) => context.projectStatus.setValue(fileName, status), - parseConfigFile: configFilePath => configFileCache.parseConfigFile(configFilePath) - }; - return { buildAllProjects, getUpToDateStatus, @@ -538,18 +509,36 @@ namespace ts { getBuildGraph, invalidateProject, - buildInvalidatedProjects, - buildDependentInvalidatedProjects, + buildInvalidatedProject, resolveProjectName, startWatching }; - function startWatching() { - if (!system) throw new Error("System host must be provided if using --watch"); - if (!system.watchFile || !system.watchDirectory || !system.setTimeout) throw new Error("System host must support watchFile / watchDirectory / setTimeout if using --watch"); + function reportStatus(message: DiagnosticMessage, ...args: string[]) { + host.reportSolutionBuilderStatus(createCompilerDiagnostic(message, ...args)); + } + function storeErrors(proj: ResolvedConfigFileName, diagnostics: ReadonlyArray) { + if (context.options.watch) { + storeErrorSummary(proj, diagnostics.filter(diagnostic => diagnostic.category === DiagnosticCategory.Error).length); + } + } + + function storeErrorSummary(proj: ResolvedConfigFileName, errorCount: number) { + if (context.options.watch) { + context.diagnostics!.setValue(proj, errorCount); + } + } + + function reportWatchStatus(message: DiagnosticMessage, ...args: (string | number | undefined)[]) { + if (hostWithWatch.onWatchStatusChange) { + hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), host.getNewLine(), { preserveWatchOutput: context.options.preserveWatchOutput }); + } + } + + function startWatching() { const graph = getGlobalDependencyGraph()!; if (!graph.buildQueue) { // Everything is broken - we don't even know what to watch. Give up. @@ -560,7 +549,7 @@ namespace ts { const cfg = configFileCache.parseConfigFile(resolved); if (cfg) { // Watch this file - system.watchFile(resolved, () => { + hostWithWatch.watchFile(resolved, () => { configFileCache.removeKey(resolved); invalidateProjectAndScheduleBuilds(resolved); }); @@ -568,7 +557,7 @@ namespace ts { // Update watchers for wildcard directories if (cfg.configFileSpecs) { updateWatchingWildcardDirectories(existingWatchersForWildcards, createMapFromTemplate(cfg.configFileSpecs.wildcardDirectories), (dir, flags) => { - return system.watchDirectory!(dir, () => { + return hostWithWatch.watchDirectory(dir, () => { invalidateProjectAndScheduleBuilds(resolved); }, !!(flags & WatchDirectoryFlags.Recursive)); }); @@ -576,18 +565,19 @@ namespace ts { // Watch input files for (const input of cfg.fileNames) { - system.watchFile(input, () => { + hostWithWatch.watchFile(input, () => { invalidateProjectAndScheduleBuilds(resolved); }); } } } - function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) { - invalidateProject(resolved); - system!.setTimeout!(buildInvalidatedProjects, 100); - system!.setTimeout!(buildDependentInvalidatedProjects, 3000); - } + } + + function invalidateProjectAndScheduleBuilds(resolved: ResolvedConfigFileName) { + reportWatchStatus(Diagnostics.File_change_detected_Starting_incremental_compilation); + invalidateProject(resolved); + scheduleBuildInvalidatedProject(); } function resetBuildContext(opts = defaultOptions) { @@ -610,7 +600,180 @@ namespace ts { } function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus { - return ts.getUpToDateStatus(upToDateHost, project); + if (project === undefined) { + return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; + } + + const prior = context.projectStatus.getValueOrUndefined(project.options.configFilePath!); + if (prior !== undefined) { + return prior; + } + + const actual = getUpToDateStatusWorker(project); + context.projectStatus.setValue(project.options.configFilePath!, actual); + return actual; + } + + function getUpToDateStatusWorker(project: ParsedCommandLine): UpToDateStatus { + let newestInputFileName: string = undefined!; + let newestInputFileTime = minimumDate; + // Get timestamps of input files + for (const inputFile of project.fileNames) { + if (!host.fileExists(inputFile)) { + return { + type: UpToDateStatusType.Unbuildable, + reason: `${inputFile} does not exist` + }; + } + + const inputTime = host.getModifiedTime(inputFile) || missingFileModifiedTime; + if (inputTime > newestInputFileTime) { + newestInputFileName = inputFile; + newestInputFileTime = inputTime; + } + } + + // Collect the expected outputs of this project + const outputs = getAllProjectOutputs(project); + + if (outputs.length === 0) { + return { + type: UpToDateStatusType.ContainerOnly + }; + } + + // Now see if all outputs are newer than the newest input + let oldestOutputFileName = "(none)"; + let oldestOutputFileTime = maximumDate; + let newestOutputFileName = "(none)"; + let newestOutputFileTime = minimumDate; + let missingOutputFileName: string | undefined; + let newestDeclarationFileContentChangedTime = minimumDate; + let isOutOfDateWithInputs = false; + for (const output of outputs) { + // Output is missing; can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (!host.fileExists(output)) { + missingOutputFileName = output; + break; + } + + const outputTime = host.getModifiedTime(output) || missingFileModifiedTime; + if (outputTime < oldestOutputFileTime) { + oldestOutputFileTime = outputTime; + oldestOutputFileName = output; + } + + // If an output is older than the newest input, we can stop checking + // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status + if (outputTime < newestInputFileTime) { + isOutOfDateWithInputs = true; + break; + } + + if (outputTime > newestOutputFileTime) { + newestOutputFileTime = outputTime; + newestOutputFileName = output; + } + + // Keep track of when the most recent time a .d.ts file was changed. + // In addition to file timestamps, we also keep track of when a .d.ts file + // had its file touched but not had its contents changed - this allows us + // to skip a downstream typecheck + if (isDeclarationFile(output)) { + const unchangedTime = context.unchangedOutputs.getValueOrUndefined(output); + if (unchangedTime !== undefined) { + newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); + } + else { + const outputModifiedTime = host.getModifiedTime(output) || missingFileModifiedTime; + newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, outputModifiedTime); + } + } + } + + let pseudoUpToDate = false; + let usesPrepend = false; + let upstreamChangedProject: string | undefined; + if (project.projectReferences) { + for (const ref of project.projectReferences) { + usesPrepend = usesPrepend || !!(ref.prepend); + const resolvedRef = resolveProjectReferencePath(host, ref); + const refStatus = getUpToDateStatus(configFileCache.parseConfigFile(resolvedRef)); + + // An upstream project is blocked + if (refStatus.type === UpToDateStatusType.Unbuildable) { + return { + type: UpToDateStatusType.UpstreamBlocked, + upstreamProjectName: ref.path + }; + } + + // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) + if (refStatus.type !== UpToDateStatusType.UpToDate) { + return { + type: UpToDateStatusType.UpstreamOutOfDate, + upstreamProjectName: ref.path + }; + } + + // 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 + }; + } + + if (usesPrepend && pseudoUpToDate) { + return { + type: UpToDateStatusType.OutOfDateWithUpstream, + outOfDateOutputFileName: oldestOutputFileName, + newerProjectName: upstreamChangedProject! + }; + } + + // Up to date + return { + type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, + newestDeclarationFileContentChangedTime, + newestInputFileTime, + newestOutputFileTime, + newestInputFileName, + newestOutputFileName, + oldestOutputFileName + }; } function invalidateProject(configFileName: string) { @@ -625,33 +788,44 @@ namespace ts { } configFileCache.removeKey(resolved); - context.invalidatedProjects.setValue(resolved, true); context.projectStatus.removeKey(resolved); - - const graph = getGlobalDependencyGraph()!; - if (graph) { - queueBuildForDownstreamReferences(resolved); + if (context.options.watch) { + context.diagnostics!.removeKey(resolved); } - // Mark all downstream projects of this one needing to be built "later" - function queueBuildForDownstreamReferences(root: ResolvedConfigFileName) { - const deps = graph.dependencyMap.getReferencesTo(root); - for (const ref of deps) { - // Can skip circular references - if (!context.queuedProjects.hasKey(ref)) { - context.queuedProjects.setValue(ref, true); - queueBuildForDownstreamReferences(ref); - } + context.invalidateProject(resolved, getGlobalDependencyGraph()); + } + + function scheduleBuildInvalidatedProject() { + if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) { + return; + } + if (timerToBuildInvalidatedProject) { + hostWithWatch.clearTimeout(timerToBuildInvalidatedProject); + } + timerToBuildInvalidatedProject = hostWithWatch.setTimeout(buildInvalidatedProject, 250); + } + + function buildInvalidatedProject() { + timerToBuildInvalidatedProject = undefined; + const buildProject = context.getNextInvalidatedProject(); + buildSomeProjects(p => p === buildProject); + if (context.hasPendingInvalidatedProjects()) { + if (!timerToBuildInvalidatedProject) { + scheduleBuildInvalidatedProject(); } } + else { + reportErrorSummary(); + } } - function buildInvalidatedProjects() { - buildSomeProjects(p => context.invalidatedProjects.hasKey(p)); - } - - function buildDependentInvalidatedProjects() { - buildSomeProjects(p => context.queuedProjects.hasKey(p)); + function reportErrorSummary() { + if (context.options.watch) { + let errorCount = 0; + context.diagnostics!.getKeys().forEach(resolved => errorCount += context.diagnostics!.getValue(resolved)); + reportWatchStatus(errorCount === 1 ? Diagnostics.Found_1_error_Watching_for_file_changes : Diagnostics.Found_0_errors_Watching_for_file_changes, errorCount); + } } function buildSomeProjects(predicate: (projName: ResolvedConfigFileName) => boolean) { @@ -671,7 +845,7 @@ namespace ts { verboseReportProjectStatus(next, status); if (status.type === UpToDateStatusType.UpstreamBlocked) { - if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); + if (context.options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, resolved, status.upstreamProjectName); continue; } @@ -708,7 +882,8 @@ namespace ts { if (temporaryMarks[projPath]) { if (!inCircularContext) { hadError = true; - buildHost.error(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); + // TODO(shkamat): Account for this error + reportStatus(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")); return; } } @@ -740,11 +915,11 @@ namespace ts { function buildSingleProject(proj: ResolvedConfigFileName): BuildResultFlags { if (context.options.dry) { - buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj); + reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj); return BuildResultFlags.Success; } - if (context.options.verbose) buildHost.verbose(Diagnostics.Building_project_0, proj); + if (context.options.verbose) reportStatus(Diagnostics.Building_project_0, proj); let resultFlags = BuildResultFlags.None; resultFlags |= BuildResultFlags.DeclarationOutputUnchanged; @@ -753,6 +928,7 @@ namespace ts { if (!configFile) { // Failed to read the config file resultFlags |= BuildResultFlags.ConfigFileErrors; + storeErrorSummary(proj, 1); context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" }); return resultFlags; } @@ -764,7 +940,7 @@ namespace ts { const programOptions: CreateProgramOptions = { projectReferences: configFile.projectReferences, - host: compilerHost, + host, rootNames: configFile.fileNames, options: configFile.options }; @@ -778,8 +954,9 @@ namespace ts { if (syntaxDiagnostics.length) { resultFlags |= BuildResultFlags.SyntaxErrors; for (const diag of syntaxDiagnostics) { - buildHost.errorDiagnostic(diag); + host.reportDiagnostic(diag); } + storeErrors(proj, syntaxDiagnostics); context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Syntactic errors" }); return resultFlags; } @@ -790,8 +967,9 @@ namespace ts { if (declDiagnostics.length) { resultFlags |= BuildResultFlags.DeclarationEmitErrors; for (const diag of declDiagnostics) { - buildHost.errorDiagnostic(diag); + host.reportDiagnostic(diag); } + storeErrors(proj, declDiagnostics); context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Declaration file errors" }); return resultFlags; } @@ -802,8 +980,9 @@ namespace ts { if (semanticDiagnostics.length) { resultFlags |= BuildResultFlags.TypeErrors; for (const diag of semanticDiagnostics) { - buildHost.errorDiagnostic(diag); + host.reportDiagnostic(diag); } + storeErrors(proj, semanticDiagnostics); context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Semantic errors" }); return resultFlags; } @@ -813,18 +992,18 @@ namespace ts { program.emit(/*targetSourceFile*/ undefined, (fileName, content, writeBom, onError) => { let priorChangeTime: Date | undefined; - if (!anyDtsChanged && isDeclarationFile(fileName) && compilerHost.fileExists(fileName)) { - if (compilerHost.readFile(fileName) === content) { + if (!anyDtsChanged && isDeclarationFile(fileName) && host.fileExists(fileName)) { + if (host.readFile(fileName) === content) { // Check for unchanged .d.ts files resultFlags &= ~BuildResultFlags.DeclarationOutputUnchanged; - priorChangeTime = compilerHost.getModifiedTime && compilerHost.getModifiedTime(fileName); + priorChangeTime = host.getModifiedTime(fileName); } else { anyDtsChanged = true; } } - compilerHost.writeFile(fileName, content, writeBom, onError, emptyArray); + host.writeFile(fileName, content, writeBom, onError, emptyArray); if (priorChangeTime !== undefined) { newestDeclarationFileContentChangedTime = newer(priorChangeTime, newestDeclarationFileContentChangedTime); context.unchangedOutputs.setValue(fileName, priorChangeTime); @@ -841,18 +1020,22 @@ namespace ts { function updateOutputTimestamps(proj: ParsedCommandLine) { if (context.options.dry) { - return buildHost.message(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!); + return reportStatus(Diagnostics.A_non_dry_build_would_build_project_0, proj.options.configFilePath!); + } + + if (context.options.verbose) { + reportStatus(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!); } - if (context.options.verbose) buildHost.verbose(Diagnostics.Updating_output_timestamps_of_project_0, proj.options.configFilePath!); const now = new Date(); const outputs = getAllProjectOutputs(proj); let priorNewestUpdateTime = minimumDate; for (const file of outputs) { if (isDeclarationFile(file)) { - priorNewestUpdateTime = newer(priorNewestUpdateTime, compilerHost.getModifiedTime!(file)); + priorNewestUpdateTime = newer(priorNewestUpdateTime, host.getModifiedTime(file) || missingFileModifiedTime); } - compilerHost.setModifiedTime!(file, now); + + host.setModifiedTime(file, now); } context.projectStatus.setValue(proj.options.configFilePath!, { type: UpToDateStatusType.UpToDate, newestDeclarationFileContentChangedTime: priorNewestUpdateTime } as UpToDateStatus); @@ -875,7 +1058,7 @@ namespace ts { } const outputs = getAllProjectOutputs(parsed); for (const output of outputs) { - if (compilerHost.fileExists(output)) { + if (host.fileExists(output)) { filesToDelete.push(output); } } @@ -894,43 +1077,39 @@ namespace ts { function cleanAllProjects() { const resolvedNames: ReadonlyArray | undefined = getAllProjectsInScope(); if (resolvedNames === undefined) { - buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); + reportStatus(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); return ExitStatus.DiagnosticsPresent_OutputsSkipped; } const filesToDelete = getFilesToClean(resolvedNames); if (filesToDelete === undefined) { - buildHost.message(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); + reportStatus(Diagnostics.Skipping_clean_because_not_all_projects_could_be_located); return ExitStatus.DiagnosticsPresent_OutputsSkipped; } if (context.options.dry) { - buildHost.message(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); + reportStatus(Diagnostics.A_non_dry_build_would_delete_the_following_files_Colon_0, filesToDelete.map(f => `\r\n * ${f}`).join("")); return ExitStatus.Success; } - // Do this check later to allow --clean --dry to function even if the host can't delete files - if (!compilerHost.deleteFile) { - throw new Error("Host does not support deleting files"); - } - for (const output of filesToDelete) { - compilerHost.deleteFile(output); + host.deleteFile(output); } return ExitStatus.Success; } function resolveProjectName(name: string): ResolvedConfigFileName | undefined { - const fullPath = resolvePath(compilerHost.getCurrentDirectory(), name); - if (compilerHost.fileExists(fullPath)) { + const fullPath = resolvePath(host.getCurrentDirectory(), name); + if (host.fileExists(fullPath)) { return fullPath as ResolvedConfigFileName; } const fullPathWithTsconfig = combinePaths(fullPath, "tsconfig.json"); - if (compilerHost.fileExists(fullPathWithTsconfig)) { + if (host.fileExists(fullPathWithTsconfig)) { return fullPathWithTsconfig as ResolvedConfigFileName; } - buildHost.error(Diagnostics.File_0_not_found, relName(fullPath)); + // TODO(shkamat): right now this is accounted as 1 error in config file, but we need to do better + host.reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, relName(fullPath))); return undefined; } @@ -947,8 +1126,12 @@ namespace ts { } function buildAllProjects(): ExitStatus { + if (context.options.watch) { reportWatchStatus(Diagnostics.Starting_compilation_in_watch_mode); } const graph = getGlobalDependencyGraph(); - if (graph === undefined) return ExitStatus.DiagnosticsPresent_OutputsSkipped; + if (graph === undefined) { + reportErrorSummary(); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } const queue = graph.buildQueue; reportBuildQueue(graph); @@ -968,7 +1151,7 @@ namespace ts { // Up to date, skip if (defaultOptions.dry) { // In a dry build, inform the user of this fact - buildHost.message(Diagnostics.Project_0_is_up_to_date, projName); + reportStatus(Diagnostics.Project_0_is_up_to_date, projName); } continue; } @@ -980,7 +1163,7 @@ namespace ts { } if (status.type === UpToDateStatusType.UpstreamBlocked) { - if (context.options.verbose) buildHost.verbose(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); + if (context.options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName); continue; } @@ -992,6 +1175,7 @@ namespace ts { const buildResult = buildSingleProject(next); anyFailed = anyFailed || !!(buildResult & BuildResultFlags.AnyErrors); } + reportErrorSummary(); return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success; } @@ -1005,15 +1189,11 @@ namespace ts { for (const name of graph.buildQueue) { names.push(name); } - if (context.options.verbose) buildHost.verbose(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); + if (context.options.verbose) reportStatus(Diagnostics.Projects_in_this_build_Colon_0, names.map(s => "\r\n * " + relName(s)).join("")); } function relName(path: string): string { - return convertToRelativePath(path, compilerHost.getCurrentDirectory(), f => compilerHost.getCanonicalFileName(f)); - } - - function reportVerbose(message: DiagnosticMessage, ...args: string[]) { - buildHost.verbose(message, ...args); + return convertToRelativePath(path, host.getCurrentDirectory(), f => host.getCanonicalFileName(f)); } /** @@ -1021,192 +1201,10 @@ namespace ts { */ function verboseReportProjectStatus(configFileName: string, status: UpToDateStatus) { if (!context.options.verbose) return; - return formatUpToDateStatus(configFileName, status, relName, reportVerbose); + return formatUpToDateStatus(configFileName, status, relName, reportStatus); } } - /** - * Gets the UpToDateStatus for a project - */ - export function getUpToDateStatus(host: UpToDateHost, project: ParsedCommandLine | undefined): UpToDateStatus { - if (project === undefined) { - return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" }; - } - - const prior = host.getLastStatus ? host.getLastStatus(project.options.configFilePath!) : undefined; - if (prior !== undefined) { - return prior; - } - - const actual = getUpToDateStatusWorker(host, project); - if (host.setLastStatus) { - host.setLastStatus(project.options.configFilePath!, actual); - } - - return actual; - } - - function getUpToDateStatusWorker(host: UpToDateHost, project: ParsedCommandLine): UpToDateStatus { - let newestInputFileName: string = undefined!; - let newestInputFileTime = minimumDate; - // Get timestamps of input files - for (const inputFile of project.fileNames) { - if (!host.fileExists(inputFile)) { - return { - type: UpToDateStatusType.Unbuildable, - reason: `${inputFile} does not exist` - }; - } - - const inputTime = host.getModifiedTime(inputFile); - if (inputTime > newestInputFileTime) { - newestInputFileName = inputFile; - newestInputFileTime = inputTime; - } - } - - // Collect the expected outputs of this project - const outputs = getAllProjectOutputs(project); - - if (outputs.length === 0) { - return { - type: UpToDateStatusType.ContainerOnly - }; - } - - // Now see if all outputs are newer than the newest input - let oldestOutputFileName = "(none)"; - let oldestOutputFileTime = maximumDate; - let newestOutputFileName = "(none)"; - let newestOutputFileTime = minimumDate; - let missingOutputFileName: string | undefined; - let newestDeclarationFileContentChangedTime = minimumDate; - let isOutOfDateWithInputs = false; - for (const output of outputs) { - // Output is missing; can stop checking - // Don't immediately return because we can still be upstream-blocked, which is a higher-priority status - if (!host.fileExists(output)) { - missingOutputFileName = output; - break; - } - - const outputTime = host.getModifiedTime(output); - 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 unchangedTime = host.getUnchangedTime ? host.getUnchangedTime(output) : undefined; - if (unchangedTime !== undefined) { - newestDeclarationFileContentChangedTime = newer(unchangedTime, newestDeclarationFileContentChangedTime); - } - else { - newestDeclarationFileContentChangedTime = newer(newestDeclarationFileContentChangedTime, host.getModifiedTime(output)); - } - } - } - - let pseudoUpToDate = false; - let usesPrepend = false; - let upstreamChangedProject: string | undefined; - if (project.projectReferences && host.parseConfigFile) { - for (const ref of project.projectReferences) { - usesPrepend = usesPrepend || !!(ref.prepend); - const resolvedRef = resolveProjectReferencePath(host, ref); - const refStatus = getUpToDateStatus(host, host.parseConfigFile(resolvedRef)); - - // An upstream project is blocked - if (refStatus.type === UpToDateStatusType.Unbuildable) { - return { - type: UpToDateStatusType.UpstreamBlocked, - upstreamProjectName: ref.path - }; - } - - // If the upstream project is out of date, then so are we (someone shouldn't have asked, though?) - if (refStatus.type !== UpToDateStatusType.UpToDate) { - return { - type: UpToDateStatusType.UpstreamOutOfDate, - upstreamProjectName: ref.path - }; - } - - // 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 - }; - } - - if (usesPrepend && pseudoUpToDate) { - return { - type: UpToDateStatusType.OutOfDateWithUpstream, - outOfDateOutputFileName: oldestOutputFileName, - newerProjectName: upstreamChangedProject! - }; - } - - // Up to date - return { - type: pseudoUpToDate ? UpToDateStatusType.UpToDateWithUpstreamTypes : UpToDateStatusType.UpToDate, - newestDeclarationFileContentChangedTime, - newestInputFileTime, - newestOutputFileTime, - newestInputFileName, - newestOutputFileName, - oldestOutputFileName - }; - } - export function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray { if (project.options.outFile) { return getOutFileOutputs(project); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4b79f4f05b9..a487a010da9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4681,16 +4681,6 @@ namespace ts { verticalTab = 0x0B, // \v } - /* @internal */ - export interface UpToDateHost { - fileExists(fileName: string): boolean; - getModifiedTime(fileName: string): Date; - getUnchangedTime?(fileName: string): Date | undefined; - getLastStatus?(fileName: string): UpToDateStatus | undefined; - setLastStatus?(fileName: string, status: UpToDateStatus): void; - parseConfigFile?(configFilePath: ResolvedConfigFileName): ParsedCommandLine | undefined; - } - export interface ModuleResolutionHost { // TODO: GH#18217 Optional methods frequently used as non-optional @@ -4821,10 +4811,6 @@ namespace ts { /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; createHash?(data: string): string; - - getModifiedTime?(fileName: string): Date; - setModifiedTime?(fileName: string, date: Date): void; - deleteFile?(fileName: string): void; } /* @internal */ diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 4533d335861..0ada3922187 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -27,12 +27,6 @@ namespace ts { }; } - /** @internal */ - export const nonClearingMessageCodes: number[] = [ - Diagnostics.Found_1_error_Watching_for_file_changes.code, - Diagnostics.Found_0_errors_Watching_for_file_changes.code - ]; - /** * @returns Whether the screen was cleared. */ @@ -41,7 +35,7 @@ namespace ts { !options.preserveWatchOutput && !options.extendedDiagnostics && !options.diagnostics && - !contains(nonClearingMessageCodes, diagnostic.code)) { + contains(screenStartingMessageCodes, diagnostic.code)) { system.clearScreen(); return true; } @@ -174,6 +168,17 @@ namespace ts { const noopFileWatcher: FileWatcher = { close: noop }; + export function createWatchHost(system = sys, reportWatchStatus?: WatchStatusReporter): WatchHost { + const onWatchStatusChange = reportWatchStatus || createWatchStatusReporter(system); + return { + onWatchStatusChange, + watchFile: system.watchFile ? ((path, callback, pollingInterval) => system.watchFile!(path, callback, pollingInterval)) : () => noopFileWatcher, + watchDirectory: system.watchDirectory ? ((path, callback, recursive) => system.watchDirectory!(path, callback, recursive)) : () => noopFileWatcher, + setTimeout: system.setTimeout ? ((callback, ms, ...args: any[]) => system.setTimeout!.call(system, callback, ms, ...args)) : noop, + clearTimeout: system.clearTimeout ? (timeoutId => system.clearTimeout!(timeoutId)) : noop + }; + } + /** * Creates the watch compiler host that can be extended with config file or root file names and options host */ @@ -185,7 +190,7 @@ namespace ts { let host: DirectoryStructureHost = system; const useCaseSensitiveFileNames = () => system.useCaseSensitiveFileNames; const writeFileName = (s: string) => system.write(s + system.newLine); - const onWatchStatusChange = reportWatchStatus || createWatchStatusReporter(system); + const { onWatchStatusChange, watchFile, watchDirectory, setTimeout, clearTimeout } = createWatchHost(system, reportWatchStatus); return { useCaseSensitiveFileNames, getNewLine: () => system.newLine, @@ -199,10 +204,10 @@ namespace ts { readDirectory: (path, extensions, exclude, include, depth) => system.readDirectory(path, extensions, exclude, include, depth), realpath: system.realpath && (path => system.realpath!(path)), getEnvironmentVariable: system.getEnvironmentVariable && (name => system.getEnvironmentVariable(name)), - watchFile: system.watchFile ? ((path, callback, pollingInterval) => system.watchFile!(path, callback, pollingInterval)) : () => noopFileWatcher, - watchDirectory: system.watchDirectory ? ((path, callback, recursive) => system.watchDirectory!(path, callback, recursive)) : () => noopFileWatcher, - setTimeout: system.setTimeout ? ((callback, ms, ...args: any[]) => system.setTimeout!.call(system, callback, ms, ...args)) : noop, - clearTimeout: system.clearTimeout ? (timeoutId => system.clearTimeout!(timeoutId)) : noop, + watchFile, + watchDirectory, + setTimeout, + clearTimeout, trace: s => system.write(s), onWatchStatusChange, createDirectory: path => system.createDirectory(path), @@ -223,10 +228,10 @@ namespace ts { const reportSummary = (errorCount: number) => { if (errorCount === 1) { - onWatchStatusChange(createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes, errorCount), newLine, compilerOptions); + onWatchStatusChange!(createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes, errorCount), newLine, compilerOptions); } else { - onWatchStatusChange(createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errorCount, errorCount), newLine, compilerOptions); + onWatchStatusChange!(createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errorCount, errorCount), newLine, compilerOptions); } }; @@ -269,7 +274,21 @@ namespace ts { export type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void; /** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */ export type CreateProgram = (rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray) => T; - export interface WatchCompilerHost { + /** Host that has watch functionality used in --watch mode */ + export interface WatchHost { + /** If provided, called with Diagnostic message that informs about change in watch status */ + onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions): void; + + /** Used to watch changes in source files, missing files needed to update the program or config file */ + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ + setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + /** If provided, will be used to reset existing delayed compilation */ + clearTimeout?(timeoutId: any): void; + } + export interface WatchCompilerHost extends WatchHost { // TODO: GH#18217 Optional methods are frequently asserted /** @@ -278,8 +297,6 @@ namespace ts { createProgram: CreateProgram; /** If provided, callback to invoke after every new program creation */ afterProgramCreate?(program: T): void; - /** If provided, called with Diagnostic message that informs about change in watch status */ - onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions): void; // Only for testing /*@internal*/ @@ -322,15 +339,6 @@ namespace ts { resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; /** If provided, used to resolve type reference directives, otherwise typescript's default resolution */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - - /** Used to watch changes in source files, missing files needed to update the program or config file */ - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; - /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; - /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ - setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - /** If provided, will be used to reset existing delayed compilation */ - clearTimeout?(timeoutId: any): void; } /** Internal interface used to wire emit through same host */ diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 1bb358698a2..bd9d62f90e5 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -374,5 +374,41 @@ namespace fakes { return parsed; } } + + export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost { + diagnostics: ts.Diagnostic[] = []; + + reportDiagnostic(diagnostic: ts.Diagnostic) { + this.diagnostics.push(diagnostic); + } + + reportSolutionBuilderStatus(diagnostic: ts.Diagnostic) { + this.diagnostics.push(diagnostic); + } + + clearDiagnostics() { + this.diagnostics.length = 0; + } + + assertDiagnosticMessages(...expected: ts.DiagnosticMessage[]) { + const actual = this.diagnostics.slice(); + if (actual.length !== expected.length) { + assert.fail(actual, expected, `Diagnostic arrays did not match - got\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\nexpected\r\n${expected.map(e => " " + e.message).join("\r\n")}`); + } + for (let i = 0; i < actual.length; i++) { + if (actual[i].code !== expected[i].code) { + assert.fail(actual[i].messageText, expected[i].message, `Mismatched error code - expected diagnostic ${i} "${actual[i].messageText}" to match ${expected[i].message}`); + } + } + } + + printDiagnostics(header = "== Diagnostics ==") { + const out = ts.createDiagnosticReporter(ts.sys); + ts.sys.write(header + "\r\n"); + for (const d of this.diagnostics) { + out(d); + } + } + } } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 572c59bb305..f84befe2359 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -617,14 +617,14 @@ interface Array {}` } } - removeFile(filePath: string) { + deleteFile(filePath: string) { const path = this.toFullPath(filePath); const currentEntry = this.fs.get(path) as FsFile; Debug.assert(isFsFile(currentEntry)); this.removeFileOrFolder(currentEntry, returnFalse); } - removeFolder(folderPath: string, recursive?: boolean) { + deleteFolder(folderPath: string, recursive?: boolean) { const path = this.toFullPath(folderPath); const currentEntry = this.fs.get(path) as FsFolder; Debug.assert(isFsFolder(currentEntry)); @@ -632,7 +632,7 @@ interface Array {}` const subEntries = currentEntry.entries.slice(); subEntries.forEach(fsEntry => { if (isFsFolder(fsEntry)) { - this.removeFolder(fsEntry.fullPath, recursive); + this.deleteFolder(fsEntry.fullPath, recursive); } else { this.removeFileOrFolder(fsEntry, returnFalse); @@ -763,6 +763,14 @@ interface Array {}` return (fsEntry && fsEntry.modifiedTime)!; // TODO: GH#18217 } + setModifiedTime(s: string, date: Date) { + const path = this.toFullPath(s); + const fsEntry = this.fs.get(path); + if (fsEntry) { + fsEntry.modifiedTime = date; + } + } + readFile(s: string): string | undefined { const fsEntry = this.getRealFile(this.toFullPath(s)); return fsEntry ? fsEntry.content : undefined; diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index b02f1a6c175..9c51b4308b9 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -79,6 +79,7 @@ "unittests/transform.ts", "unittests/transpile.ts", "unittests/tsbuild.ts", + "unittests/tsbuildWatchMode.ts", "unittests/tsconfigParsing.ts", "unittests/tscWatchMode.ts", "unittests/versionCache.ts", diff --git a/src/testRunner/unittests/tsbuild.ts b/src/testRunner/unittests/tsbuild.ts index 5cb546f8abb..d3f00280c74 100644 --- a/src/testRunner/unittests/tsbuild.ts +++ b/src/testRunner/unittests/tsbuild.ts @@ -1,15 +1,5 @@ namespace ts { let currentTime = 100; - let lastDiagnostics: Diagnostic[] = []; - const reportDiagnostic: DiagnosticReporter = diagnostic => lastDiagnostics.push(diagnostic); - const report = (message: DiagnosticMessage, ...args: string[]) => reportDiagnostic(createCompilerDiagnostic(message, ...args)); - const buildHost: BuildHost = { - error: report, - verbose: report, - message: report, - errorDiagnostic: d => reportDiagnostic(d) - }; - export namespace Sample1 { tick(); const projFs = loadProjectFromDisk("tests/projects/sample1"); @@ -21,12 +11,12 @@ namespace ts { describe("tsbuild - sanity check of clean build of 'sample1' project", () => { it("can build the sample project 'sample1' without error", () => { const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - clearDiagnostics(); + host.clearDiagnostics(); builder.buildAllProjects(); - assertDiagnosticMessages(/*empty*/); + host.assertDiagnosticMessages(/*empty*/); // Check for outputs. Not an exhaustive list for (const output of allExpectedOutputs) { @@ -37,12 +27,11 @@ namespace ts { describe("tsbuild - dry builds", () => { it("doesn't write any files in a dry build", () => { - clearDiagnostics(); const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); + host.assertDiagnosticMessages(Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0, Diagnostics.A_non_dry_build_would_build_project_0); // Check for outputs to not be written. Not an exhaustive list for (const output of allExpectedOutputs) { @@ -51,28 +40,26 @@ namespace ts { }); it("indicates that it would skip builds during a dry build", () => { - clearDiagnostics(); const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); + const host = new fakes.SolutionBuilderHost(fs); - let builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + let builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); tick(); - clearDiagnostics(); - builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: true, force: false, verbose: false }); + host.clearDiagnostics(); + builder = createSolutionBuilder(host, ["/src/tests"], { dry: true, force: false, verbose: false }); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); + host.assertDiagnosticMessages(Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date, Diagnostics.Project_0_is_up_to_date); }); }); describe("tsbuild - clean builds", () => { it("removes all files it built", () => { - clearDiagnostics(); const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); + const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); // Verify they exist for (const output of allExpectedOutputs) { @@ -91,9 +78,9 @@ namespace ts { describe("tsbuild - force builds", () => { it("always builds under --force", () => { const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); + const host = new fakes.SolutionBuilderHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: true, verbose: false }); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: true, verbose: false }); builder.buildAllProjects(); let currentTime = time(); checkOutputTimestamps(currentTime); @@ -116,14 +103,14 @@ namespace ts { describe("tsbuild - can detect when and what to rebuild", () => { const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); it("Builds the project", () => { - clearDiagnostics(); + host.clearDiagnostics(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + host.assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, @@ -135,10 +122,10 @@ namespace ts { // All three projects are up to date it("Detects that all projects are up to date", () => { - clearDiagnostics(); + host.clearDiagnostics(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + host.assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2); @@ -147,12 +134,12 @@ namespace ts { // Update a file in the leaf node (tests), only it should rebuild the last one it("Only builds the leaf node project", () => { - clearDiagnostics(); + host.clearDiagnostics(); fs.writeFileSync("/src/tests/index.ts", "const m = 10;"); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + host.assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, @@ -162,12 +149,12 @@ namespace ts { // Update a file in the parent (without affecting types), should get fast downstream builds it("Detects type-only changes in upstream projects", () => { - clearDiagnostics(); + host.clearDiagnostics(); replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, + host.assertDiagnosticMessages(Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, Diagnostics.Building_project_0, Diagnostics.Project_0_is_up_to_date_with_d_ts_files_from_its_dependencies, @@ -180,15 +167,13 @@ namespace ts { describe("tsbuild - downstream-blocked compilations", () => { it("won't build downstream projects if upstream projects have errors", () => { const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: true }); - - clearDiagnostics(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: true }); // Induce an error in the middle project replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`); builder.buildAllProjects(); - assertDiagnosticMessages( + host.assertDiagnosticMessages( Diagnostics.Projects_in_this_build_Colon_0, Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, Diagnostics.Building_project_0, @@ -204,12 +189,11 @@ namespace ts { describe("tsbuild - project invalidation", () => { it("invalidates projects correctly", () => { const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/tests"], { dry: false, force: false, verbose: false }); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - clearDiagnostics(); builder.buildAllProjects(); - assertDiagnosticMessages(/*empty*/); + host.assertDiagnosticMessages(/*empty*/); // Update a timestamp in the middle project tick(); @@ -221,14 +205,14 @@ namespace ts { // Rebuild this project tick(); builder.invalidateProject("/src/logic"); - builder.buildInvalidatedProjects(); + builder.buildInvalidatedProject(); // The file should be updated assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); // Build downstream projects should update 'tests', but not 'core' tick(); - builder.buildDependentInvalidatedProjects(); + builder.buildInvalidatedProject(); assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have been rebuilt"); assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); }); @@ -240,11 +224,10 @@ namespace ts { function verifyProjectWithResolveJsonModule(configFile: string, ...expectedDiagnosticMessages: DiagnosticMessage[]) { const fs = projFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, [configFile], { dry: false, force: false, verbose: false }); - clearDiagnostics(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, [configFile], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); - assertDiagnosticMessages(...expectedDiagnosticMessages); + host.assertDiagnosticMessages(...expectedDiagnosticMessages); if (!expectedDiagnosticMessages.length) { // Check for outputs. Not an exhaustive list for (const output of allExpectedOutputs) { @@ -274,11 +257,11 @@ namespace ts { let fs: vfs.FileSystem | undefined; before(() => { fs = outFileFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/third"], { dry: false, force: false, verbose: false }); - clearDiagnostics(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: false }); + host.clearDiagnostics(); builder.buildAllProjects(); - assertDiagnosticMessages(/*none*/); + host.assertDiagnosticMessages(/*none*/); }); after(() => { fs = undefined; @@ -295,25 +278,24 @@ namespace ts { describe("tsbuild - downstream prepend projects always get rebuilt", () => { it("", () => { const fs = outFileFs.shadow(); - const host = new fakes.CompilerHost(fs); - const builder = createSolutionBuilder(host, buildHost, ["/src/third"], { dry: false, force: false, verbose: false }); - clearDiagnostics(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: false }); builder.buildAllProjects(); - assertDiagnosticMessages(/*none*/); + host.assertDiagnosticMessages(/*none*/); assert.equal(fs.statSync("src/third/thirdjs/output/third-output.js").mtimeMs, time(), "First build timestamp is correct"); tick(); replaceText(fs, "src/first/first_PART1.ts", "Hello", "Hola"); tick(); builder.resetBuildContext(); builder.buildAllProjects(); - assertDiagnosticMessages(/*none*/); + host.assertDiagnosticMessages(/*none*/); assert.equal(fs.statSync("src/third/thirdjs/output/third-output.js").mtimeMs, time(), "Second build timestamp is correct"); }); }); } describe("tsbuild - graph-ordering", () => { - let host: fakes.CompilerHost | undefined; + let host: fakes.SolutionBuilderHost | undefined; const deps: [string, string][] = [ ["A", "B"], ["B", "C"], @@ -326,7 +308,7 @@ namespace ts { before(() => { const fs = new vfs.FileSystem(false); - host = new fakes.CompilerHost(fs); + host = new fakes.SolutionBuilderHost(fs); writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps); }); @@ -351,7 +333,7 @@ namespace ts { }); function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) { - const builder = createSolutionBuilder(host!, buildHost, rootNames, { dry: true, force: false, verbose: false }); + const builder = createSolutionBuilder(host!, rootNames, { dry: true, force: false, verbose: false }); const projFileNames = rootNames.map(getProjectFileName); const graph = builder.getBuildGraph(projFileNames); @@ -406,30 +388,6 @@ namespace ts { fs.writeFileSync(path, newContent, "utf-8"); } - function assertDiagnosticMessages(...expected: DiagnosticMessage[]) { - const actual = lastDiagnostics.slice(); - if (actual.length !== expected.length) { - assert.fail(actual, expected, `Diagnostic arrays did not match - got\r\n${actual.map(a => " " + a.messageText).join("\r\n")}\r\nexpected\r\n${expected.map(e => " " + e.message).join("\r\n")}`); - } - for (let i = 0; i < actual.length; i++) { - if (actual[i].code !== expected[i].code) { - assert.fail(actual[i].messageText, expected[i].message, `Mismatched error code - expected diagnostic ${i} "${actual[i].messageText}" to match ${expected[i].message}`); - } - } - } - - function clearDiagnostics() { - lastDiagnostics = []; - } - - export function printDiagnostics(header = "== Diagnostics ==") { - const out = createDiagnosticReporter(sys); - sys.write(header + "\r\n"); - for (const d of lastDiagnostics) { - out(d); - } - } - function tick() { currentTime += 60_000; } diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts new file mode 100644 index 00000000000..d1405fa9e2c --- /dev/null +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -0,0 +1,133 @@ +namespace ts.tscWatch { + export import libFile = TestFSWithWatch.libFile; + function createSolutionBuilder(system: WatchedSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { + const host = createSolutionBuilderWithWatchHost(system); + return ts.createSolutionBuilder(host, rootNames, defaultOptions || { dry: false, force: false, verbose: false, watch: true }); + } + + function createSolutionBuilderWithWatch(host: WatchedSystem, rootNames: ReadonlyArray, defaultOptions?: BuildOptions) { + const solutionBuilder = createSolutionBuilder(host, rootNames, defaultOptions); + solutionBuilder.buildAllProjects(); + solutionBuilder.startWatching(); + return solutionBuilder; + } + + describe("tsbuild-watch program updates", () => { + const projectsLocation = "/user/username/projects"; + const project = "sample1"; + const enum SubProject { + core = "core", + logic = "logic", + tests = "tests", + ui = "ui" + } + type ReadonlyFile = Readonly; + /** [tsconfig, index] | [tsconfig, index, anotherModule, someDecl] */ + type SubProjectFiles = [ReadonlyFile, ReadonlyFile] | [ReadonlyFile, ReadonlyFile, ReadonlyFile, ReadonlyFile]; + const root = Harness.IO.getWorkspaceRoot(); + + function projectFilePath(subProject: SubProject, baseFileName: string) { + return `${projectsLocation}/${project}/${subProject}/${baseFileName.toLowerCase()}`; + } + + function projectFile(subProject: SubProject, baseFileName: string): File { + return { + path: projectFilePath(subProject, baseFileName), + content: Harness.IO.readFile(`${root}/tests/projects/${project}/${subProject}/${baseFileName}`)! + }; + } + + function subProjectFiles(subProject: SubProject, anotherModuleAndSomeDecl?: true): SubProjectFiles { + const tsconfig = projectFile(subProject, "tsconfig.json"); + const index = projectFile(subProject, "index.ts"); + if (!anotherModuleAndSomeDecl) { + return [tsconfig, index]; + } + const anotherModule = projectFile(SubProject.core, "anotherModule.ts"); + const someDecl = projectFile(SubProject.core, "some_decl.ts"); + return [tsconfig, index, anotherModule, someDecl]; + } + + function getOutputFileNames(subProject: SubProject, baseFileNameWithoutExtension: string) { + const file = projectFilePath(subProject, baseFileNameWithoutExtension); + return [`${file}.js`, `${file}.d.ts`]; + } + + type OutputFileStamp = [string, Date | undefined]; + function getOutputStamps(host: WatchedSystem, subProject: SubProject, baseFileNameWithoutExtension: string): OutputFileStamp[] { + return getOutputFileNames(subProject, baseFileNameWithoutExtension).map(f => [f, host.getModifiedTime(f)] as OutputFileStamp); + } + + function getOutputFileStamps(host: WatchedSystem): OutputFileStamp[] { + return [ + ...getOutputStamps(host, SubProject.core, "anotherModule"), + ...getOutputStamps(host, SubProject.core, "index"), + ...getOutputStamps(host, SubProject.logic, "index"), + ...getOutputStamps(host, SubProject.tests, "index"), + ]; + } + + function verifyChangedFiles(actualStamps: OutputFileStamp[], oldTimeStamps: OutputFileStamp[], changedFiles: string[]) { + for (let i = 0; i < oldTimeStamps.length; i++) { + const actual = actualStamps[i]; + const old = oldTimeStamps[i]; + if (contains(changedFiles, actual[0])) { + assert.isTrue((actual[1] || 0) > (old[1] || 0), `${actual[0]} expected to written`); + } + else { + assert.equal(actual[1], old[1], `${actual[0]} expected to not change`); + } + } + } + + const core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); + const logic = subProjectFiles(SubProject.logic); + const tests = subProjectFiles(SubProject.tests); + const ui = subProjectFiles(SubProject.ui); + const allFiles: ReadonlyArray = [libFile, ...core, ...logic, ...tests, ...ui]; + const testProjectExpectedWatchedFiles = [core[0], core[1], core[2], ...logic, ...tests].map(f => f.path); + + function createSolutionInWatchMode() { + const host = createWatchedSystem(allFiles, { currentDirectory: projectsLocation }); + createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`]); + checkWatchedFiles(host, testProjectExpectedWatchedFiles); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); // TODO: #26524 + checkOutputErrorsInitial(host, emptyArray); + const outputFileStamps = getOutputFileStamps(host); + for (const stamp of outputFileStamps) { + assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); + } + return { host, outputFileStamps }; + } + it("creates solution in watch mode", () => { + createSolutionInWatchMode(); + }); + + it("change builds changes and reports found errors message", () => { + const { host, outputFileStamps } = createSolutionInWatchMode(); + host.writeFile(core[1].path, `${core[1].content} +export class someClass { }`); + host.checkTimeoutQueueLengthAndRun(1); // Builds core + const changedCore = getOutputFileStamps(host); + verifyChangedFiles(changedCore, outputFileStamps, [ + ...getOutputFileNames(SubProject.core, "anotherModule"), // This should not be written really + ...getOutputFileNames(SubProject.core, "index") + ]); + host.checkTimeoutQueueLengthAndRun(1); // Builds tests + const changedTests = getOutputFileStamps(host); + verifyChangedFiles(changedTests, changedCore, [ + ...getOutputFileNames(SubProject.tests, "index") // Again these need not be written + ]); + host.checkTimeoutQueueLengthAndRun(1); // Builds logic + const changedLogic = getOutputFileStamps(host); + verifyChangedFiles(changedLogic, changedTests, [ + ...getOutputFileNames(SubProject.logic, "index") // Again these need not be written + ]); + host.checkTimeoutQueueLength(0); + checkOutputErrorsIncremental(host, emptyArray); + }); + + // TODO: write tests reporting errors but that will have more involved work since file + }); +} diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index e6c805148ee..effbd0ede7d 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -1,17 +1,16 @@ namespace ts.tscWatch { - import WatchedSystem = TestFSWithWatch.TestServerHost; - type File = TestFSWithWatch.File; - type SymLink = TestFSWithWatch.SymLink; - import createWatchedSystem = TestFSWithWatch.createWatchedSystem; - import checkArray = TestFSWithWatch.checkArray; - import libFile = TestFSWithWatch.libFile; - import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - import checkOutputContains = TestFSWithWatch.checkOutputContains; - import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; - import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; + export import WatchedSystem = TestFSWithWatch.TestServerHost; + export type File = TestFSWithWatch.File; + export type SymLink = TestFSWithWatch.SymLink; + export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; + export import checkArray = TestFSWithWatch.checkArray; + export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; + export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; + export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; + export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; + export import checkOutputContains = TestFSWithWatch.checkOutputContains; + export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; + export import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); @@ -111,7 +110,7 @@ namespace ts.tscWatch { function assertWatchDiagnostic(diagnostic: Diagnostic) { const expected = getWatchDiagnosticWithoutDate(diagnostic); - if (!disableConsoleClears && !contains(nonClearingMessageCodes, diagnostic.code)) { + if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); screenClears++; } @@ -137,7 +136,7 @@ namespace ts.tscWatch { : createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length); } - function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { + export function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray, disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { checkOutputErrors( host, /*logsBeforeWatchDiagnostic*/ undefined, @@ -148,7 +147,7 @@ namespace ts.tscWatch { createErrorsFoundCompilerDiagnostic(errors)); } - function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + export function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { checkOutputErrors( host, logsBeforeWatchDiagnostic, diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 309107923e9..26002b4b180 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -8074,7 +8074,7 @@ new C();` verifyProjectWithResolvedModule(session); - host.removeFolder(recognizersTextDist, /*recursive*/ true); + host.deleteFolder(recognizersTextDist, /*recursive*/ true); host.runQueuedTimeoutCallbacks(); verifyProjectWithUnresolvedModule(session); @@ -8906,7 +8906,7 @@ describe("Test Suite 1", () => { checkProjectActualFiles(project, expectedFilesWithUnitTest1); const navBarResultUnitTest1 = navBarFull(session, unitTest1); - host.removeFile(unitTest1.path); + host.deleteFile(unitTest1.path); host.checkTimeoutQueueLengthAndRun(2); checkProjectActualFiles(project, expectedFilesWithoutUnitTest1); @@ -9011,7 +9011,7 @@ export function Test2() { checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); // Testing what happens if we delete the original sources. - host.removeFile(bTs.path); + host.deleteFile(bTs.path); openFilesForSession([userTs], session); diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 97e239c8bca..1685dfc3449 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -13,7 +13,7 @@ namespace ts { } let reportDiagnostic = createDiagnosticReporter(sys); - function updateReportDiagnostic(options: CompilerOptions) { + function updateReportDiagnostic(options?: CompilerOptions) { if (shouldBePretty(options)) { reportDiagnostic = createDiagnosticReporter(sys, /*pretty*/ true); } @@ -23,8 +23,8 @@ namespace ts { return !!sys.writeOutputIsTTY && sys.writeOutputIsTTY(); } - function shouldBePretty(options: CompilerOptions) { - if (typeof options.pretty === "undefined") { + function shouldBePretty(options?: CompilerOptions) { + if (!options || typeof options.pretty === "undefined") { return defaultIsPretty(); } return options.pretty; @@ -54,15 +54,7 @@ namespace ts { export function executeCommandLine(args: string[]): void { if (args.length > 0 && ((args[0].toLowerCase() === "--build") || (args[0].toLowerCase() === "-b"))) { - const reportDiag = createDiagnosticReporter(sys, defaultIsPretty()); - const report = (message: DiagnosticMessage, ...args: string[]) => reportDiag(createCompilerDiagnostic(message, ...args)); - const buildHost: BuildHost = { - error: report, - verbose: report, - message: report, - errorDiagnostic: d => reportDiag(d) - }; - const result = performBuild(args.slice(1), createCompilerHost({}), buildHost, sys); + const result = performBuild(args.slice(1)); // undefined = in watch mode, do not exit if (result !== undefined) { return sys.exit(result); @@ -172,6 +164,146 @@ namespace ts { } } + function performBuild(args: string[]): number | undefined { + const buildOpts: CommandLineOption[] = [ + { + name: "help", + shortName: "h", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Print_this_message, + }, + { + name: "help", + shortName: "?", + type: "boolean" + }, + { + name: "verbose", + shortName: "v", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Enable_verbose_logging, + type: "boolean" + }, + { + name: "dry", + shortName: "d", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean" + }, + { + name: "force", + shortName: "f", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean" + }, + { + name: "clean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean" + }, + { + name: "watch", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Watch_input_files, + type: "boolean" + }, + { + name: "preserveWatchOutput", + type: "boolean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Whether_to_keep_outdated_console_output_in_watch_mode_instead_of_clearing_the_screen, + }, + ]; + let buildOptionNameMap: OptionNameMap | undefined; + const returnBuildOptionNameMap = () => (buildOptionNameMap || (buildOptionNameMap = createOptionNameMap(buildOpts))); + + const buildOptions: BuildOptions = {}; + const projects: string[] = []; + for (const arg of args) { + if (arg.charCodeAt(0) === CharacterCodes.minus) { + const opt = getOptionDeclarationFromName(returnBuildOptionNameMap, arg.slice(arg.charCodeAt(1) === CharacterCodes.minus ? 2 : 1), /*allowShort*/ true); + if (opt) { + buildOptions[opt.name as keyof BuildOptions] = true; + } + else { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Unknown_build_option_0, arg)); + } + } + else { + // Not a flag, parse as filename + addProject(arg); + } + } + + if (buildOptions.help) { + printVersion(); + printHelp(buildOpts, "--build "); + return ExitStatus.Success; + } + + // Update to pretty if host supports it + updateReportDiagnostic(); + + if (!sys.getModifiedTime || !sys.setModifiedTime || (buildOptions.clean && !sys.deleteFile)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--build")); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + if (buildOptions.watch) { + reportWatchModeWithoutSysSupport(); + } + + // Nonsensical combinations + if (buildOptions.clean && buildOptions.force) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + if (buildOptions.clean && buildOptions.verbose) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + if (buildOptions.clean && buildOptions.watch) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + if (buildOptions.watch && buildOptions.dry) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + addProject("."); + } + + // TODO: change this to host if watch => watchHost otherwiue without wathc + const builder = createSolutionBuilder(createSolutionBuilderWithWatchHost(sys, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty()), createWatchStatusReporter()), projects, buildOptions); + if (buildOptions.clean) { + return builder.cleanAllProjects(); + } + + if (buildOptions.watch) { + builder.buildAllProjects(); + builder.startWatching(); + return undefined; + } + + return builder.buildAllProjects(); + + function addProject(projectSpecification: string) { + const fileName = resolvePath(sys.getCurrentDirectory(), projectSpecification); + const refPath = resolveProjectReferencePath(sys, { path: fileName }); + if (!sys.fileExists(refPath)) { + return reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + } + projects.push(refPath); + } + } + function performCompilation(rootNames: string[], projectReferences: ReadonlyArray | undefined, options: CompilerOptions, configFileParsingDiagnostics?: ReadonlyArray) { const host = createCompilerHost(options); enableStatistics(options); @@ -205,7 +337,7 @@ namespace ts { }; } - function createWatchStatusReporter(options: CompilerOptions) { + function createWatchStatusReporter(options?: CompilerOptions) { return ts.createWatchStatusReporter(sys, shouldBePretty(options)); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index b16026b18a8..7221c99f9ef 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2677,9 +2677,6 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string | undefined; createHash?(data: string): string; - getModifiedTime?(fileName: string): Date; - setModifiedTime?(fileName: string, date: Date): void; - deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -4295,15 +4292,26 @@ declare namespace ts { type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void; /** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */ type CreateProgram = (rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray) => T; - interface WatchCompilerHost { + /** Host that has watch functionality used in --watch mode */ + interface WatchHost { + /** If provided, called with Diagnostic message that informs about change in watch status */ + onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions): void; + /** Used to watch changes in source files, missing files needed to update the program or config file */ + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ + setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + /** If provided, will be used to reset existing delayed compilation */ + clearTimeout?(timeoutId: any): void; + } + interface WatchCompilerHost extends WatchHost { /** * Used to create the program when need for program creation or recreation detected */ createProgram: CreateProgram; /** If provided, callback to invoke after every new program creation */ afterProgramCreate?(program: T): void; - /** If provided, called with Diagnostic message that informs about change in watch status */ - onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions): void; useCaseSensitiveFileNames(): boolean; getNewLine(): string; getCurrentDirectory(): string; @@ -4336,14 +4344,6 @@ declare namespace ts { resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; /** If provided, used to resolve type reference directives, otherwise typescript's default resolution */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - /** Used to watch changes in source files, missing files needed to update the program or config file */ - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; - /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; - /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ - setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - /** If provided, will be used to reset existing delayed compilation */ - clearTimeout?(timeoutId: any): void; } /** * Host to create watch with root files and options diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index bb03f59ceb8..f7ab36b4702 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2677,9 +2677,6 @@ declare namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string | undefined; createHash?(data: string): string; - getModifiedTime?(fileName: string): Date; - setModifiedTime?(fileName: string, date: Date): void; - deleteFile?(fileName: string): void; } interface SourceMapRange extends TextRange { source?: SourceMapSource; @@ -4295,15 +4292,26 @@ declare namespace ts { type WatchStatusReporter = (diagnostic: Diagnostic, newLine: string, options: CompilerOptions) => void; /** Create the program with rootNames and options, if they are undefined, oldProgram and new configFile diagnostics create new program */ type CreateProgram = (rootNames: ReadonlyArray | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: T, configFileParsingDiagnostics?: ReadonlyArray) => T; - interface WatchCompilerHost { + /** Host that has watch functionality used in --watch mode */ + interface WatchHost { + /** If provided, called with Diagnostic message that informs about change in watch status */ + onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions): void; + /** Used to watch changes in source files, missing files needed to update the program or config file */ + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; + /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ + setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + /** If provided, will be used to reset existing delayed compilation */ + clearTimeout?(timeoutId: any): void; + } + interface WatchCompilerHost extends WatchHost { /** * Used to create the program when need for program creation or recreation detected */ createProgram: CreateProgram; /** If provided, callback to invoke after every new program creation */ afterProgramCreate?(program: T): void; - /** If provided, called with Diagnostic message that informs about change in watch status */ - onWatchStatusChange?(diagnostic: Diagnostic, newLine: string, options: CompilerOptions): void; useCaseSensitiveFileNames(): boolean; getNewLine(): string; getCurrentDirectory(): string; @@ -4336,14 +4344,6 @@ declare namespace ts { resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; /** If provided, used to resolve type reference directives, otherwise typescript's default resolution */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - /** Used to watch changes in source files, missing files needed to update the program or config file */ - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; - /** Used to watch resolved module's failed lookup locations, config file specs, type roots where auto type reference directives are added */ - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; - /** If provided, will be used to set delayed compilation, so that multiple changes in short span are compiled together */ - setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - /** If provided, will be used to reset existing delayed compilation */ - clearTimeout?(timeoutId: any): void; } /** * Host to create watch with root files and options