Merge pull request #27155 from Microsoft/fixIncrementalWatchProjectWithReferences

Fix incremental watch when project built has project references
This commit is contained in:
Sheetal Nandi
2018-09-17 15:34:40 -07:00
committed by GitHub
8 changed files with 190 additions and 35 deletions
+10 -3
View File
@@ -88,7 +88,7 @@ namespace ts.BuilderState {
function getReferencedFileFromImportedModuleSymbol(symbol: Symbol) {
if (symbol.declarations && symbol.declarations[0]) {
const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]);
return declarationSourceFile && declarationSourceFile.path;
return declarationSourceFile && (declarationSourceFile.resolvedPath || declarationSourceFile.path);
}
}
@@ -100,6 +100,13 @@ namespace ts.BuilderState {
return symbol && getReferencedFileFromImportedModuleSymbol(symbol);
}
/**
* Gets the path to reference file from file name, it could be resolvedPath if present otherwise path
*/
function getReferencedFileFromFileName(program: Program, fileName: string, sourceFileDirectory: Path, getCanonicalFileName: GetCanonicalFileName): Path {
return toPath(program.getProjectReferenceRedirect(fileName) || fileName, sourceFileDirectory, getCanonicalFileName);
}
/**
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
*/
@@ -123,7 +130,7 @@ namespace ts.BuilderState {
// 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 = getReferencedFileFromFileName(program, referencedFile.fileName, sourceFileDirectory, getCanonicalFileName);
addReferencedFile(referencedPath);
}
}
@@ -136,7 +143,7 @@ namespace ts.BuilderState {
}
const fileName = resolvedTypeReferenceDirective.resolvedFileName!; // TODO: GH#18217
const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName);
const typeFilePath = getReferencedFileFromFileName(program, fileName, sourceFileDirectory, getCanonicalFileName);
addReferencedFile(typeFilePath);
});
}
+2 -2
View File
@@ -842,7 +842,7 @@ namespace ts {
return deduplicateSorted(sort(array, comparer), equalityComparer || comparer);
}
export function arrayIsEqualTo<T>(array1: ReadonlyArray<T> | undefined, array2: ReadonlyArray<T> | undefined, equalityComparer: (a: T, b: T) => boolean = equateValues): boolean {
export function arrayIsEqualTo<T>(array1: ReadonlyArray<T> | undefined, array2: ReadonlyArray<T> | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean {
if (!array1 || !array2) {
return array1 === array2;
}
@@ -852,7 +852,7 @@ namespace ts {
}
for (let i = 0; i < array1.length; i++) {
if (!equalityComparer(array1[i], array2[i])) {
if (!equalityComparer(array1[i], array2[i], i)) {
return false;
}
}
+43 -14
View File
@@ -455,7 +455,7 @@ namespace ts {
}
// If project references dont match
if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences)) {
if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) {
return false;
}
@@ -483,9 +483,27 @@ namespace ts {
return true;
function sourceFileNotUptoDate(sourceFile: SourceFile): boolean {
return sourceFile.version !== getSourceVersion(sourceFile.path) ||
hasInvalidatedResolution(sourceFile.path);
function sourceFileNotUptoDate(sourceFile: SourceFile) {
return !sourceFileVersionUptoDate(sourceFile) ||
hasInvalidatedResolution(sourceFile.resolvedPath || sourceFile.path);
}
function sourceFileVersionUptoDate(sourceFile: SourceFile) {
return sourceFile.version === getSourceVersion(sourceFile.resolvedPath || sourceFile.path);
}
function projectReferenceUptoDate(oldRef: ProjectReference, newRef: ProjectReference, index: number) {
if (!projectReferenceIsEqualTo(oldRef, newRef)) {
return false;
}
const oldResolvedRef = program!.getResolvedProjectReferences()![index];
if (oldResolvedRef) {
// If sourceFile for the oldResolvedRef existed, check the version for uptodate
return sourceFileVersionUptoDate(oldResolvedRef.sourceFile);
}
// In old program, not able to resolve project reference path,
// so if config file doesnt exist, it is uptodate.
return !fileExists(resolveProjectReferencePath(oldRef));
}
}
@@ -658,8 +676,9 @@ namespace ts {
const parsedRef = parseProjectReferenceConfigFile(ref);
resolvedProjectReferences!.push(parsedRef);
if (parsedRef) {
if (parsedRef.commandLine.options.outFile) {
const dtsOutfile = changeExtension(parsedRef.commandLine.options.outFile, ".d.ts");
const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out;
if (out) {
const dtsOutfile = changeExtension(out, ".d.ts");
processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined);
}
addProjectReferenceRedirects(parsedRef.commandLine, projectReferenceRedirects);
@@ -758,7 +777,8 @@ namespace ts {
getConfigFileParsingDiagnostics,
getResolvedModuleWithFailedLookupLocationsFromCache,
getProjectReferences,
getResolvedProjectReferences
getResolvedProjectReferences,
getProjectReferenceRedirect
};
verifyCompilerOptions();
@@ -1225,6 +1245,13 @@ namespace ts {
}
resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives();
resolvedProjectReferences = oldProgram.getResolvedProjectReferences();
if (resolvedProjectReferences) {
resolvedProjectReferences.forEach(ref => {
if (ref) {
addProjectReferenceRedirects(ref.commandLine, projectReferenceRedirects);
}
});
}
sourceFileToPackageName = oldProgram.sourceFileToPackageName;
redirectTargetsMap = oldProgram.redirectTargetsMap;
@@ -1280,12 +1307,13 @@ namespace ts {
const ref = projectReferences[i];
const resolvedRefOpts = resolvedProjectReferences![i]!.commandLine;
if (ref.prepend && resolvedRefOpts && resolvedRefOpts.options) {
const out = resolvedRefOpts.options.outFile || resolvedRefOpts.options.out;
// Upstream project didn't have outFile set -- skip (error will have been issued earlier)
if (!resolvedRefOpts.options.outFile) continue;
if (!out) continue;
const dtsFilename = changeExtension(resolvedRefOpts.options.outFile, ".d.ts");
const js = host.readFile(resolvedRefOpts.options.outFile) || `/* Input file ${resolvedRefOpts.options.outFile} was missing */\r\n`;
const jsMapPath = resolvedRefOpts.options.outFile + ".map"; // TODO: try to read sourceMappingUrl comment from the file
const dtsFilename = changeExtension(out, ".d.ts");
const js = host.readFile(out) || `/* Input file ${out} was missing */\r\n`;
const jsMapPath = out + ".map"; // TODO: try to read sourceMappingUrl comment from the file
const jsMap = host.readFile(jsMapPath);
const dts = host.readFile(dtsFilename) || `/* Input file ${dtsFilename} was missing */\r\n`;
const dtsMapPath = dtsFilename + ".map";
@@ -2427,9 +2455,10 @@ namespace ts {
createDiagnosticForReference(i, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path);
}
if (ref.prepend) {
if (resolvedRefOpts.outFile) {
if (!host.fileExists(resolvedRefOpts.outFile)) {
createDiagnosticForReference(i, Diagnostics.Output_file_0_from_project_1_does_not_exist, resolvedRefOpts.outFile, ref.path);
const out = resolvedRefOpts.outFile || resolvedRefOpts.out;
if (out) {
if (!host.fileExists(out)) {
createDiagnosticForReference(i, Diagnostics.Output_file_0_from_project_1_does_not_exist, out, ref.path);
}
}
else {
+7 -6
View File
@@ -285,16 +285,17 @@ namespace ts {
}
function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray<string> {
if (!project.options.outFile) {
const out = project.options.outFile || project.options.out;
if (!out) {
return Debug.fail("outFile must be set");
}
const outputs: string[] = [];
outputs.push(project.options.outFile);
outputs.push(out);
if (project.options.sourceMap) {
outputs.push(`${project.options.outFile}.map`);
outputs.push(`${out}.map`);
}
if (getEmitDeclarations(project.options)) {
const dts = changeExtension(project.options.outFile, Extension.Dts);
const dts = changeExtension(out, Extension.Dts);
outputs.push(dts);
if (project.options.declarationMap) {
outputs.push(`${dts}.map`);
@@ -862,7 +863,7 @@ namespace ts {
if (buildProject) {
buildSingleInvalidatedProject(buildProject.project, buildProject.reloadLevel);
if (hasPendingInvalidatedProjects()) {
if (!timerToBuildInvalidatedProject) {
if (options.watch && !timerToBuildInvalidatedProject) {
scheduleBuildInvalidatedProject();
}
}
@@ -1248,7 +1249,7 @@ namespace ts {
}
export function getAllProjectOutputs(project: ParsedCommandLine): ReadonlyArray<string> {
if (project.options.outFile) {
if (project.options.outFile || project.options.out) {
return getOutFileOutputs(project);
}
else {
+6
View File
@@ -2551,6 +2551,11 @@ namespace ts {
fileName: string;
/* @internal */ path: Path;
text: string;
/** Resolved path can be different from path property,
* when file is included through project reference is mapped to its output instead of source
* in that case resolvedPath = path to output file
* path = input file's path
*/
/* @internal */ resolvedPath: Path;
/**
@@ -2819,6 +2824,7 @@ namespace ts {
getProjectReferences(): ReadonlyArray<ProjectReference> | undefined;
getResolvedProjectReferences(): (ResolvedProjectReference | undefined)[] | undefined;
/*@internal*/ getProjectReferenceRedirect(fileName: string): string | undefined;
}
/* @internal */
+8 -2
View File
@@ -187,9 +187,10 @@ interface Array<T> {}`
}
export function checkArray(caption: string, actual: ReadonlyArray<string>, expected: ReadonlyArray<string>) {
checkMapKeys(caption, arrayToMap(actual, identity), expected);
assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`);
for (const f of expected) {
assert.equal(true, contains(actual, f), `${caption}: expected to find ${f} in ${actual}`);
assert.isTrue(contains(actual, f), `${caption}: expected to find ${f} in ${actual}`);
}
}
@@ -934,7 +935,12 @@ interface Array<T> {}`
const folder = this.fs.get(base) as FsFolder;
Debug.assert(isFsFolder(folder));
this.addFileOrFolderInFolder(folder, file);
if (!this.fs.has(file.path)) {
this.addFileOrFolderInFolder(folder, file);
}
else {
this.modifyFile(path, content);
}
}
write(message: string) {
+107 -8
View File
@@ -34,6 +34,10 @@ namespace ts.tscWatch {
return `${projectPath(subProject)}/${baseFileName.toLowerCase()}`;
}
function projectFileName(subProject: SubProject, baseFileName: string) {
return `${projectPath(subProject)}/${baseFileName}`;
}
function projectFile(subProject: SubProject, baseFileName: string): File {
return {
path: projectFilePath(subProject, baseFileName),
@@ -94,6 +98,7 @@ namespace ts.tscWatch {
const ui = subProjectFiles(SubProject.ui);
const allFiles: ReadonlyArray<File> = [libFile, ...core, ...logic, ...tests, ...ui];
const testProjectExpectedWatchedFiles = [core[0], core[1], core[2], ...logic, ...tests].map(f => f.path);
const testProjectExpectedWatchedDirectoriesRecursive = [projectPath(SubProject.core), projectPath(SubProject.logic)];
function createSolutionInWatchMode(allFiles: ReadonlyArray<File>, defaultOptions?: BuildOptions, disableConsoleClears?: boolean) {
const host = createWatchedSystem(allFiles, { currentDirectory: projectsLocation });
@@ -110,7 +115,7 @@ namespace ts.tscWatch {
function verifyWatches(host: WatchedSystem) {
checkWatchedFiles(host, testProjectExpectedWatchedFiles);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true);
checkWatchedDirectories(host, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true);
}
it("creates solution in watch mode", () => {
@@ -161,7 +166,7 @@ namespace ts.tscWatch {
function verifyWatches() {
checkWatchedFiles(host, additionalFiles ? testProjectExpectedWatchedFiles.concat(newFile.path) : testProjectExpectedWatchedFiles);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true);
checkWatchedDirectories(host, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true);
}
}
@@ -347,7 +352,7 @@ function myFunc() { return 100; }`);
function verifyWatches() {
checkWatchedFiles(host, projectFiles.map(f => f.path));
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true);
checkWatchedDirectories(host, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true);
}
});
@@ -389,12 +394,106 @@ let x: string = 10;`);
});
});
it("tsc-watch works with project references", () => {
// Build the composite project
const host = createSolutionInWatchMode(allFiles);
describe("tsc-watch works with project references", () => {
const coreIndexDts = projectFileName(SubProject.core, "index.d.ts");
const coreAnotherModuleDts = projectFileName(SubProject.core, "anotherModule.d.ts");
const logicIndexDts = projectFileName(SubProject.logic, "index.d.ts");
const expectedWatchedFiles = [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase()));
const expectedWatchedDirectoriesRecursive = projectSystem.getTypeRootsFromLocation(projectPath(SubProject.tests));
createWatchOfConfigFile(tests[0].path, host);
checkOutputErrorsInitial(host, emptyArray);
function createSolution() {
const host = createWatchedSystem(allFiles, { currentDirectory: projectsLocation });
const solutionBuilder = createSolutionBuilder(host, [`${project}/${SubProject.tests}`], {});
return { host, solutionBuilder };
}
function createBuiltSolution() {
const result = createSolution();
const { host, solutionBuilder } = result;
solutionBuilder.buildAllProjects();
const outputFileStamps = getOutputFileStamps(host);
for (const stamp of outputFileStamps) {
assert.isDefined(stamp[1], `${stamp[0]} expected to be present`);
}
return result;
}
function verifyWatches(host: WatchedSystem) {
checkWatchedFilesDetailed(host, expectedWatchedFiles, 1);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, 1, /*recursive*/ true);
}
function createSolutionAndWatchMode() {
// Build the composite project
const { host, solutionBuilder } = createBuiltSolution();
// Build in watch mode
const watch = createWatchOfConfigFileReturningBuilder(tests[0].path, host);
checkOutputErrorsInitial(host, emptyArray);
return { host, solutionBuilder, watch };
}
function verifyDependencies(watch: () => BuilderProgram, filePath: string, expected: ReadonlyArray<string>) {
checkArray(`${filePath} dependencies`, watch().getAllDependencies(watch().getSourceFile(filePath)!), expected);
}
describe("invoking when references are already built", () => {
it("verifies dependencies and watches", () => {
const { host, watch } = createSolutionAndWatchMode();
verifyWatches(host);
verifyDependencies(watch, coreIndexDts, [coreIndexDts]);
verifyDependencies(watch, coreAnotherModuleDts, [coreAnotherModuleDts]);
verifyDependencies(watch, logicIndexDts, [logicIndexDts, coreAnotherModuleDts]);
verifyDependencies(watch, tests[1].path, [tests[1].path, coreAnotherModuleDts, logicIndexDts, coreAnotherModuleDts]);
});
it("local edit in ts file, result in watch compilation because logic.d.ts is written", () => {
const { host, solutionBuilder, watch } = createSolutionAndWatchMode();
host.writeFile(logic[1].path, `${logic[1].content}
function foo() {
}`);
solutionBuilder.invalidateProject(`${project}/${SubProject.logic}`);
solutionBuilder.buildInvalidatedProject();
host.checkTimeoutQueueLengthAndRun(1); // not ideal, but currently because of d.ts but no new file is written
checkOutputErrorsIncremental(host, emptyArray);
checkProgramActualFiles(watch().getProgram(), [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]);
});
it("non local edit in ts file, rebuilds in watch compilation", () => {
const { host, solutionBuilder, watch } = createSolutionAndWatchMode();
host.writeFile(logic[1].path, `${logic[1].content}
export function gfoo() {
}`);
solutionBuilder.invalidateProject(logic[0].path);
solutionBuilder.buildInvalidatedProject();
host.checkTimeoutQueueLengthAndRun(1);
checkOutputErrorsIncremental(host, emptyArray);
checkProgramActualFiles(watch().getProgram(), [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]);
});
it("change in project reference config file builds correctly", () => {
const { host, solutionBuilder, watch } = createSolutionAndWatchMode();
host.writeFile(logic[0].path, JSON.stringify({
compilerOptions: { composite: true, declaration: true, declarationDir: "decls" },
references: [{ path: "../core" }]
}));
solutionBuilder.invalidateProject(logic[0].path, ConfigFileProgramReloadLevel.Full);
solutionBuilder.buildInvalidatedProject();
host.checkTimeoutQueueLengthAndRun(1);
checkOutputErrorsIncremental(host, [
// TODO: #26036
// The error is reported in d.ts file because it isnt resolved from ts file path, but is resolved from .d.ts file
"sample1/logic/decls/index.d.ts(2,22): error TS2307: Cannot find module '../core/anotherModule'.\n"
]);
checkProgramActualFiles(watch().getProgram(), [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")]);
});
});
});
});
}
+7
View File
@@ -20,6 +20,13 @@ namespace ts.tscWatch {
checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles);
}
export function createWatchOfConfigFileReturningBuilder(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) {
const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host);
compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation;
const watch = createWatchProgram(compilerHost);
return () => watch.getCurrentProgram();
}
export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) {
const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, {}, host);
compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation;