Skip to content
130 changes: 77 additions & 53 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ namespace ts {
readonly rootNames: readonly string[];
readonly baseWatchOptions: WatchOptions | undefined;

readonly resolvedConfigFilePaths: ESMap<string, ResolvedConfigFilePath>;
readonly resolvedConfigFilePaths: ESMap<ResolvedConfigFileName, ResolvedConfigFilePath>;
readonly resolvedPackageJsonPaths: ESMap<ResolvedConfigFileName, string>;
readonly configFileCache: ESMap<ResolvedConfigFilePath, ConfigFileCacheEntry>;
/** Map from config file name to up-to-date status */
readonly projectStatus: ESMap<ResolvedConfigFilePath, UpToDateStatus>;
Expand Down Expand Up @@ -270,13 +271,11 @@ namespace ts {
readonly allWatchedWildcardDirectories: ESMap<ResolvedConfigFilePath, ESMap<string, WildcardDirectoryWatcher>>;
readonly allWatchedInputFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
readonly allWatchedConfigFiles: ESMap<ResolvedConfigFilePath, FileWatcher>;
readonly allWatchedExtendedConfigFiles: ESMap<Path, SharedExtendedConfigFileWatcher<ResolvedConfigFilePath>>;
readonly allWatchedPackageJsonFiles: ESMap<ResolvedConfigFilePath, ESMap<Path, FileWatcher>>;
readonly allWatchedExtendedConfigFiles: ESMap<Path, SharedFileWatcher<ResolvedConfigFilePath>>;
readonly allWatchedPackageJsonFiles: ESMap<Path, SharedFileWatcher<ResolvedConfigFilePath>>;
readonly filesWatched: ESMap<Path, FileWatcherWithModifiedTime | Date>;
readonly outputTimeStamps: ESMap<ResolvedConfigFilePath, ESMap<Path, Date>>;

readonly lastCachedPackageJsonLookups: ESMap<ResolvedConfigFilePath, readonly (readonly [Path, object | boolean])[] | undefined>;

timerToBuildInvalidatedProject: any;
reportFileChangeDetected: boolean;
writeLog: (s: string) => void;
Expand Down Expand Up @@ -327,6 +326,7 @@ namespace ts {
baseWatchOptions,

resolvedConfigFilePaths: new Map(),
resolvedPackageJsonPaths: new Map(),
configFileCache: new Map(),
projectStatus: new Map(),
extendedConfigCache: new Map(),
Expand Down Expand Up @@ -360,8 +360,6 @@ namespace ts {
allWatchedPackageJsonFiles: new Map(),
filesWatched: new Map(),

lastCachedPackageJsonLookups: new Map(),

timerToBuildInvalidatedProject: undefined,
reportFileChangeDetected: false,
watchFile,
Expand Down Expand Up @@ -478,6 +476,7 @@ namespace ts {

// Clear all to ResolvedConfigFilePaths cache to start fresh
state.resolvedConfigFilePaths.clear();
state.resolvedPackageJsonPaths.clear();

// TODO(rbuckton): Should be a `Set`, but that requires changing the code below that uses `mutateMapSkippingNewValues`
const currentProjects = new Map(
Expand All @@ -504,14 +503,7 @@ namespace ts {
{ onDeleteValue: closeFileWatcher }
);

state.allWatchedExtendedConfigFiles.forEach(watcher => {
watcher.projects.forEach(project => {
if (!currentProjects.has(project)) {
watcher.projects.delete(project);
}
});
watcher.close();
});
state.allWatchedExtendedConfigFiles.forEach(closeSharedWatcherOfUnknownProjects);

mutateMapSkippingNewValues(
state.allWatchedWildcardDirectories,
Expand All @@ -525,13 +517,18 @@ namespace ts {
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
);

mutateMapSkippingNewValues(
state.allWatchedPackageJsonFiles,
currentProjects,
{ onDeleteValue: existingMap => existingMap.forEach(closeFileWatcher) }
);
state.allWatchedPackageJsonFiles.forEach(closeSharedWatcherOfUnknownProjects);
}
return state.buildOrder = buildOrder;

function closeSharedWatcherOfUnknownProjects(watcher: SharedFileWatcher<ResolvedConfigFilePath>) {
watcher.projects.forEach(project => {
if (!currentProjects.has(project)) {
watcher.projects.delete(project);
}
});
watcher.close();
}
}

function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined): AnyBuildOrder | undefined {
Expand Down Expand Up @@ -589,7 +586,7 @@ namespace ts {
function disableCache(state: SolutionBuilderState) {
if (!state.cache) return;

const { cache, host, compilerHost, extendedConfigCache, moduleResolutionCache, typeReferenceDirectiveResolutionCache } = state;
const { cache, host, compilerHost, extendedConfigCache, moduleResolutionCache, typeReferenceDirectiveResolutionCache, } = state;

host.readFile = cache.originalReadFile;
host.fileExists = cache.originalFileExists;
Expand Down Expand Up @@ -893,11 +890,6 @@ namespace ts {
config.projectReferences
);
if (state.watch) {
state.lastCachedPackageJsonLookups.set(projectPath, state.moduleResolutionCache && map(
state.moduleResolutionCache.getPackageJsonInfoCache().entries(),
([path, data]) => ([state.host.realpath && data ? toPath(state, state.host.realpath(path)) : path, data] as const)
));

state.builderPrograms.set(projectPath, program);
}
step++;
Expand Down Expand Up @@ -1241,10 +1233,9 @@ namespace ts {
config.fileNames = getFileNamesFromConfigSpecs(config.options.configFile!.configFileSpecs!, getDirectoryPath(project), config.options, state.parseConfigFileHost);
updateErrorForNoInputFiles(config.fileNames, project, config.options.configFile!.configFileSpecs!, config.errors, canJsonReportNoInputFiles(config.raw));
watchInputFiles(state, project, projectPath, config);
watchPackageJsonFiles(state, project, projectPath, config);
}

const status = getUpToDateStatus(state, config, projectPath);
const status = getUpToDateStatus(state, config, project, projectPath);
if (!options.force) {
if (status.type === UpToDateStatusType.UpToDate) {
verboseReportProjectStatus(state, project, status);
Expand Down Expand Up @@ -1507,7 +1498,7 @@ namespace ts {
}
}

function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
function getUpToDateStatusWorker(state: SolutionBuilderState, project: ParsedCommandLine, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
// Container if no files are specified in the project
if (!project.fileNames.length && !canJsonReportNoInputFiles(project.raw)) {
return {
Expand All @@ -1524,7 +1515,7 @@ namespace ts {
const resolvedRef = resolveProjectReferencePath(ref);
const resolvedRefPath = toResolvedConfigFilePath(state, resolvedRef);
const resolvedConfig = parseConfigFile(state, resolvedRef, resolvedRefPath)!;
const refStatus = getUpToDateStatus(state, resolvedConfig, resolvedRefPath);
const refStatus = getUpToDateStatus(state, resolvedConfig, resolvedRef, resolvedRefPath);

// Its a circular reference ignore the status of this project
if (refStatus.type === UpToDateStatusType.ComputingUpstream ||
Expand Down Expand Up @@ -1739,13 +1730,16 @@ namespace ts {
if (configStatus) return configStatus;

// Check extended config time
const extendedConfigStatus = forEach(project.options.configFile!.extendedSourceFiles || emptyArray, configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName!));
const extendedConfigStatus = forEach(
project.options.configFile!.extendedSourceFiles || emptyArray,
configFile => checkConfigFileUpToDateStatus(state, configFile, oldestOutputFileTime, oldestOutputFileName!)
);
if (extendedConfigStatus) return extendedConfigStatus;

// Check package file time
const dependentPackageFileStatus = forEach(
state.lastCachedPackageJsonLookups.get(resolvedPath) || emptyArray,
([path]) => checkConfigFileUpToDateStatus(state, path, oldestOutputFileTime, oldestOutputFileName!)
getPackageJsonsFromConfig(state, resolved, project),
file => checkConfigFileUpToDateStatus(state, file, oldestOutputFileTime, oldestOutputFileName!)
);
if (dependentPackageFileStatus) return dependentPackageFileStatus;

Expand Down Expand Up @@ -1789,7 +1783,7 @@ namespace ts {
return false;
}

function getUpToDateStatus(state: SolutionBuilderState, project: ParsedCommandLine | undefined, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
function getUpToDateStatus(state: SolutionBuilderState, project: ParsedCommandLine | undefined, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath): UpToDateStatus {
if (project === undefined) {
return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" };
}
Expand All @@ -1799,7 +1793,7 @@ namespace ts {
return prior;
}

const actual = getUpToDateStatusWorker(state, project, resolvedPath);
const actual = getUpToDateStatusWorker(state, project, resolved, resolvedPath);
state.projectStatus.set(resolvedPath, actual);
return actual;
}
Expand Down Expand Up @@ -2038,6 +2032,12 @@ namespace ts {
scheduleBuildInvalidatedProject(state, 250, /*changeDetected*/ true);
}

function invalidateProjectAndScheduledBuildsOfSharedFileWacher(state: SolutionBuilderState, sharedWatcher: SharedFileWatcher<ResolvedConfigFilePath> | undefined, reloadLevel: ConfigFileProgramReloadLevel) {
sharedWatcher?.projects.forEach(projectConfigFilePath =>
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, reloadLevel)
);
}

function scheduleBuildInvalidatedProject(state: SolutionBuilderState, time: number, changeDetected: boolean) {
const { hostWithWatch } = state;
if (!hostWithWatch.setTimeout || !hostWithWatch.clearTimeout) {
Expand Down Expand Up @@ -2103,8 +2103,11 @@ namespace ts {
(extendedConfigFileName, extendedConfigFilePath) => watchFile(
state,
extendedConfigFileName,
() => state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath)?.projects.forEach(projectConfigFilePath =>
invalidateProjectAndScheduleBuilds(state, projectConfigFilePath, ConfigFileProgramReloadLevel.Full)),
() => invalidateProjectAndScheduledBuildsOfSharedFileWacher(
state,
state.allWatchedExtendedConfigFiles.get(extendedConfigFilePath),
ConfigFileProgramReloadLevel.Full
),
PollingInterval.High,
parsed?.watchOptions,
WatchType.ExtendedConfigFile,
Expand Down Expand Up @@ -2164,23 +2167,44 @@ namespace ts {
);
}

function getPackageJsonsFromConfig(state: SolutionBuilderState, resolved: ResolvedConfigFileName, parsed: ParsedCommandLine): string[] {
const result: string[] = [getPackageJsonPathFromConfig(state, resolved)];
parsed.projectReferences?.forEach(ref => result.push(getPackageJsonPathFromConfig(state, resolveProjectName(state, ref.path))));
return result;
}

function getPackageJsonPathFromConfig(state: SolutionBuilderState, resolved: ResolvedConfigFileName) {
const { resolvedPackageJsonPaths } = state;
const path = resolvedPackageJsonPaths.get(resolved);
if (path !== undefined) return path;

const packageJsonPath = combinePaths(getDirectoryPath(resolved), "package.json");
resolvedPackageJsonPaths.set(resolved, packageJsonPath);
return packageJsonPath;
}

function watchPackageJsonFiles(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please specify the return type for this function?

if (!state.watch || !state.lastCachedPackageJsonLookups) return;
mutateMap(
getOrCreateValueMapFromConfigFileMap(state.allWatchedPackageJsonFiles, resolvedPath),
new Map(state.lastCachedPackageJsonLookups.get(resolvedPath)),
{
createNewValue: (path, _input) => watchFile(
if (!state.watch) return;
// Container if no files are specified in the project, nothing to watch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚙️

Suggested change
// Container if no files are specified in the project, nothing to watch
// If no files are specified in the project, there is nothing to watch in the container.

if (!parsed.fileNames.length && !canJsonReportNoInputFiles(parsed.raw)) return;
updateSharedFileWatcher(
resolvedPath,
getPackageJsonsFromConfig(state, resolved, parsed),
state.allWatchedPackageJsonFiles,
(file, path) => watchFile(
state,
file,
() => invalidateProjectAndScheduledBuildsOfSharedFileWacher(
state,
path,
() => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None),
PollingInterval.High,
parsed?.watchOptions,
WatchType.PackageJson,
resolved
state.allWatchedPackageJsonFiles.get(path),
ConfigFileProgramReloadLevel.None,
),
onDeleteValue: closeFileWatcher,
}
PollingInterval.High,
parsed?.watchOptions,
WatchType.PackageJson,
resolved
),
fileName => toPath(state, fileName),
);
}

Expand Down Expand Up @@ -2211,7 +2235,7 @@ namespace ts {
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf);
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
clearMap(state.allWatchedInputFiles, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcher));
clearMap(state.allWatchedPackageJsonFiles, watchedPacageJsonFiles => clearMap(watchedPacageJsonFiles, closeFileWatcher));
clearMap(state.allWatchedPackageJsonFiles, closeFileWatcherOf);
}

/**
Expand All @@ -2235,7 +2259,7 @@ namespace ts {
getUpToDateStatusOfProject: project => {
const configFileName = resolveProjectName(state, project);
const configFilePath = toResolvedConfigFilePath(state, configFileName);
return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFilePath);
return getUpToDateStatus(state, parseConfigFile(state, configFileName, configFilePath), configFileName, configFilePath);
},
invalidateProject: (configFilePath, reloadLevel) => invalidateProject(state, configFilePath, reloadLevel || ConfigFileProgramReloadLevel.None),
close: () => stopWatching(state),
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ namespace ts {
let timerToUpdateProgram: any; // timer callback to recompile the program
let timerToInvalidateFailedLookupResolutions: any; // timer callback to invalidate resolutions for changes in failed lookup locations
let parsedConfigs: ESMap<Path, ParsedConfig> | undefined; // Parsed commandline and watching cached for referenced projects
let sharedExtendedConfigFileWatchers: ESMap<Path, SharedExtendedConfigFileWatcher<Path>>; // Map of file watchers for extended files, shared between different referenced projects
let sharedExtendedConfigFileWatchers: ESMap<Path, SharedFileWatcher<Path>>; // Map of file watchers for extended files, shared between different referenced projects
let extendedConfigCache = host.extendedConfigCache; // Cache for extended config evaluation
let changesAffectResolution = false; // Flag for indicating non-config changes affect module resolution
let reportFileChangeDetectedOnCreateProgram = false; // True if synchronizeProgram should report "File change detected..." when a new program is created
Expand Down
Loading