diff --git a/Jakefile.js b/Jakefile.js index 0d6474fc571..0b3659e2051 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -129,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([ "textStorage.ts", "moduleResolution.ts", "tsconfigParsing.ts", + "builder.ts", "commandLineParsing.ts", "configurationExtension.ts", "convertCompilerOptionsFromJson.ts", diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 38969b85bac..6bb94ea2de7 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -93,12 +93,14 @@ namespace ts { signature: string; } - export function createBuilder( - getCanonicalFileName: (fileName: string) => string, - getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed, - computeHash: (data: string) => string, - shouldEmitFile: (sourceFile: SourceFile) => boolean - ): Builder { + 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 { let isModuleEmit: boolean | undefined; const fileInfos = createMap(); const semanticDiagnosticsPerFile = createMap>(); @@ -181,7 +183,7 @@ namespace ts { ensureProgramGraph(program); const sourceFile = program.getSourceFile(path); - const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; + const singleFileResult = sourceFile && options.shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; const info = fileInfos.get(path); if (!info || !updateShapeSignature(program, sourceFile, info)) { return singleFileResult; @@ -197,7 +199,7 @@ namespace ts { return { outputFiles: [], emitSkipped: true }; } - return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false); + return options.getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false); } function enumerateChangedFilesSet( @@ -220,21 +222,21 @@ namespace ts { onChangedFile: (fileName: string, path: Path) => void, onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void ) { - const seenFiles = createMap(); + const seenFiles = createMap(); enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => { if (!seenFiles.has(fileName)) { - seenFiles.set(fileName, sourceFile); + seenFiles.set(fileName, true); if (sourceFile) { // Any affected file shouldnt have the cached diagnostics semanticDiagnosticsPerFile.delete(sourceFile.path); - const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed; + 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, file); + seenFiles.set(file.fileName, true); } } } @@ -309,13 +311,13 @@ namespace ts { const prevSignature = info.signature; let latestSignature: string; if (sourceFile.isDeclarationFile) { - latestSignature = computeHash(sourceFile.text); + latestSignature = options.computeHash(sourceFile.text); info.signature = latestSignature; } else { - const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false); + const emitOutput = options.getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { - latestSignature = computeHash(emitOutput.outputFiles[0].text); + latestSignature = options.computeHash(emitOutput.outputFiles[0].text); info.signature = latestSignature; } else { @@ -352,7 +354,7 @@ namespace ts { // Handle triple slash references if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); + const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, options.getCanonicalFileName); addReferencedFile(referencedPath); } } @@ -365,7 +367,7 @@ namespace ts { } const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); + const typeFilePath = toPath(fileName, sourceFileDirectory, options.getCanonicalFileName); addReferencedFile(typeFilePath); }); } @@ -381,18 +383,26 @@ namespace ts { } /** - * Gets all the emittable files from the program + * Gets all the emittable files from the program. + * @param firstSourceFile This one will be emitted first. See https://github.com/Microsoft/TypeScript/issues/16888 */ - function getAllEmittableFiles(program: Program) { + 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) { - if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && shouldEmitFile(sourceFile)) { - result.push(sourceFile.fileName); + if (sourceFile !== firstSourceFile) { + add(sourceFile); } } return result; + + function add(sourceFile: SourceFile): void { + if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && options.shouldEmitFile(sourceFile)) { + result.push(sourceFile.fileName); + } + } } function getNonModuleEmitHandler(): EmitHandler { @@ -404,14 +414,14 @@ namespace ts { getFilesAffectedByUpdatedShape }; - function getFilesAffectedByUpdatedShape(program: Program, _sourceFile: SourceFile, singleFileResult: string[]): string[] { + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { 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 getAllEmittableFiles(program); + return getAllEmittableFiles(program, sourceFile); } } @@ -484,11 +494,11 @@ namespace ts { function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) { - return getAllEmittableFiles(program); + return getAllEmittableFiles(program, sourceFile); } - const options = program.getCompilerOptions(); - if (options && (options.isolatedModules || options.out || options.outFile)) { + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { return singleFileResult; } @@ -498,7 +508,7 @@ namespace ts { const seenFileNamesMap = createMap(); const setSeenFileName = (path: Path, sourceFile: SourceFile) => { - seenFileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); + seenFileNamesMap.set(path, sourceFile && options.shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); }; // Start with the paths this file was referenced by diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 2900737b09f..779efb62c46 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -308,7 +308,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, getFileEmitOutput, computeHash, _sourceFile => true); + const builder = createBuilder({ getCanonicalFileName, getEmitOutput: getFileEmitOutput, computeHash, shouldEmitFile: () => true }); synchronizeProgram(); diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 509fe550c07..a0c141f4f17 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -116,6 +116,7 @@ "./unittests/reuseProgramStructure.ts", "./unittests/moduleResolution.ts", "./unittests/tsconfigParsing.ts", + "./unittests/builder.ts", "./unittests/commandLineParsing.ts", "./unittests/configurationExtension.ts", "./unittests/convertCompilerOptionsFromJson.ts", diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts new file mode 100644 index 00000000000..edd471e9ebe --- /dev/null +++ b/src/harness/unittests/builder.ts @@ -0,0 +1,73 @@ +/// + +namespace ts { + describe("builder", () => { + it("emits dependent files", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, + ]; + + let program = newProgram(files, ["/a.ts"], {}); + const assertChanges = makeAssertChanges(() => program); + + assertChanges(["/c.js", "/b.js", "/a.js"]); + + program = updateProgramFile(program, "/a.ts", "//comment"); + assertChanges(["/a.js"]); + + program = updateProgramFile(program, "/b.ts", "export const b = c + 1;"); + assertChanges(["/b.js", "/a.js"]); + + program = updateProgramFile(program, "/c.ts", "export const c = 1;"); + assertChanges(["/c.js", "/b.js"]); + }); + + it("if emitting all files, emits the changed file first", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", "", "namespace A { export const x = 0; }") }, + { name: "/b.ts", text: SourceText.New("", "", "namespace B { export const x = 0; }") }, + ]; + + let program = newProgram(files, ["/a.ts", "/b.ts"], {}); + const assertChanges = makeAssertChanges(() => program); + + assertChanges(["/a.js", "/b.js"]); + + program = updateProgramFile(program, "/a.ts", "namespace A { export const x = 1; }"); + assertChanges(["/a.js", "/b.js"]); + + program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }"); + assertChanges(["/b.js", "/a.js"]); + }); + }); + + function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { + const builder = createBuilder({ + getCanonicalFileName: identity, + getEmitOutput: getFileEmitOutput, + computeHash: identity, + shouldEmitFile: returnTrue, + }); + return fileNames => { + const program = getProgram(); + builder.updateProgram(program); + const changedFiles = builder.emitChangedFiles(program); + assert.deepEqual(changedFileNames(changedFiles), fileNames); + }; + } + + function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts { + return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { + 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/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 5c2b9edd9d0..e0c0e6f80a8 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -15,12 +15,12 @@ namespace ts { sourceText?: SourceText; } - interface NamedSourceText { + export interface NamedSourceText { name: string; text: SourceText; } - interface ProgramWithSourceTexts extends Program { + export interface ProgramWithSourceTexts extends Program { sourceTexts?: ReadonlyArray; host: TestCompilerHost; } @@ -29,7 +29,7 @@ namespace ts { getTrace(): string[]; } - class SourceText implements IScriptSnapshot { + export class SourceText implements IScriptSnapshot { private fullText: string; constructor(private references: string, @@ -103,10 +103,11 @@ namespace ts { function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ScriptTarget) { const file = createSourceFile(fileName, sourceText.getFullText(), target); file.sourceText = sourceText; + file.version = "" + sourceText.getVersion(); return file; } - function createTestCompilerHost(texts: ReadonlyArray, target: ScriptTarget, oldProgram?: ProgramWithSourceTexts): TestCompilerHost { + export function createTestCompilerHost(texts: ReadonlyArray, target: ScriptTarget, oldProgram?: ProgramWithSourceTexts): TestCompilerHost { const files = arrayToMap(texts, t => t.name, t => { if (oldProgram) { let oldFile = oldProgram.getSourceFile(t.name); @@ -154,7 +155,7 @@ namespace ts { }; } - function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions): ProgramWithSourceTexts { + export function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions): ProgramWithSourceTexts { const host = createTestCompilerHost(texts, options.target); const program = createProgram(rootNames, options, host); program.sourceTexts = texts; @@ -162,7 +163,7 @@ namespace ts { return program; } - function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: ReadonlyArray, options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[]) { + export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: ReadonlyArray, options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[]) { if (!newTexts) { newTexts = (oldProgram).sourceTexts.slice(0); } @@ -174,7 +175,7 @@ namespace ts { return program; } - function updateProgramText(files: ReadonlyArray, fileName: string, newProgramText: string) { + export function updateProgramText(files: ReadonlyArray, fileName: string, newProgramText: string) { const file = find(files, f => f.name === fileName)!; file.text = file.text.updateProgram(newProgramText); } diff --git a/src/server/project.ts b/src/server/project.ts index 99473b44162..655f3ed0cfc 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -420,12 +420,15 @@ namespace ts.server { private ensureBuilder() { if (!this.builder) { - this.builder = createBuilder( - this.projectService.toCanonicalFileName, - (_program, sourceFile, emitOnlyDts, isDetailed) => this.getFileEmitOutput(sourceFile, emitOnlyDts, isDetailed), - data => this.projectService.host.createHash(data), - sourceFile => !this.projectService.getScriptInfoForPath(sourceFile.path).isDynamicOrHasMixedContent() - ); + 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() + }); } }