Skip to content

Commit 8e855d1

Browse files
authored
Watch mode watches for changes in package.json files used in resolution (#44935)
* watch mode watches for changes in package.json files used in resolution * Pathify result of realpath * Actually accept pathified baselines
1 parent 0746f70 commit 8e855d1

File tree

39 files changed

+1244
-3
lines changed

39 files changed

+1244
-3
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ namespace ts {
494494
export interface PackageJsonInfoCache {
495495
/*@internal*/ getPackageJsonInfo(packageJsonPath: string): PackageJsonInfo | boolean | undefined;
496496
/*@internal*/ setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | boolean): void;
497+
/*@internal*/ entries(): [Path, PackageJsonInfo | boolean][];
497498
clear(): void;
498499
}
499500

@@ -559,7 +560,7 @@ namespace ts {
559560

560561
function createPackageJsonInfoCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): PackageJsonInfoCache {
561562
let cache: ESMap<Path, PackageJsonInfo | boolean> | undefined;
562-
return { getPackageJsonInfo, setPackageJsonInfo, clear };
563+
return { getPackageJsonInfo, setPackageJsonInfo, clear, entries };
563564
function getPackageJsonInfo(packageJsonPath: string) {
564565
return cache?.get(toPath(packageJsonPath, currentDirectory, getCanonicalFileName));
565566
}
@@ -569,6 +570,10 @@ namespace ts {
569570
function clear() {
570571
cache = undefined;
571572
}
573+
function entries() {
574+
const iter = cache?.entries();
575+
return iter ? arrayFrom(iter) : [];
576+
}
572577
}
573578

574579
function getOrCreateCache<T>(cacheWithRedirects: CacheWithRedirects<T>, redirectedReference: ResolvedProjectReference | undefined, key: string, create: () => T): T {

src/compiler/program.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,7 @@ namespace ts {
10541054
getSourceFileByPath,
10551055
getSourceFiles: () => files,
10561056
getMissingFilePaths: () => missingFilePaths!, // TODO: GH#18217
1057+
getModuleResolutionCache: () => moduleResolutionCache,
10571058
getFilesByNameMap: () => filesByName,
10581059
getCompilerOptions: () => options,
10591060
getSyntacticDiagnostics,

src/compiler/resolutionCache.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ namespace ts {
2525
updateTypeRootsWatch(): void;
2626
closeTypeRootsWatch(): void;
2727

28+
getModuleResolutionCache(): ModuleResolutionCache;
29+
2830
clear(): void;
2931
}
3032

@@ -203,6 +205,7 @@ namespace ts {
203205
const typeRootsWatches = new Map<string, FileWatcher>();
204206

205207
return {
208+
getModuleResolutionCache: () => moduleResolutionCache,
206209
startRecordingFilesWithChangedResolutions,
207210
finishRecordingFilesWithChangedResolutions,
208211
// perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update

src/compiler/tsbuildPublic.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ namespace ts {
257257
readonly allWatchedInputFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
258258
readonly allWatchedConfigFiles: ESMap<ResolvedConfigFilePath, FileWatcher>;
259259
readonly allWatchedExtendedConfigFiles: ESMap<Path, SharedExtendedConfigFileWatcher<ResolvedConfigFilePath>>;
260+
readonly allWatchedPackageJsonFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
261+
readonly lastCachedPackageJsonLookups: ESMap<ResolvedConfigFilePath, readonly (readonly [Path, object | boolean])[] | undefined>;
260262

261263
timerToBuildInvalidatedProject: any;
262264
reportFileChangeDetected: boolean;
@@ -336,6 +338,8 @@ namespace ts {
336338
allWatchedInputFiles: new Map(),
337339
allWatchedConfigFiles: new Map(),
338340
allWatchedExtendedConfigFiles: new Map(),
341+
allWatchedPackageJsonFiles: new Map(),
342+
lastCachedPackageJsonLookups: new Map(),
339343

340344
timerToBuildInvalidatedProject: undefined,
341345
reportFileChangeDetected: false,
@@ -498,6 +502,12 @@ namespace ts {
498502
currentProjects,
499503
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
500504
);
505+
506+
mutateMapSkippingNewValues(
507+
state.allWatchedPackageJsonFiles,
508+
currentProjects,
509+
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
510+
);
501511
}
502512
return state.buildOrder = buildOrder;
503513
}
@@ -861,6 +871,11 @@ namespace ts {
861871
getConfigFileParsingDiagnostics(config),
862872
config.projectReferences
863873
);
874+
state.lastCachedPackageJsonLookups.set(projectPath, state.moduleResolutionCache && map(
875+
state.moduleResolutionCache.getPackageJsonInfoCache().entries(),
876+
([path, data]) => ([state.host.realpath ? toPath(state, state.host.realpath(path)) : path, data] as const)
877+
));
878+
864879
if (state.watch) {
865880
state.builderPrograms.set(projectPath, program);
866881
}
@@ -1192,12 +1207,14 @@ namespace ts {
11921207
watchExtendedConfigFiles(state, projectPath, config);
11931208
watchWildCardDirectories(state, project, projectPath, config);
11941209
watchInputFiles(state, project, projectPath, config);
1210+
watchPackageJsonFiles(state, project, projectPath, config);
11951211
}
11961212
else if (reloadLevel === ConfigFileProgramReloadLevel.Partial) {
11971213
// Update file names
11981214
config.fileNames = getFileNamesFromConfigSpecs(config.options.configFile!.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost);
11991215
updateErrorForNoInputFiles(config.fileNames, project, config.options.configFile!.configFileSpecs!, config.errors, canJsonReportNoInputFiles(config.raw));
12001216
watchInputFiles(state, project, projectPath, config);
1217+
watchPackageJsonFiles(state, project, projectPath, config);
12011218
}
12021219

12031220
const status = getUpToDateStatus(state, config, projectPath);
@@ -1490,6 +1507,13 @@ namespace ts {
14901507
// Check extended config time
14911508
const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName));
14921509
if (extendedConfigStatus) return extendedConfigStatus;
1510+
1511+
// Check package file time
1512+
const dependentPackageFileStatus = forEach(
1513+
state.lastCachedPackageJsonLookups.get(resolvedPath) || emptyArray,
1514+
([path]) => checkConfigFileUpToDateStatus(state, path, oldestOutputFileTime, oldestOutputFileName)
1515+
);
1516+
if (dependentPackageFileStatus) return dependentPackageFileStatus;
14931517
}
14941518

14951519
if (!force && !state.buildInfoChecked.has(resolvedPath)) {
@@ -1862,6 +1886,25 @@ namespace ts {
18621886
);
18631887
}
18641888

1889+
function watchPackageJsonFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
1890+
if (!state.watch || !state.lastCachedPackageJsonLookups) return;
1891+
mutateMap(
1892+
getOrCreateValueMapFromConfigFileMap(state.allWatchedPackageJsonFiles, resolvedPath),
1893+
new Map(state.lastCachedPackageJsonLookups.get(resolvedPath)),
1894+
{
1895+
createNewValue: (path, _input) => state.watchFile(
1896+
path,
1897+
() => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full),
1898+
PollingInterval.High,
1899+
parsed?.watchOptions,
1900+
WatchType.PackageJson,
1901+
resolved
1902+
),
1903+
onDeleteValue: closeFileWatcher,
1904+
}
1905+
);
1906+
}
1907+
18651908
function startWatching(state: SolutionBuilderState, buildOrder: AnyBuildOrder) {
18661909
if (!state.watchAllProjectsPending) return;
18671910
state.watchAllProjectsPending = false;
@@ -1877,6 +1920,9 @@ namespace ts {
18771920

18781921
// Watch input files
18791922
watchInputFiles(state, resolved, resolvedPath, cfg);
1923+
1924+
// Watch package json files
1925+
watchPackageJsonFiles(state, resolved, resolvedPath, cfg);
18801926
}
18811927
}
18821928
}
@@ -1886,6 +1932,7 @@ namespace ts {
18861932
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf);
18871933
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
18881934
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
1935+
clearMap(state.allWatchedPackageJsonFiles, watchedPacageJsonFiles => clearMap(watchedPacageJsonFiles, closeFileWatcher));
18891936
}
18901937

