diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index 12936e5b2e860..6a7c54d61865b 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -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); }); } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9eabae70d2ed4..45b9351759c82 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -842,7 +842,7 @@ namespace ts { return deduplicateSorted(sort(array, comparer), equalityComparer || comparer); } - export function arrayIsEqualTo(array1: ReadonlyArray | undefined, array2: ReadonlyArray | undefined, equalityComparer: (a: T, b: T) => boolean = equateValues): boolean { + export function arrayIsEqualTo(array1: ReadonlyArray | undefined, array2: ReadonlyArray | 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; } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index ffe07c4cbc16d..82ae2aac38bf1 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -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 { diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 069aedff751d7..6c3681dbd5cf1 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -285,16 +285,17 @@ namespace ts { } function getOutFileOutputs(project: ParsedCommandLine): ReadonlyArray { - 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 { - if (project.options.outFile) { + if (project.options.outFile || project.options.out) { return getOutFileOutputs(project); } else { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e0d027ded221e..03a5d2b9ce7e0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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 | undefined; getResolvedProjectReferences(): (ResolvedProjectReference | undefined)[] | undefined; + /*@internal*/ getProjectReferenceRedirect(fileName: string): string | undefined; } /* @internal */ diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 7e8241f51b438..b85d5aa1c79a3 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -187,9 +187,10 @@ interface Array {}` } export function checkArray(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + 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 {}` 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) { diff --git a/src/testRunner/unittests/tsbuildWatchMode.ts b/src/testRunner/unittests/tsbuildWatchMode.ts index 672feaa565b12..f0745a373a023 100644 --- a/src/testRunner/unittests/tsbuildWatchMode.ts +++ b/src/testRunner/unittests/tsbuildWatchMode.ts @@ -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 = [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, 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)); + + function createSolution() { + const host = createWatchedSystem(allFiles, { currentDirectory: projectsLocation }); + const solutionBuilder = createSolutionBuilder(host, [`${project}/${SubProject.tests}`], {}); + return { host, solutionBuilder }; + } - createWatchOfConfigFile(tests[0].path, host); - checkOutputErrorsInitial(host, emptyArray); + 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) { + 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")]); + }); + }); }); }); } diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index f4df3df4721f1..c4b7d6adf444f 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -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;