Skip to content

Commit a545ab1

Browse files
authored
Cache resolved project references and watch wild card directories from them to update them (microsoft#42929)
* Test for not watchiong referenced projects fileNames and invalidating it * Add watching wild card directories and caching parsed command line for projects so that its shared * Handle config file watching and commandline cache together * Watch extended files for commndline cache instead of project * Use extended config cache now that we are watching extended config files * Structure for getParsedCommandLine from the LS * Adding some more skeleton with todos * getParsedCommandLine on WatchCompilerHost * Tests for Watch, LS scenarios * Handle getParsedCommandLine so we are looking at all things for referenced * Cleanup and commenting * Test for transitive references with tsc-watch * Cache parsed command line even if host implements getParsedCommandLine * Cleanup * Cleanup * Some tests to verify exclude from referenced project doesnt trigger the update * Baseline when program is same * Test for incremental scenario * Tests for output from referenced project * Comments
1 parent a26acf4 commit a545ab1

39 files changed

+4649
-586
lines changed

src/compiler/commandLineParser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1564,7 +1564,7 @@ namespace ts {
15641564
*/
15651565
export function getParsedCommandLineOfConfigFile(
15661566
configFileName: string,
1567-
optionsToExtend: CompilerOptions,
1567+
optionsToExtend: CompilerOptions | undefined,
15681568
host: ParseConfigFileHost,
15691569
extendedConfigCache?: Map<ExtendedConfigCacheEntry>,
15701570
watchOptionsToExtend?: WatchOptions,

src/compiler/program.ts

+48-38
Original file line numberDiff line numberDiff line change
@@ -656,46 +656,33 @@ namespace ts {
656656
fileExists: (fileName: string) => boolean,
657657
hasInvalidatedResolution: HasInvalidatedResolution,
658658
hasChangedAutomaticTypeDirectiveNames: HasChangedAutomaticTypeDirectiveNames | undefined,
659+
getParsedCommandLine: (fileName: string) => ParsedCommandLine | undefined,
659660
projectReferences: readonly ProjectReference[] | undefined
660661
): boolean {
661662
// If we haven't created a program yet or have changed automatic type directives, then it is not up-to-date
662-
if (!program || hasChangedAutomaticTypeDirectiveNames?.()) {
663-
return false;
664-
}
663+
if (!program || hasChangedAutomaticTypeDirectiveNames?.()) return false;
665664

666665
// If root file names don't match
667-
if (!arrayIsEqualTo(program.getRootFileNames(), rootFileNames)) {
668-
return false;
669-
}
666+
if (!arrayIsEqualTo(program.getRootFileNames(), rootFileNames)) return false;
670667

671668
let seenResolvedRefs: ResolvedProjectReference[] | undefined;
672669

673670
// If project references don't match
674-
if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) {
675-
return false;
676-
}
671+
if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) return false;
677672

678673
// If any file is not up-to-date, then the whole program is not up-to-date
679-
if (program.getSourceFiles().some(sourceFileNotUptoDate)) {
680-
return false;
681-
}
674+
if (program.getSourceFiles().some(sourceFileNotUptoDate)) return false;
682675

683676
// If any of the missing file paths are now created
684-
if (program.getMissingFilePaths().some(fileExists)) {
685-
return false;
686-
}
677+
if (program.getMissingFilePaths().some(fileExists)) return false;
687678

688679
const currentOptions = program.getCompilerOptions();
689680
// If the compilation settings do no match, then the program is not up-to-date
690-
if (!compareDataObjects(currentOptions, newOptions)) {
691-
return false;
692-
}
681+
if (!compareDataObjects(currentOptions, newOptions)) return false;
693682

694683
// If everything matches but the text of config file is changed,
695684
// error locations can change for program options, so update the program
696-
if (currentOptions.configFile && newOptions.configFile) {
697-
return currentOptions.configFile.text === newOptions.configFile.text;
698-
}
685+
if (currentOptions.configFile && newOptions.configFile) return currentOptions.configFile.text === newOptions.configFile.text;
699686

700687
return true;
701688

@@ -709,23 +696,26 @@ namespace ts {
709696
}
710697

711698
function projectReferenceUptoDate(oldRef: ProjectReference, newRef: ProjectReference, index: number) {
712-
if (!projectReferenceIsEqualTo(oldRef, newRef)) {
713-
return false;
714-
}
715-
return resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef);
699+
return projectReferenceIsEqualTo(oldRef, newRef) &&
700+
resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef);
716701
}
717702

