diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 909dd0feedeb4..aee8c98db0d62 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -18,8 +18,8 @@ namespace ts { * If an array, the full list of source files to emit. * Else, calls `getSourceFilesToEmit` with the (optional) target source file to determine the list of source files to emit. */ - export function forEachEmittedFile( - host: EmitHost, action: (emitFileNames: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle, emitOnlyDtsFiles: boolean) => void, + export function forEachEmittedFile( + host: EmitHost, action: (emitFileNames: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle, emitOnlyDtsFiles: boolean) => T, sourceFilesOrTargetSourceFile?: SourceFile[] | SourceFile, emitOnlyDtsFiles?: boolean) { @@ -30,7 +30,10 @@ namespace ts { const jsFilePath = options.outFile || options.out; const sourceMapFilePath = getSourceMapFilePath(jsFilePath, options); const declarationFilePath = options.declaration ? removeFileExtension(jsFilePath) + Extension.Dts : ""; - action({ jsFilePath, sourceMapFilePath, declarationFilePath }, createBundle(sourceFiles), emitOnlyDtsFiles); + const result = action({ jsFilePath, sourceMapFilePath, declarationFilePath }, createBundle(sourceFiles), emitOnlyDtsFiles); + if (result) { + return result; + } } } else { @@ -38,7 +41,10 @@ namespace ts { const jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, getOutputExtension(sourceFile, options)); const sourceMapFilePath = getSourceMapFilePath(jsFilePath, options); const declarationFilePath = !isSourceFileJavaScript(sourceFile) && (emitOnlyDtsFiles || options.declaration) ? getDeclarationEmitOutputFilePath(sourceFile, host) : undefined; - action({ jsFilePath, sourceMapFilePath, declarationFilePath }, sourceFile, emitOnlyDtsFiles); + const result = action({ jsFilePath, sourceMapFilePath, declarationFilePath }, sourceFile, emitOnlyDtsFiles); + if (result) { + return result; + } } } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 88a2f5ee8f1f1..0d1bbf233a3f5 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -667,7 +667,8 @@ namespace ts { dropDiagnosticsProducingTypeChecker, getSourceFileFromReference, sourceFileToPackageName, - redirectTargetsSet + redirectTargetsSet, + isEmittedFile }; verifyCompilerOptions(); @@ -2343,6 +2344,20 @@ namespace ts { hasEmitBlockingDiagnostics.set(toPath(emitFileName), true); programDiagnostics.add(diag); } + + function isEmittedFile(file: string) { + if (options.noEmit) { + return false; + } + + return forEachEmittedFile(getEmitHost(), ({ jsFilePath, declarationFilePath }) => + isSameFile(jsFilePath, file) || + (declarationFilePath && isSameFile(declarationFilePath, file))); + } + + function isSameFile(file1: string, file2: string) { + return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + } } /* @internal */ diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index e21e81a2e888c..aac25c5d59687 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -52,6 +52,7 @@ namespace ts { getGlobalCache?(): string | undefined; writeLog(s: string): void; maxNumberOfFilesToIterateForInvalidation?: number; + getCurrentProgram(): Program; } interface DirectoryWatchesOfFailedLookup { @@ -472,6 +473,11 @@ namespace ts { resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } + // Ignore emits from the program + if (isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectory)) { + return; + } + // If the files are added to project root or node_modules directory, always run through the invalidation process // Otherwise run through invalidation only if adding to the immediate directory if (!allFilesHaveInvalidatedResolution && diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 23798635671a8..e4ada136730a7 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2659,6 +2659,8 @@ namespace ts { /* @internal */ sourceFileToPackageName: Map; /** Set of all source files that some other source file redirects to. */ /* @internal */ redirectTargetsSet: Map; + /** Is the file emitted file */ + /* @internal */ isEmittedFile(file: string): boolean; } /* @internal */ diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index b4ed1c1ac1b13..79e26f5986cf8 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -20,6 +20,9 @@ namespace ts { // Callbacks to do custom action before creating program and after creating program beforeCompile(compilerOptions: CompilerOptions): void; afterCompile(host: DirectoryStructureHost, program: Program, builder: Builder): void; + + // Only for testing + maxNumberOfFilesToIterateForInvalidation?: number; } const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { @@ -301,6 +304,8 @@ namespace ts { hasChangedAutomaticTypeDirectiveNames = true; scheduleProgramUpdate(); }, + maxNumberOfFilesToIterateForInvalidation: watchingHost.maxNumberOfFilesToIterateForInvalidation, + getCurrentProgram, writeLog }; // Cache for the module resolution @@ -318,7 +323,11 @@ namespace ts { // Update the wild card directory watch watchConfigFileWildCardDirectories(); - return () => program; + return getCurrentProgram; + + function getCurrentProgram() { + return program; + } function synchronizeProgram() { writeLog(`Synchronizing program`); diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 0cf38f372d907..0bc6920479be4 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -82,6 +82,14 @@ namespace ts { } } + export function isEmittedFileOfProgram(program: Program | undefined, file: string) { + if (!program) { + return false; + } + + return program.isEmittedFile(file); + } + export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { return host.watchFile(file, cb); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 758a55b8f9518..9e01021155c72 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -30,8 +30,9 @@ namespace ts.tscWatch { return ts.parseConfigFile(configFileName, {}, watchingSystemHost.system, watchingSystemHost.reportDiagnostic, watchingSystemHost.reportWatchDiagnostic); } - function createWatchModeWithConfigFile(configFilePath: string, host: WatchedSystem) { + function createWatchModeWithConfigFile(configFilePath: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { const watchingSystemHost = createWatchingSystemHost(host); + watchingSystemHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; const configFileResult = parseConfigFile(configFilePath, watchingSystemHost); return ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); } @@ -1067,6 +1068,36 @@ namespace ts.tscWatch { assert.equal(nowErrors[0].start, intialErrors[0].start - configFileContentComment.length); assert.equal(nowErrors[1].start, intialErrors[1].start - configFileContentComment.length); }); + + it("should not trigger recompilation because of program emit", () => { + const proj = "/user/username/projects/myproject"; + const file1: FileOrFolder = { + path: `${proj}/file1.ts`, + content: "export const c = 30;" + }; + const file2: FileOrFolder = { + path: `${proj}/src/file2.ts`, + content: `import {c} from "file1"; export const d = 30;` + }; + const tsconfig: FileOrFolder = { + path: `${proj}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "amd", + outDir: "build" + } + }) + }; + const host = createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: proj }); + const watch = createWatchModeWithConfigFile(tsconfig.path, host, /*maxNumberOfFilesToIterateForInvalidation*/1); + checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); + + assert.isTrue(host.fileExists("build/file1.js")); + assert.isTrue(host.fileExists("build/src/file2.js")); + + // This should be 0 + host.checkTimeoutQueueLengthAndRun(0); + }); }); describe("tsc-watch emit with outFile or out setting", () => { diff --git a/src/server/project.ts b/src/server/project.ts index ebc93ae3e1e09..a25e6870bdab2 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -830,7 +830,10 @@ namespace ts.server { return !hasChanges; } - + /* @internal */ + getCurrentProgram() { + return this.program; + } protected removeExistingTypings(include: string[]): string[] { const existing = ts.getAutomaticTypeDirectiveNames(this.getCompilerOptions(), this.directoryStructureHost);