Skip to content

Commit 0ac9658

Browse files
committed
Resolve project references transitively
1 parent 10edf6f commit 0ac9658

File tree

13 files changed

+163
-78
lines changed

13 files changed

+163
-78
lines changed

src/compiler/commandLineParser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,7 @@ namespace ts {
12931293

12941294
const result = parseJsonText(configFileName, configFileText);
12951295
const cwd = host.getCurrentDirectory();
1296+
result.path = toPath(configFileName, cwd, createGetCanonicalFileName(host.useCaseSensitiveFileNames));
12961297
return parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd));
12971298
}
12981299

src/compiler/program.ts

Lines changed: 102 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ namespace ts {
454454
return false;
455455
}
456456

457+
let seenResolvedRefs: ResolvedProjectReference[] | undefined;
458+
457459
// If project references dont match
458460
if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) {
459461
return false;
@@ -496,11 +498,29 @@ namespace ts {
496498
if (!projectReferenceIsEqualTo(oldRef, newRef)) {
497499
return false;
498500
}
499-
const oldResolvedRef = program!.getResolvedProjectReferences()![index];
501+
return resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef);
502+
}
503+
504+
function resolvedProjectReferenceUptoDate(oldResolvedRef: ResolvedProjectReference | undefined, oldRef: ProjectReference): boolean {
500505
if (oldResolvedRef) {
506+
if (contains(seenResolvedRefs, oldResolvedRef)) {
507+
// Assume true
508+
return true;
509+
}
510+
501511
// If sourceFile for the oldResolvedRef existed, check the version for uptodate
502-
return sourceFileVersionUptoDate(oldResolvedRef.sourceFile);
512+
if (!sourceFileVersionUptoDate(oldResolvedRef.sourceFile)) {
513+
return false;
514+
}
515+
516+
// Add to seen before checking the referenced paths of this config file
517+
(seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef);
518+
519+
// If child project references are upto date, this project reference is uptodate
520+
return !forEach(oldResolvedRef.references, (childResolvedRef, index) =>
521+
!resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index]));
503522
}
523+
504524
// In old program, not able to resolve project reference path,
505525
// so if config file doesnt exist, it is uptodate.
506526
return !fileExists(resolveProjectReferencePath(oldRef));
@@ -662,8 +682,8 @@ namespace ts {
662682
const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap<SourceFile>() : undefined;
663683

664684
// A parallel array to projectReferences storing the results of reading in the referenced tsconfig files
665-
let resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined;
666-
let projectReferenceRedirects: ParsedCommandLine[] | undefined;
685+
let resolvedProjectReferences: ReadonlyArray<ResolvedProjectReference | undefined> | undefined;
686+
let projectReferenceRedirects: Map<ResolvedProjectReference | false> | undefined;
667687

668688
const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
669689
const structuralIsReused = tryReuseStructureFromOldProgram();
@@ -672,16 +692,16 @@ namespace ts {
672692
processingOtherFiles = [];
673693

674694
if (projectReferences) {
675-
for (const ref of projectReferences) {
676-
const parsedRef = parseProjectReferenceConfigFile(ref);
677-
resolvedProjectReferences!.push(parsedRef);
695+
if (!resolvedProjectReferences) {
696+
resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile);
697+
}
698+
for (const parsedRef of resolvedProjectReferences) {
678699
if (parsedRef) {
679700
const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out;
680701
if (out) {
681702
const dtsOutfile = changeExtension(out, ".d.ts");
682703
processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined);
683704
}
684-
addProjectReferenceRedirects(parsedRef.commandLine);
685705
}
686706
}
687707
}
@@ -740,6 +760,12 @@ namespace ts {
740760

741761
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
742762
oldProgram = undefined;
763+
// Do not use our own command line for projectReferenceRedirects
764+
if (projectReferenceRedirects) {
765+
Debug.assert(!!options.configFilePath);
766+
const path = toPath(options.configFilePath!);
767+
projectReferenceRedirects.delete(path);
768+
}
743769

744770
program = {
745771
getRootFileNames: () => rootNames,
@@ -1001,6 +1027,48 @@ namespace ts {
10011027
}
10021028
}
10031029

1030+
function canReuseProjectReferences(
1031+
newProjectReferences: ReadonlyArray<ProjectReference> | undefined,
1032+
oldProjectReferences: ReadonlyArray<ProjectReference> | undefined,
1033+
oldResolvedReferences: ReadonlyArray<ResolvedProjectReference | undefined> | undefined): boolean {
1034+
// If array of references is changed, we cant resue old program
1035+
if (!arrayIsEqualTo(oldProjectReferences!, newProjectReferences, projectReferenceIsEqualTo)) {
1036+
return false;
1037+
}
1038+
1039+
// Check the json files for the project references
1040+
if (newProjectReferences) {
1041+
// Resolved project referenced should be array if projectReferences provided are array
1042+
Debug.assert(!!oldResolvedReferences);
1043+
for (let i = 0; i < newProjectReferences.length; i++) {
1044+
const oldRef = oldResolvedReferences![i];
1045+
const newRef = parseProjectReferenceConfigFile(newProjectReferences[i]);
1046+
if (oldRef) {
1047+
if (!newRef || newRef.sourceFile !== oldRef.sourceFile) {
1048+
// Resolved project reference has gone missing or changed
1049+
return false;
1050+
}
1051+
1052+
// If the transitive references can be reused then only this reference can be reused
1053+
if (!canReuseProjectReferences(newRef.commandLine.projectReferences, oldRef.commandLine.projectReferences, oldRef.references)) {
1054+
return false;
1055+
}
1056+
}
1057+
else {
1058+
// A previously-unresolved reference may be resolved now
1059+
if (newRef !== undefined) {
1060+
return false;
1061+
}
1062+
}
1063+
}
1064+
}
1065+
else {
1066+
// Resolved project referenced should be undefined if projectReferences is undefined
1067+
Debug.assert(!oldResolvedReferences);
1068+
}
1069+
return true;
1070+
}
1071+
10041072
function tryReuseStructureFromOldProgram(): StructureIsReused {
10051073
if (!oldProgram) {
10061074
return StructureIsReused.Not;
@@ -1026,39 +1094,10 @@ namespace ts {
10261094
}
10271095

10281096
// Check if any referenced project tsconfig files are different
1029-
1030-
// If array of references is changed, we cant resue old program
1031-
const oldProjectReferences = oldProgram.getProjectReferences();
1032-
if (!arrayIsEqualTo(oldProjectReferences!, projectReferences, projectReferenceIsEqualTo)) {
1097+
if (!canReuseProjectReferences(projectReferences, oldProgram.getProjectReferences(), oldProgram.getResolvedProjectReferences())) {
10331098
return oldProgram.structureIsReused = StructureIsReused.Not;
10341099
}
1035-
1036-
// Check the json files for the project references
1037-
const oldRefs = oldProgram.getResolvedProjectReferences();
1038-
if (projectReferences) {
1039-
// Resolved project referenced should be array if projectReferences provided are array
1040-
Debug.assert(!!oldRefs);
1041-
for (let i = 0; i < projectReferences.length; i++) {
1042-
const oldRef = oldRefs![i];
1043-
const newRef = parseProjectReferenceConfigFile(projectReferences[i]);
1044-
if (oldRef) {
1045-
if (!newRef || newRef.sourceFile !== oldRef.sourceFile) {
1046-
// Resolved project reference has gone missing or changed
1047-
return oldProgram.structureIsReused = StructureIsReused.Not;
1048-
}
1049-
}
1050-
else {
1051-
// A previously-unresolved reference may be resolved now
1052-
if (newRef !== undefined) {
1053-
return oldProgram.structureIsReused = StructureIsReused.Not;
1054-
}
1055-
}
1056-
}
1057-
}
1058-
else {
1059-
// Resolved project referenced should be undefined if projectReferences is undefined
1060-
Debug.assert(!oldRefs);
1061-
}
1100+
resolvedProjectReferences = oldProgram.getResolvedProjectReferences();
10621101

10631102
// check if program source files has changed in the way that can affect structure of the program
10641103
const newSourceFiles: SourceFile[] = [];
@@ -1248,14 +1287,6 @@ namespace ts {
12481287
fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile);
12491288
}
12501289
resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives();
1251-
resolvedProjectReferences = oldProgram.getResolvedProjectReferences();
1252-
if (resolvedProjectReferences) {
1253-
resolvedProjectReferences.forEach(ref => {
1254-
if (ref) {
1255-
addProjectReferenceRedirects(ref.commandLine);
1256-
}
1257-
});
1258-
}
12591290

12601291
sourceFileToPackageName = oldProgram.sourceFileToPackageName;
12611292
redirectTargetsMap = oldProgram.redirectTargetsMap;
@@ -2182,22 +2213,22 @@ namespace ts {
21822213

21832214
function getProjectReferenceRedirect(fileName: string): string | undefined {
21842215
// Ignore dts or any of the non ts files
2185-
if (!projectReferenceRedirects || fileExtensionIs(fileName, Extension.Dts) || !fileExtensionIsOneOf(fileName, supportedTSExtensions)) {
2216+
if (!resolvedProjectReferences || !resolvedProjectReferences.length || fileExtensionIs(fileName, Extension.Dts) || !fileExtensionIsOneOf(fileName, supportedTSExtensions)) {
21862217
return undefined;
21872218
}
21882219

21892220
// If this file is produced by a referenced project, we need to rewrite it to
21902221
// look in the output folder of the referenced project rather than the input
2191-
return forEach(projectReferenceRedirects, referencedProject => {
2222+
return forEachEntry(projectReferenceRedirects!, referencedProject => {
21922223
// not input file from the referenced project, ignore
2193-
if (!contains(referencedProject.fileNames, fileName, isSameFile)) {
2224+
if (!referencedProject || !contains(referencedProject.commandLine.fileNames, fileName, isSameFile)) {
21942225
return undefined;
21952226
}
21962227

2197-
const out = referencedProject.options.outFile || referencedProject.options.out;
2228+
const out = referencedProject.commandLine.options.outFile || referencedProject.commandLine.options.out;
21982229
return out ?
21992230
changeExtension(out, Extension.Dts) :
2200-
getOutputDeclarationFileName(fileName, referencedProject);
2231+
getOutputDeclarationFileName(fileName, referencedProject.commandLine);
22012232
});
22022233
}
22032234

@@ -2388,22 +2419,34 @@ namespace ts {
23882419
return allFilesBelongToPath;
23892420
}
23902421

2391-
function parseProjectReferenceConfigFile(ref: ProjectReference): { commandLine: ParsedCommandLine, sourceFile: SourceFile } | undefined {
2422+
function parseProjectReferenceConfigFile(ref: ProjectReference): ResolvedProjectReference | undefined {
2423+
if (!projectReferenceRedirects) {
2424+
projectReferenceRedirects = createMap<ResolvedProjectReference | false>();
2425+
}
2426+
23922427
// The actual filename (i.e. add "/tsconfig.json" if necessary)
23932428
const refPath = resolveProjectReferencePath(ref);
2429+
const sourceFilePath = toPath(refPath);
2430+
const fromCache = projectReferenceRedirects.get(sourceFilePath);
2431+
if (fromCache !== undefined) {
2432+
return fromCache || undefined;
2433+
}
2434+
23942435
// An absolute path pointing to the containing directory of the config file
23952436
const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory());
23962437
const sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined;
23972438
if (sourceFile === undefined) {
2439+
projectReferenceRedirects.set(sourceFilePath, false);
23982440
return undefined;
23992441
}
2400-
sourceFile.path = toPath(refPath);
2442+
sourceFile.path = sourceFilePath;
24012443
const commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath);
2402-
return { commandLine, sourceFile };
2403-
}
2404-
2405-
function addProjectReferenceRedirects(referencedProject: ParsedCommandLine) {
2406-
(projectReferenceRedirects || (projectReferenceRedirects = [])).push(referencedProject);
2444+
const resolvedRef: ResolvedProjectReference = { commandLine, sourceFile };
2445+
projectReferenceRedirects.set(sourceFilePath, resolvedRef);
2446+
if (commandLine.projectReferences) {
2447+
resolvedRef.references = commandLine.projectReferences.map(parseProjectReferenceConfigFile);
2448+
}
2449+
return resolvedRef;
24072450
}
24082451

24092452
function verifyCompilerOptions() {

src/compiler/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2829,7 +2829,7 @@ namespace ts {
28292829
/* @internal */ getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined;
28302830

28312831
getProjectReferences(): ReadonlyArray<ProjectReference> | undefined;
2832-
getResolvedProjectReferences(): (ResolvedProjectReference | undefined)[] | undefined;
2832+
getResolvedProjectReferences(): ReadonlyArray<ResolvedProjectReference | undefined> | undefined;
28332833
/*@internal*/ getProjectReferenceRedirect(fileName: string): string | undefined;
28342834
}
28352835

@@ -2839,6 +2839,7 @@ namespace ts {
28392839
export interface ResolvedProjectReference {
28402840
commandLine: ParsedCommandLine;
28412841
sourceFile: SourceFile;
2842+
references?: ReadonlyArray<ResolvedProjectReference | undefined>;
28422843
}
28432844

28442845
/* @internal */

src/server/project.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,8 @@ namespace ts.server {
281281
return this.projectStateVersion.toString();
282282
}
283283

284-
getProjectReferences(): ReadonlyArray<ProjectReference> {
285-
return emptyArray;
284+
getProjectReferences(): ReadonlyArray<ProjectReference> | undefined {
285+
return undefined;
286286
}
287287

288288
getScriptFileNames() {
@@ -1391,8 +1391,8 @@ namespace ts.server {
13911391
return asNormalizedPath(this.getProjectName());
13921392
}
13931393

1394-
getProjectReferences(): ReadonlyArray<ProjectReference> {
1395-
return this.projectReferences || emptyArray;
1394+
getProjectReferences(): ReadonlyArray<ProjectReference> | undefined {
1395+
return this.projectReferences;
13961396
}
13971397

13981398
updateReferences(refs: ReadonlyArray<ProjectReference> | undefined) {

src/testRunner/unittests/tsbuild.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -321,16 +321,6 @@ export class cNew {}`);
321321
"/src/tests/index.ts"
322322
]);
323323

324-
function getLibs() {
325-
return [
326-
"/lib/lib.d.ts",
327-
"/lib/lib.es5.d.ts",
328-
"/lib/lib.dom.d.ts",
329-
"/lib/lib.webworker.importscripts.d.ts",
330-
"/lib/lib.scripthost.d.ts"
331-
];
332-
}
333-
334324
function getCoreOutputs() {
335325
return [
336326
"/src/core/index.d.ts",
@@ -376,6 +366,36 @@ export class cNew {}`);
376366
}
377367
});
378368
});
369+
370+
describe("tsbuild - when project reference is referenced transitively", () => {
371+
const projFs = loadProjectFromDisk("tests/projects/transitiveReferences");
372+
const allExpectedOutputs = [
373+
"/src/a.js", "/src/a.d.ts",
374+
"/src/b.js", "/src/b.d.ts",
375+
"/src/c.js"
376+
];
377+
it("verify that it builds correctly", () => {
378+
const fs = projFs.shadow();
379+
const host = new fakes.SolutionBuilderHost(fs);
380+
const builder = createSolutionBuilder(host, ["/src/tsconfig.c.json"], { listFiles: true });
381+
builder.buildAllProjects();
382+
host.assertDiagnosticMessages(/*empty*/);
383+
for (const output of allExpectedOutputs) {
384+
assert(fs.existsSync(output), `Expect file ${output} to exist`);
385+
}
386+
assert.deepEqual(host.traces, [
387+
...getLibs(),
388+
"/src/a.ts",
389+
...getLibs(),
390+
"/src/a.d.ts",
391+
"/src/b.ts",
392+
...getLibs(),
393+
"/src/a.d.ts",
394+
"/src/b.d.ts",
395+
"/src/c.ts"
396+
]);
397+
});
398+
});
379399
}
380400

381401
export namespace OutFile {
@@ -584,4 +604,14 @@ export class cNew {}`);
584604
fs.makeReadonly();
585605
return fs;
586606
}
607+
608+
function getLibs() {
609+
return [
610+
"/lib/lib.d.ts",
611+
"/lib/lib.es5.d.ts",
612+
"/lib/lib.dom.d.ts",
613+
"/lib/lib.webworker.importscripts.d.ts",
614+
"/lib/lib.scripthost.d.ts"
615+
];
616+
}
587617
}

0 commit comments

Comments
 (0)