718703
function resolvedProjectReferenceUptoDate(oldResolvedRef: ResolvedProjectReference | undefined, oldRef: ProjectReference): boolean {
719704
if (oldResolvedRef) {
720-
if (contains(seenResolvedRefs, oldResolvedRef)) {
721705
// Assume true
722-
return true;
723-
}
706+
if (contains(seenResolvedRefs, oldResolvedRef)) return true;
724707

725-
// If sourceFile for the oldResolvedRef existed, check the version for uptodate
726-
if (!sourceFileVersionUptoDate(oldResolvedRef.sourceFile)) {
727-
return false;
728-
}
708+
const refPath = resolveProjectReferencePath(oldRef);
709+
const newParsedCommandLine = getParsedCommandLine(refPath);
710+
711+
// Check if config file exists
712+
if (!newParsedCommandLine) return false;
713+
714+
// If change in source file
715+
if (oldResolvedRef.commandLine.options.configFile !== newParsedCommandLine.options.configFile) return false;
716+
717+
// check file names
718+
if (!arrayIsEqualTo(oldResolvedRef.commandLine.fileNames, newParsedCommandLine.fileNames)) return false;
729719

730720
// Add to seen before checking the referenced paths of this config file
731721
(seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef);
@@ -737,7 +727,8 @@ namespace ts {
737727

738728
// In old program, not able to resolve project reference path,
739729
// so if config file doesnt exist, it is uptodate.
740-
return !fileExists(resolveProjectReferencePath(oldRef));
730+
const refPath = resolveProjectReferencePath(oldRef);
731+
return !getParsedCommandLine(refPath);
741732
}
742733
}
743734

@@ -1021,11 +1012,28 @@ namespace ts {
10211012
host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions(), !!getSourceFileByPath(oldSourceFile.path));
10221013
}
10231014
}
1024-
oldProgram.forEachResolvedProjectReference(resolvedProjectReference => {
1025-
if (!getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) {
1026-
host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false);
1015+
if (!host.getParsedCommandLine) {
1016+
oldProgram.forEachResolvedProjectReference(resolvedProjectReference => {
1017+
if (!getResolvedProjectReferenceByPath(resolvedProjectReference.sourceFile.path)) {
1018+
host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false);
1019+
}
1020+
});
1021+
}
1022+
}
1023+
1024+
// Release commandlines that new program does not use
1025+
if (oldProgram && host.onReleaseParsedCommandLine) {
1026+
forEachProjectReference(
1027+
oldProgram.getProjectReferences(),
1028+
oldProgram.getResolvedProjectReferences(),
1029+
(oldResolvedRef, parent, index) => {
1030+
const oldReference = parent?.commandLine.projectReferences![index] || oldProgram!.getProjectReferences()![index];
1031+
const oldRefPath = resolveProjectReferencePath(oldReference);
1032+
if (!projectReferenceRedirects?.has(toPath(oldRefPath))) {
1033+
host.onReleaseParsedCommandLine!(oldRefPath, oldResolvedRef, oldProgram!.getCompilerOptions());
1034+
}
10271035
}
1028-
});
1036+
);
10291037
}
10301038