18911938
/**

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3895,6 +3895,8 @@ namespace ts {
38953895
/* @internal */
38963896
getMissingFilePaths(): readonly Path[];
38973897
/* @internal */
3898+
getModuleResolutionCache(): ModuleResolutionCache | undefined;
3899+
/* @internal */
38983900
getFilesByNameMap(): ESMap<string, SourceFile | false | undefined>;
38993901

39003902
/**

src/compiler/watch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ namespace ts {
418418
ConfigFileOfReferencedProject: "Config file of referened project",
419419
ExtendedConfigOfReferencedProject: "Extended config file of referenced project",
420420
WildcardDirectoryOfReferencedProject: "Wild card directory of referenced project",
421+
PackageJson: "package.json file",
421422
};
422423

423424
export interface WatchTypeRegistry {
@@ -431,6 +432,7 @@ namespace ts {
431432
ConfigFileOfReferencedProject: "Config file of referened project",
432433
ExtendedConfigOfReferencedProject: "Extended config file of referenced project",
433434
WildcardDirectoryOfReferencedProject: "Wild card directory of referenced project",
435+
PackageJson: "package.json file",
434436
}
435437

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

src/compiler/watchPublic.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,14 @@ namespace ts {
265265
let builderProgram: T;
266266
let reloadLevel: ConfigFileProgramReloadLevel; // level to indicate if the program needs to be reloaded from config file/just filenames etc
267267
let missingFilesMap: ESMap<Path, FileWatcher>; // Map of file watchers for the missing files
268+
let packageJsonMap: ESMap<Path, FileWatcher>; // map of watchers for package json files used in module resolution
268269
let watchedWildcardDirectories: ESMap<string, WildcardDirectoryWatcher>; // map of watchers for the wild card directories in the config file
269270
let timerToUpdateProgram: any; // timer callback to recompile the program
270271
let timerToInvalidateFailedLookupResolutions: any; // timer callback to invalidate resolutions for changes in failed lookup locations
271272
let parsedConfigs: ESMap<Path, ParsedConfig> | undefined; // Parsed commandline and watching cached for referenced projects
272273
let sharedExtendedConfigFileWatchers: ESMap<Path, SharedExtendedConfigFileWatcher<Path>>; // Map of file watchers for extended files, shared between different referenced projects
273274
let extendedConfigCache = host.extendedConfigCache; // Cache for extended config evaluation
275+
let changesAffectResolution = false; // Flag for indicating non-config changes affect module resolution
274276

275277
const sourceFilesCache = new Map<string, HostFileInfo>(); // Cache that stores the source file and version info
276278
let missingFilePathsRequestedForRelease: Path[] | undefined; // These paths are held temporarily so that we can remove the entry from source file cache if the file is not tracked by missing files
@@ -419,13 +421,13 @@ namespace ts {
419421
const program = getCurrentBuilderProgram();
420422
if (hasChangedCompilerOptions) {
421423
newLine = updateNewLine();
422-
if (program && changesAffectModuleResolution(program.getCompilerOptions(), compilerOptions)) {
424+
if (program && (changesAffectResolution || changesAffectModuleResolution(program.getCompilerOptions(), compilerOptions))) {
423425
resolutionCache.clear();
424426
}
425427
}
426428

427429
// All resolutions are invalid if user provided resolutions
428-
const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(userProvidedResolution);
430+
const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(userProvidedResolution || changesAffectResolution);
429431
if (isProgramUptoDate(getCurrentProgram(), rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
430432
if (hasChangedConfigFileParsingErrors) {
431433
builderProgram = createProgram(/*rootNames*/ undefined, /*options*/ undefined, compilerHost, builderProgram, configFileParsingDiagnostics, projectReferences);
@@ -436,6 +438,8 @@ namespace ts {
436438
createNewProgram(hasInvalidatedResolution);
437439
}
438440

