diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 192f1e430278e..635855083c08d 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -6,58 +6,38 @@ namespace ts { emitSkipped: boolean; } - export interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } - export interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; } +} - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { +/* @internal */ +namespace ts { + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { const outputFiles: OutputFile[] = []; - let emittedSourceFiles: SourceFile[]; const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - if (!isDetailed) { return { outputFiles, emitSkipped: emitResult.emitSkipped }; - } - - return { - outputFiles, - emitSkipped: emitResult.emitSkipped, - diagnostics: emitResult.diagnostics, - sourceMaps: emitResult.sourceMaps, - emittedSourceFiles - }; - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) { + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { outputFiles.push({ name: fileName, writeByteOrderMark, text }); - if (isDetailed) { - emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles); - } } } -} -/* @internal */ -namespace ts { export interface Builder { - /** - * Call this to feed new program - */ + /** Called to inform builder about new program */ updateProgram(newProgram: Program): void; - getFilesAffectedBy(program: Program, path: Path): string[]; - emitFile(program: Program, path: Path): EmitOutput; + + /** Gets the files affected by the file path */ + getFilesAffectedBy(program: Program, path: Path): ReadonlyArray; /** Emit the changed files and clear the cache of the changed files */ - emitChangedFiles(program: Program): EmitOutputDetailed[]; + emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray; + /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ - getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[]; + getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray; /** Called to reset the status of the builder */ clear(): void; @@ -88,20 +68,17 @@ namespace ts { /** * Gets the files affected by the script info which has updated shape from the known one */ - getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; + getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray; } interface FileInfo { - fileName: string; version: string; signature: string; } export interface BuilderOptions { getCanonicalFileName: (fileName: string) => string; - getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed; computeHash: (data: string) => string; - shouldEmitFile: (sourceFile: SourceFile) => boolean; } export function createBuilder(options: BuilderOptions): Builder { @@ -109,12 +86,13 @@ namespace ts { const fileInfos = createMap(); const semanticDiagnosticsPerFile = createMap>(); /** The map has key by source file's path that has been changed */ - const changedFileNames = createMap(); + const changedFilesSet = createMap(); + const hasShapeChanged = createMap(); + let allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; let emitHandler: EmitHandler; return { updateProgram, getFilesAffectedBy, - emitFile, emitChangedFiles, getSemanticDiagnostics, clear @@ -128,6 +106,8 @@ namespace ts { fileInfos.clear(); semanticDiagnosticsPerFile.clear(); } + hasShapeChanged.clear(); + allFilesExcludingDefaultLibraryFile = undefined; mutateMap( fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), @@ -142,31 +122,34 @@ namespace ts { ); } - function registerChangedFile(path: Path, fileName: string) { - changedFileNames.set(path, fileName); + function registerChangedFile(path: Path) { + changedFilesSet.set(path, true); // All changed files need to re-evaluate its semantic diagnostics semanticDiagnosticsPerFile.delete(path); } function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { - registerChangedFile(sourceFile.path, sourceFile.fileName); + registerChangedFile(sourceFile.path); emitHandler.onAddSourceFile(program, sourceFile); - return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined }; + return { version: sourceFile.version, signature: undefined }; } - function removeExistingFileInfo(existingFileInfo: FileInfo, path: Path) { - registerChangedFile(path, existingFileInfo.fileName); + function removeExistingFileInfo(_existingFileInfo: FileInfo, path: Path) { + // Since we dont need to track removed file as changed file + // We can just remove its diagnostics + changedFilesSet.delete(path); + semanticDiagnosticsPerFile.delete(path); emitHandler.onRemoveSourceFile(path); } function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { if (existingInfo.version !== sourceFile.version) { - registerChangedFile(sourceFile.path, sourceFile.fileName); + registerChangedFile(sourceFile.path); existingInfo.version = sourceFile.version; emitHandler.onUpdateSourceFile(program, sourceFile); } else if (emitHandler.onUpdateSourceFileWithSameVersion(program, sourceFile)) { - registerChangedFile(sourceFile.path, sourceFile.fileName); + registerChangedFile(sourceFile.path); } } @@ -182,114 +165,95 @@ namespace ts { } } - function getFilesAffectedBy(program: Program, path: Path): string[] { + function getFilesAffectedBy(program: Program, path: Path): ReadonlyArray { ensureProgramGraph(program); - const sourceFile = program.getSourceFile(path); - const singleFileResult = sourceFile && options.shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; - const info = fileInfos.get(path); - if (!info || !updateShapeSignature(program, sourceFile, info)) { - return singleFileResult; + const sourceFile = program.getSourceFileByPath(path); + if (!sourceFile) { + return emptyArray; } - Debug.assert(!!sourceFile); - return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult); + if (!updateShapeSignature(program, sourceFile)) { + return [sourceFile]; + } + return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile); } - function emitFile(program: Program, path: Path) { + function emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray { ensureProgramGraph(program); - if (!fileInfos.has(path)) { - return { outputFiles: [], emitSkipped: true }; + const compilerOptions = program.getCompilerOptions(); + + if (!changedFilesSet.size) { + return emptyArray; } - return options.getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false); - } + // With --out or --outFile all outputs go into single file, do it only once + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(semanticDiagnosticsPerFile.size === 0); + changedFilesSet.clear(); + return [program.emit(/*targetSourceFile*/ undefined, writeFileCallback)]; + } - function enumerateChangedFilesSet( - program: Program, - onChangedFile: (fileName: string, path: Path) => void, - onAffectedFile: (fileName: string, sourceFile: SourceFile) => void - ) { - changedFileNames.forEach((fileName, path) => { - onChangedFile(fileName, path as Path); + const seenFiles = createMap(); + let result: EmitResult[] | undefined; + changedFilesSet.forEach((_true, path) => { + // Get the affected Files by this program const affectedFiles = getFilesAffectedBy(program, path as Path); - for (const file of affectedFiles) { - onAffectedFile(file, program.getSourceFile(file)); - } - }); - } + affectedFiles.forEach(affectedFile => { + // Affected files shouldnt have cached diagnostics + semanticDiagnosticsPerFile.delete(affectedFile.path); - function enumerateChangedFilesEmitOutput( - program: Program, - emitOnlyDtsFiles: boolean, - onChangedFile: (fileName: string, path: Path) => void, - onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void - ) { - const seenFiles = createMap(); - enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => { - if (!seenFiles.has(fileName)) { - seenFiles.set(fileName, true); - if (sourceFile) { - // Any affected file shouldnt have the cached diagnostics - semanticDiagnosticsPerFile.delete(sourceFile.path); - - const emitOutput = options.getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed; - onEmitOutput(emitOutput, sourceFile); - - // mark all the emitted source files as seen - if (emitOutput.emittedSourceFiles) { - for (const file of emitOutput.emittedSourceFiles) { - seenFiles.set(file.fileName, true); - } - } + if (!seenFiles.has(affectedFile.path)) { + seenFiles.set(affectedFile.path, true); + + // Emit the affected file + (result || (result = [])).push(program.emit(affectedFile, writeFileCallback)); } - } + }); }); + changedFilesSet.clear(); + return result || emptyArray; } - function emitChangedFiles(program: Program): EmitOutputDetailed[] { - ensureProgramGraph(program); - const result: EmitOutputDetailed[] = []; - enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false, - /*onChangedFile*/ noop, emitOutput => result.push(emitOutput)); - changedFileNames.clear(); - return result; - } - - function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] { + function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray { ensureProgramGraph(program); + Debug.assert(changedFilesSet.size === 0); - // Ensure that changed files have cleared their respective - enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => { - if (sourceFile) { - semanticDiagnosticsPerFile.delete(sourceFile.path); - } - }); + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(semanticDiagnosticsPerFile.size === 0); + // We dont need to cache the diagnostics just return them from program + return program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); + } let diagnostics: Diagnostic[]; for (const sourceFile of program.getSourceFiles()) { - const path = sourceFile.path; - const cachedDiagnostics = semanticDiagnosticsPerFile.get(path); - // Report the semantic diagnostics from the cache if we already have those diagnostics present - if (cachedDiagnostics) { - diagnostics = addRange(diagnostics, cachedDiagnostics); - } - else { - // Diagnostics werent cached, get them from program, and cache the result - const cachedDiagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken); - semanticDiagnosticsPerFile.set(path, cachedDiagnostics); - diagnostics = addRange(diagnostics, cachedDiagnostics); - } + diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(program, sourceFile, cancellationToken)); } return diagnostics || emptyArray; } + function getSemanticDiagnosticsOfFile(program: Program, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + const path = sourceFile.path; + const cachedDiagnostics = semanticDiagnosticsPerFile.get(path); + // Report the semantic diagnostics from the cache if we already have those diagnostics present + if (cachedDiagnostics) { + return cachedDiagnostics; + } + + // Diagnostics werent cached, get them from program, and cache the result + const diagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken); + semanticDiagnosticsPerFile.set(path, diagnostics); + return diagnostics; + } + function clear() { isModuleEmit = undefined; emitHandler = undefined; fileInfos.clear(); semanticDiagnosticsPerFile.clear(); - changedFileNames.clear(); + changedFilesSet.clear(); + hasShapeChanged.clear(); } /** @@ -310,15 +274,26 @@ namespace ts { /** * @return {boolean} indicates if the shape signature has changed since last update. */ - function updateShapeSignature(program: Program, sourceFile: SourceFile, info: FileInfo) { + function updateShapeSignature(program: Program, sourceFile: SourceFile) { + Debug.assert(!!sourceFile); + + // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate + if (hasShapeChanged.has(sourceFile.path)) { + return false; + } + + hasShapeChanged.set(sourceFile.path, true); + const info = fileInfos.get(sourceFile.path); + Debug.assert(!!info); + const prevSignature = info.signature; let latestSignature: string; if (sourceFile.isDeclarationFile) { - latestSignature = options.computeHash(sourceFile.text); + latestSignature = sourceFile.version; info.signature = latestSignature; } else { - const emitOutput = options.getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false); + const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = options.computeHash(emitOutput.outputFiles[0].text); info.signature = latestSignature; @@ -386,24 +361,27 @@ namespace ts { } /** - * Gets all the emittable files from the program. - * @param firstSourceFile This one will be emitted first. See https://github.com/Microsoft/TypeScript/issues/16888 + * Gets all files of the program excluding the default library file */ - function getAllEmittableFiles(program: Program, firstSourceFile: SourceFile): string[] { - const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions()); - const sourceFiles = program.getSourceFiles(); - const result: string[] = []; - add(firstSourceFile); - for (const sourceFile of sourceFiles) { + function getAllFilesExcludingDefaultLibraryFile(program: Program, firstSourceFile: SourceFile): ReadonlyArray { + // Use cached result + if (allFilesExcludingDefaultLibraryFile) { + return allFilesExcludingDefaultLibraryFile; + } + + let result: SourceFile[]; + addSourceFile(firstSourceFile); + for (const sourceFile of program.getSourceFiles()) { if (sourceFile !== firstSourceFile) { - add(sourceFile); + addSourceFile(sourceFile); } } - return result; + allFilesExcludingDefaultLibraryFile = result || emptyArray; + return allFilesExcludingDefaultLibraryFile; - function add(sourceFile: SourceFile): void { - if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && options.shouldEmitFile(sourceFile)) { - result.push(sourceFile.fileName); + function addSourceFile(sourceFile: SourceFile) { + if (!program.isSourceFileDefaultLibrary(sourceFile)) { + (result || (result = [])).push(sourceFile); } } } @@ -417,14 +395,14 @@ namespace ts { getFilesAffectedByUpdatedShape }; - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { const options = program.getCompilerOptions(); // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, // so returning the file itself is good enough. if (options && (options.out || options.outFile)) { - return singleFileResult; + return [sourceFile]; } - return getAllEmittableFiles(program, sourceFile); + return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); } } @@ -481,7 +459,7 @@ namespace ts { // add files referencing the removedFilePath, as changed files too const referencedByInfo = fileInfos.get(filePath); if (referencedByInfo) { - registerChangedFile(filePath as Path, referencedByInfo.fileName); + registerChangedFile(filePath as Path); } } }); @@ -495,37 +473,33 @@ namespace ts { ); } - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) { - return getAllEmittableFiles(program, sourceFile); + return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); } const compilerOptions = program.getCompilerOptions(); if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { - return singleFileResult; + return [sourceFile]; } // Now we need to if each file in the referencedBy list has a shape change as well. // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. - - const seenFileNamesMap = createMap(); - const setSeenFileName = (path: Path, sourceFile: SourceFile) => { - seenFileNamesMap.set(path, sourceFile && options.shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); - }; + const seenFileNamesMap = createMap(); // Start with the paths this file was referenced by const path = sourceFile.path; - setSeenFileName(path, sourceFile); + seenFileNamesMap.set(path, sourceFile); const queue = getReferencedByPaths(path); while (queue.length > 0) { const currentPath = queue.pop(); if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = program.getSourceFileByPath(currentPath); - if (currentSourceFile && updateShapeSignature(program, currentSourceFile, fileInfos.get(currentPath))) { + seenFileNamesMap.set(currentPath, currentSourceFile); + if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { queue.push(...getReferencedByPaths(currentPath)); } - setSeenFileName(currentPath, currentSourceFile); } } diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 6f61469197b21..53692fe1e0488 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -144,13 +144,14 @@ namespace ts { function compileWatchedProgram(host: DirectoryStructureHost, program: Program, builder: Builder) { // First get and report any syntactic errors. - let diagnostics = program.getSyntacticDiagnostics().slice(); + const diagnostics = program.getSyntacticDiagnostics().slice(); let reportSemanticDiagnostics = false; // If we didn't have any syntactic errors, then also try getting the global and // semantic errors. if (diagnostics.length === 0) { - diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); + addRange(diagnostics, program.getOptionsDiagnostics()); + addRange(diagnostics, program.getGlobalDiagnostics()); if (diagnostics.length === 0) { reportSemanticDiagnostics = true; @@ -162,7 +163,7 @@ namespace ts { let sourceMaps: SourceMapData[]; let emitSkipped: boolean; - const result = builder.emitChangedFiles(program); + const result = builder.emitChangedFiles(program, writeFile); if (result.length === 0) { emitSkipped = true; } @@ -171,14 +172,13 @@ namespace ts { if (emitOutput.emitSkipped) { emitSkipped = true; } - diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + addRange(diagnostics, emitOutput.diagnostics); sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - writeOutputFiles(emitOutput.outputFiles); } } if (reportSemanticDiagnostics) { - diagnostics = diagnostics.concat(builder.getSemanticDiagnostics(program)); + addRange(diagnostics, builder.getSemanticDiagnostics(program)); } return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic); @@ -191,31 +191,23 @@ namespace ts { } } - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError: (message: string) => void) { try { performance.mark("beforeIOWrite"); ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - host.writeFile(fileName, data, writeByteOrderMark); + host.writeFile(fileName, text, writeByteOrderMark); performance.mark("afterIOWrite"); performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + + if (emittedFiles) { + emittedFiles.push(fileName); + } } catch (e) { - return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); - } - } - - function writeOutputFiles(outputFiles: OutputFile[]) { - if (outputFiles) { - for (const outputFile of outputFiles) { - const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); - if (error) { - diagnostics.push(error); - } - if (emittedFiles) { - emittedFiles.push(outputFile.name); - } + if (onError) { + onError(e.message); } } } @@ -308,7 +300,7 @@ namespace ts { getCurrentDirectory() ); // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder({ getCanonicalFileName, getEmitOutput: getFileEmitOutput, computeHash, shouldEmitFile: () => true }); + const builder = createBuilder({ getCanonicalFileName, computeHash }); synchronizeProgram(); diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 26bf689401a8c..f39024d5a7cfd 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -1,5 +1,6 @@ /// +/* @internal */ namespace ts { /** * Updates the existing missing file watches with the new set of missing files after new program is created @@ -72,10 +73,7 @@ namespace ts { existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags)); } } -} -/* @internal */ -namespace ts { export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { return host.watchFile(file, cb); } diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index edd471e9ebe9e..bfdeb1a40678a 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -46,15 +46,14 @@ namespace ts { function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { const builder = createBuilder({ getCanonicalFileName: identity, - getEmitOutput: getFileEmitOutput, - computeHash: identity, - shouldEmitFile: returnTrue, + computeHash: identity }); return fileNames => { const program = getProgram(); builder.updateProgram(program); - const changedFiles = builder.emitChangedFiles(program); - assert.deepEqual(changedFileNames(changedFiles), fileNames); + const outputFileNames: string[] = []; + builder.emitChangedFiles(program, fileName => outputFileNames.push(fileName)); + assert.deepEqual(outputFileNames, fileNames); }; } @@ -63,11 +62,4 @@ namespace ts { updateProgramText(files, fileName, fileContent); }); } - - function changedFileNames(changedFiles: ReadonlyArray): string[] { - return changedFiles.map(f => { - assert.lengthOf(f.outputFiles, 1); - return f.outputFiles[0].name; - }); - } } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 2ee86d4255867..308021f0b0a84 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1105,6 +1105,64 @@ namespace ts.tscWatch { const outJs = "/a/out.js"; createWatchForOut(/*out*/ undefined, outJs); }); + + function verifyFilesEmittedOnce(useOutFile: boolean) { + const file1: FileOrFolder = { + path: "/a/b/output/AnotherDependency/file1.d.ts", + content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" + }; + const file2: FileOrFolder = { + path: "/a/b/dependencies/file2.d.ts", + content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" + }; + const file3: FileOrFolder = { + path: "/a/b/project/src/main.ts", + content: "namespace Main { export function fooBar() {} }" + }; + const file4: FileOrFolder = { + path: "/a/b/project/src/main2.ts", + content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" + }; + const configFile: FileOrFolder = { + path: "/a/b/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: useOutFile ? + { outFile: "../output/common.js", target: "es5" } : + { outDir: "../output", target: "es5" }, + files: [file1.path, file2.path, file3.path, file4.path] + }) + }; + const files = [file1, file2, file3, file4]; + const allfiles = files.concat(configFile); + const host = createWatchedSystem(allfiles); + const originalWriteFile = host.writeFile.bind(host); + const mapOfFilesWritten = createMap(); + host.writeFile = (p: string, content: string) => { + const count = mapOfFilesWritten.get(p); + mapOfFilesWritten.set(p, count ? count + 1 : 1); + return originalWriteFile(p, content); + }; + createWatchModeWithConfigFile(configFile.path, host); + if (useOutFile) { + // Only out file + assert.equal(mapOfFilesWritten.size, 1); + } + else { + // main.js and main2.js + assert.equal(mapOfFilesWritten.size, 2); + } + mapOfFilesWritten.forEach((value, key) => { + assert.equal(value, 1, "Key: " + key); + }); + } + + it("with --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ true); + }); + + it("without --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ false); + }); }); describe("tsc-watch emit for configured projects", () => { diff --git a/src/server/project.ts b/src/server/project.ts index 725ce725c3881..3cadfd6822499 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -443,31 +443,33 @@ namespace ts.server { if (!this.builder) { this.builder = createBuilder({ getCanonicalFileName: this.projectService.toCanonicalFileName, - getEmitOutput: (_program, sourceFile, emitOnlyDts, isDetailed) => - this.getFileEmitOutput(sourceFile, emitOnlyDts, isDetailed), - computeHash: data => - this.projectService.host.createHash(data), - shouldEmitFile: sourceFile => - !this.projectService.getScriptInfoForPath(sourceFile.path).isDynamicOrHasMixedContent() + computeHash: data => this.projectService.host.createHash(data) }); } } + private shouldEmitFile(scriptInfo: ScriptInfo) { + return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); + } + getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { if (!this.languageServiceEnabled) { return []; } this.updateGraph(); this.ensureBuilder(); - return this.builder.getFilesAffectedBy(this.program, scriptInfo.path); + return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), + sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } /** * Returns true if emit was conducted */ emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { - this.ensureBuilder(); - const { emitSkipped, outputFiles } = this.builder.emitFile(this.program, scriptInfo.path); + if (!this.languageServiceEnabled || !this.shouldEmitFile(scriptInfo)) { + return false; + } + const { emitSkipped, outputFiles } = this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(scriptInfo.fileName); if (!emitSkipped) { for (const outputFile of outputFiles) { const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory); @@ -593,13 +595,6 @@ namespace ts.server { }); } - private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) { - if (!this.languageServiceEnabled) { - return undefined; - } - return this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed); - } - getExcludedFiles(): ReadonlyArray { return emptyArray; } diff --git a/src/services/services.ts b/src/services/services.ts index 114692ebba2e5..2923acf404cef 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1511,12 +1511,12 @@ namespace ts { return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); } - function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean) { + function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean) { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, isDetailed, cancellationToken, customTransformers); + return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, cancellationToken, customTransformers); } // Signature help diff --git a/src/services/types.ts b/src/services/types.ts index 7ec559be92283..f9d725c2fa3e3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -287,7 +287,6 @@ namespace ts { getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed; getProgram(): Program; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 52132ae832f25..5f3d90e5ad0e5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3731,17 +3731,11 @@ declare namespace ts { outputFiles: OutputFile[]; emitSkipped: boolean; } - interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; } - function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -3953,7 +3947,6 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed; getProgram(): Program; dispose(): void; } @@ -7043,23 +7036,6 @@ declare namespace ts.server { isJavaScript(): boolean; } } -declare namespace ts { - /** - * Updates the existing missing file watches with the new set of missing files after new program is created - */ - function updateMissingFilePathsWatch(program: Program, missingFileWatches: Map, createMissingFileWatch: (missingFilePath: Path) => FileWatcher): void; - interface WildcardDirectoryWatcher { - watcher: FileWatcher; - flags: WatchDirectoryFlags; - } - /** - * Updates the existing wild card directory watches with the new set of wild card directories from the config file - * after new program is created because the config file was reloaded or program was created first time from the config file - * Note that there is no need to call this function when the program is updated with additional files without reloading config files, - * as wildcard directories wont change unless reloading config file - */ - function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher): void; -} declare namespace ts.server { interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions { projectRootPath: Path; @@ -7204,6 +7180,7 @@ declare namespace ts.server { getAllProjectErrors(): ReadonlyArray; getLanguageService(ensureSynchronized?: boolean): LanguageService; private ensureBuilder(); + private shouldEmitFile(scriptInfo); getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /** * Returns true if emit was conducted @@ -7222,7 +7199,6 @@ declare namespace ts.server { getRootFiles(): NormalizedPath[]; getRootScriptInfos(): ScriptInfo[]; getScriptInfos(): ScriptInfo[]; - private getFileEmitOutput(sourceFile, emitOnlyDtsFiles, isDetailed); getExcludedFiles(): ReadonlyArray; getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): NormalizedPath[]; hasConfigFile(configFilePath: NormalizedPath): boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 976d50942ae1b..d14def7d79156 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3678,17 +3678,11 @@ declare namespace ts { outputFiles: OutputFile[]; emitSkipped: boolean; } - interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; } - function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -3953,7 +3947,6 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed; getProgram(): Program; dispose(): void; }