10311039
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
@@ -1367,7 +1375,9 @@ namespace ts {
13671375
const newResolvedRef = parseProjectReferenceConfigFile(newRef);
13681376
if (oldResolvedRef) {
13691377
// Resolved project reference has gone missing or changed
1370-
return !newResolvedRef || newResolvedRef.sourceFile !== oldResolvedRef.sourceFile;
1378+
return !newResolvedRef ||
1379+
newResolvedRef.sourceFile !== oldResolvedRef.sourceFile ||
1380+
!arrayIsEqualTo(oldResolvedRef.commandLine.fileNames, newResolvedRef.commandLine.fileNames);
13711381
}
13721382
else {
13731383
// A previously-unresolved reference may be resolved now

src/compiler/tsbuildPublic.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ namespace ts {
356356
return !!(entry as ParsedCommandLine).options;
357357
}
358358

359+
function getCachedParsedConfigFile(state: SolutionBuilderState, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined {
360+
const value = state.configFileCache.get(configFilePath);
361+
return value && isParsedCommandLine(value) ? value : undefined;
362+
}
363+
359364
function parseConfigFile(state: SolutionBuilderState, configFileName: ResolvedConfigFileName, configFilePath: ResolvedConfigFilePath): ParsedCommandLine | undefined {
360365
const { configFileCache } = state;
361366
const value = configFileCache.get(configFilePath);
@@ -1804,7 +1809,7 @@ namespace ts {
18041809
function watchExtendedConfigFiles(state: SolutionBuilderState, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
18051810
updateSharedExtendedConfigFileWatcher(
18061811
resolvedPath,
1807-
parsed,
1812+
parsed?.options,
18081813
state.allWatchedExtendedConfigFiles,
18091814
(extendedConfigFileName, extendedConfigFilePath) => state.watchFile(
18101815
extendedConfigFileName,
@@ -1834,9 +1839,10 @@ namespace ts {
18341839
configFileName: resolved,
18351840
currentDirectory: state.currentDirectory,
18361841
options: parsed.options,
1837-
program: state.builderPrograms.get(resolvedPath),
1842+
program: state.builderPrograms.get(resolvedPath) || getCachedParsedConfigFile(state, resolvedPath)?.fileNames,
18381843
useCaseSensitiveFileNames: state.parseConfigFileHost.useCaseSensitiveFileNames,
1839-
writeLog: s => state.writeLog(s)
1844+
writeLog: s => state.writeLog(s),
1845+
toPath: fileName => toPath(state, fileName)
18401846
})) return;
18411847

18421848
invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Partial);
@@ -1889,10 +1895,7 @@ namespace ts {
18891895

18901896
function stopWatching(state: SolutionBuilderState) {
18911897
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
1892-
clearMap(state.allWatchedExtendedConfigFiles, watcher => {
1893-
watcher.projects.clear();
1894-
watcher.close();
1895-
});
1898+
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf);
18961899
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
18971900
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
18981901
}

src/compiler/types.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -3928,9 +3928,9 @@ namespace ts {
39283928

39293929
/* @internal */
39303930
export const enum StructureIsReused {
3931-
Not = 0,
3932-
SafeModules = 1 << 0,
3933-
Completely = 1 << 1,
3931+
Not,
3932+
SafeModules,
3933+
Completely,
39343934
}
39353935

39363936
export type CustomTransformerFactory = (context: TransformationContext) => CustomTransformer;
@@ -6443,6 +6443,7 @@ namespace ts {
64436443
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[];
64446444
getEnvironmentVariable?(name: string): string | undefined;
64456445
/* @internal */ onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions, hasSourceFileByPath: boolean): void;
6446+
/* @internal */ onReleaseParsedCommandLine?(configFileName: string, oldResolvedRef: ResolvedProjectReference | undefined, optionOptions: CompilerOptions): void;
64466447
/* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution;
64476448
/* @internal */ hasChangedAutomaticTypeDirectiveNames?: HasChangedAutomaticTypeDirectiveNames;
64486449
createHash?(data: string): string;

src/compiler/utilities.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -5392,31 +5392,31 @@ namespace ts {
53925392
/**
53935393
* clears already present map by calling onDeleteExistingValue callback before deleting that key/value
53945394
*/
5395-
export function clearMap<T>(map: { forEach: ESMap<string, T>["forEach"]; clear: ESMap<string, T>["clear"]; }, onDeleteValue: (valueInMap: T, key: string) => void) {
5395+
export function clearMap<K, T>(map: { forEach: ESMap<K, T>["forEach"]; clear: ESMap<K, T>["clear"]; }, onDeleteValue: (valueInMap: T, key: K) => void) {
53965396
// Remove all
53975397
map.forEach(onDeleteValue);
53985398
map.clear();
53995399
}
54005400

5401-
export interface MutateMapSkippingNewValuesOptions<T, U> {
5402-
onDeleteValue(existingValue: T, key: string): void;
5401+
export interface MutateMapSkippingNewValuesOptions<K, T, U> {
5402+
onDeleteValue(existingValue: T, key: K): void;
54035403

54045404
/**
54055405
* If present this is called with the key when there is value for that key both in new map as well as existing map provided
54065406
* Caller can then decide to update or remove this key.
54075407
* If the key is removed, caller will get callback of createNewValue for that key.
54085408
* If this callback is not provided, the value of such keys is not updated.
54095409
*/
5410-
onExistingValue?(existingValue: T, valueInNewMap: U, key: string): void;
5410+
onExistingValue?(existingValue: T, valueInNewMap: U, key: K): void;
54115411
}
54125412

54135413
/**
54145414
* Mutates the map with newMap such that keys in map will be same as newMap.
54155415
*/
5416-
export function mutateMapSkippingNewValues<T, U>(
5417-
map: ESMap<string, T>,
5418-
newMap: ReadonlyESMap<string, U>,
5419-
options: MutateMapSkippingNewValuesOptions<T, U>
5416+
export function mutateMapSkippingNewValues<K, T, U>(
5417+
map: ESMap<K, T>,
5418+
newMap: ReadonlyESMap<K, U>,
5419+
options: MutateMapSkippingNewValuesOptions<K, T, U>
54205420
) {
54215421
const { onDeleteValue, onExistingValue } = options;
54225422
// Needs update
@@ -5434,14 +5434,14 @@ namespace ts {
54345434
});
54355435
}
54365436

5437-
export interface MutateMapOptions<T, U> extends MutateMapSkippingNewValuesOptions<T, U> {
5438-
createNewValue(key: string, valueInNewMap: U): T;
5437+
export interface MutateMapOptions<K, T, U> extends MutateMapSkippingNewValuesOptions<K, T, U> {
5438+
createNewValue(key: K, valueInNewMap: U): T;
54395439
}
54405440

54415441
/**
54425442
* Mutates the map with newMap such that keys in map will be same as newMap.
54435443
*/
5444-
export function mutateMap<T, U>(map: ESMap<string, T>, newMap: ReadonlyESMap<string, U>, options: MutateMapOptions<T, U>) {
5444+
export function mutateMap<K, T, U>(map: ESMap<K, T>, newMap: ReadonlyESMap<K, U>, options: MutateMapOptions<K, T, U>) {
54455445
// Needs update
54465446
mutateMapSkippingNewValues(map, newMap, options);
54475447

src/compiler/watch.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ namespace ts {
8989
}
9090

9191
/** Parses config file using System interface */
92-
export function parseConfigFileWithSystem(configFileName: string, optionsToExtend: CompilerOptions, watchOptionsToExtend: WatchOptions | undefined, system: System, reportDiagnostic: DiagnosticReporter) {
92+
export function parseConfigFileWithSystem(configFileName: string, optionsToExtend: CompilerOptions, extendedConfigCache: Map<ExtendedConfigCacheEntry> | undefined, watchOptionsToExtend: WatchOptions | undefined, system: System, reportDiagnostic: DiagnosticReporter) {
9393
const host: ParseConfigFileHost = <any>system;
9494
host.onUnRecoverableConfigFileDiagnostic = diagnostic => reportUnrecoverableDiagnostic(system, reportDiagnostic, diagnostic);
95-
const result = getParsedCommandLineOfConfigFile(configFileName, optionsToExtend, host, /*extendedConfigCache*/ undefined, watchOptionsToExtend);
95+
const result = getParsedCommandLineOfConfigFile(configFileName, optionsToExtend, host, extendedConfigCache, watchOptionsToExtend);
9696
host.onUnRecoverableConfigFileDiagnostic = undefined!; // TODO: GH#18217
9797
return result;
9898
}
@@ -414,7 +414,10 @@ namespace ts {
414414
MissingFile: "Missing file",
415415
WildcardDirectory: "Wild card directory",
416416
FailedLookupLocations: "Failed Lookup Locations",
417-
TypeRoots: "Type roots"
417+
TypeRoots: "Type roots",
418+
ConfigFileOfReferencedProject: "Config file of referened project",
419+
ExtendedConfigOfReferencedProject: "Extended config file of referenced project",
420+
WildcardDirectoryOfReferencedProject: "Wild card directory of referenced project",
418421
};
419422

420423
export interface WatchTypeRegistry {
@@ -424,7 +427,10 @@ namespace ts {
424427
MissingFile: "Missing file",
425428
WildcardDirectory: "Wild card directory",
426429
FailedLookupLocations: "Failed Lookup Locations",
427-
TypeRoots: "Type roots"
430+
TypeRoots: "Type roots",
431+
ConfigFileOfReferencedProject: "Config file of referened project",
432+
ExtendedConfigOfReferencedProject: "Extended config file of referenced project",
433+
WildcardDirectoryOfReferencedProject: "Wild card directory of referenced project",
428434
}
429435

430436
interface WatchFactory<X, Y = undefined> extends ts.WatchFactory<X, Y> {

0 commit comments

Comments
 (0)