441+
changesAffectResolution = false; // reset for next sync
442+
439443
if (host.afterProgramCreate && program !== builderProgram) {
440444
host.afterProgramCreate(builderProgram);
441445
}
@@ -457,10 +461,13 @@ namespace ts {
457461
compilerHost.hasInvalidatedResolution = hasInvalidatedResolution;
458462
compilerHost.hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames;
459463
builderProgram = createProgram(rootFileNames, compilerOptions, compilerHost, builderProgram, configFileParsingDiagnostics, projectReferences);
464+
// map package json cache entries to their realpaths so we don't try to watch across symlinks
465+
const packageCacheEntries = map(resolutionCache.getModuleResolutionCache().getPackageJsonInfoCache().entries(), ([path, data]) => ([compilerHost.realpath ? toPath(compilerHost.realpath(path)) : path, data] as const));
460466
resolutionCache.finishCachingPerDirectoryResolution();
461467

462468
// Update watches
463469
updateMissingFilePathsWatch(builderProgram.getProgram(), missingFilesMap || (missingFilesMap = new Map()), watchMissingFilePath);
470+
updatePackageJsonWatch(packageCacheEntries, packageJsonMap || (packageJsonMap = new Map()), watchPackageJsonLookupPath);
464471
if (needsUpdateInTypeRootWatch) {
465472
resolutionCache.updateTypeRootsWatch();
466473
}
@@ -823,6 +830,24 @@ namespace ts {
823830
watchFilePath(missingFilePath, missingFilePath, onMissingFileChange, PollingInterval.Medium, watchOptions, WatchType.MissingFile);
824831
}
825832

833+
function watchPackageJsonLookupPath(packageJsonPath: Path) {
834+
// If the package.json is pulled into the compilation itself (eg, via json imports), don't add a second watcher here
835+
return sourceFilesCache.has(packageJsonPath) ?
836+
noopFileWatcher :
837+
watchFilePath(packageJsonPath, packageJsonPath, onPackageJsonChange, PollingInterval.High, watchOptions, WatchType.PackageJson);
838+
}
839+
840+
function onPackageJsonChange(fileName: string, eventKind: FileWatcherEventKind, path: Path) {
841+
updateCachedSystemWithFile(fileName, path, eventKind);
842+
843+
// package.json changes invalidate module resolution and can change the set of loaded files
844+
// so if we witness a change to one, we have to do a full reload
845+
reloadLevel = ConfigFileProgramReloadLevel.Full;
846+
changesAffectResolution = true;
847+
// Update the program
848+
scheduleProgramUpdate();
849+
}
850+
826851
function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) {
827852
updateCachedSystemWithFile(fileName, missingFilePath, eventKind);
828853

src/compiler/watchUtilities.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,25 @@ namespace ts {
355355
});
356356
}
357357

358+
/**
359+
* Updates watchers based on the package json files used in module resolution
360+
*/
361+
export function updatePackageJsonWatch(
362+
lookups: readonly (readonly [Path, object | boolean])[],
363+
packageJsonWatches: ESMap<Path, FileWatcher>,
364+
createPackageJsonWatch: (packageJsonPath: Path, data: object | boolean) => FileWatcher,
365+
) {
366+
const newMap = new Map(lookups);
367+
mutateMap(
368+
packageJsonWatches,
369+
newMap,
370+
{
371+
createNewValue: createPackageJsonWatch,
372+
onDeleteValue: closeFileWatcher
373+
}
374+
);
375+
}
376+
358377
/**
359378
* Updates the existing missing file watches with the new set of missing files after new program is created
360379
*/

0 commit comments

Comments
 (0)