Skip to content

Commit a76a166

Browse files
authored
Move useSourceOfProjectReferenceRedirect to program so other hosts can use it too, enabling it for WatchHost (#37370)
1 parent 1f71016 commit a76a166

File tree

13 files changed

+378
-223
lines changed

13 files changed

+378
-223
lines changed

src/compiler/program.ts

+189-13
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,17 @@ namespace ts {
806806
let projectReferenceRedirects: Map<ResolvedProjectReference | false> | undefined;
807807
let mapFromFileToProjectReferenceRedirects: Map<Path> | undefined;
808808
let mapFromToProjectReferenceRedirectSource: Map<SourceOfProjectReferenceRedirect> | undefined;
809-
const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect && host.useSourceOfProjectReferenceRedirect();
809+
810+
const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() &&
811+
!options.disableSourceOfProjectReferenceRedirect;
812+
const onProgramCreateComplete = updateHostForUseSourceOfProjectReferenceRedirect({
813+
compilerHost: host,
814+
useSourceOfProjectReferenceRedirect,
815+
toPath,
816+
getResolvedProjectReferences,
817+
getSourceOfProjectReferenceRedirect,
818+
forEachResolvedProjectReference
819+
});
810820

811821
const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
812822
// We set `structuralIsReused` to `undefined` because `tryReuseStructureFromOldProgram` calls `tryReuseStructureFromOldProgram` which checks
@@ -821,12 +831,6 @@ namespace ts {
821831
if (!resolvedProjectReferences) {
822832
resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile);
823833
}
824-
if (host.setResolvedProjectReferenceCallbacks) {
825-
host.setResolvedProjectReferenceCallbacks({
826-
getSourceOfProjectReferenceRedirect,
827-
forEachResolvedProjectReference
828-
});
829-
}
830834
if (rootNames.length) {
831835
for (const parsedRef of resolvedProjectReferences) {
832836
if (!parsedRef) continue;
@@ -970,6 +974,7 @@ namespace ts {
970974
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
971975
};
972976

977+
onProgramCreateComplete();
973978
verifyCompilerOptions();
974979
performance.mark("afterProgram");
975980
performance.measure("Program", "beforeProgram", "afterProgram");
@@ -1248,12 +1253,6 @@ namespace ts {
12481253
}
12491254
if (projectReferences) {
12501255
resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile);
1251-
if (host.setResolvedProjectReferenceCallbacks) {
1252-
host.setResolvedProjectReferenceCallbacks({
1253-
getSourceOfProjectReferenceRedirect,
1254-
forEachResolvedProjectReference
1255-
});
1256-
}
12571256
}
12581257

12591258
// check if program source files has changed in the way that can affect structure of the program
@@ -3460,6 +3459,183 @@ namespace ts {
34603459
}
34613460
}
34623461

3462+
interface SymlinkedDirectory {
3463+
real: string;
3464+
realPath: Path;
3465+
}
3466+
3467+
interface HostForUseSourceOfProjectReferenceRedirect {
3468+
compilerHost: CompilerHost;
3469+
useSourceOfProjectReferenceRedirect: boolean;
3470+
toPath(fileName: string): Path;
3471+
getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined;
3472+
getSourceOfProjectReferenceRedirect(fileName: string): SourceOfProjectReferenceRedirect | undefined;
3473+
forEachResolvedProjectReference<T>(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined;
3474+
}
3475+
3476+
function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) {
3477+
let mapOfDeclarationDirectories: Map<true> | undefined;
3478+
let symlinkedDirectories: Map<SymlinkedDirectory | false> | undefined;
3479+
let symlinkedFiles: Map<string> | undefined;
3480+
3481+
const originalFileExists = host.compilerHost.fileExists;
3482+
const originalDirectoryExists = host.compilerHost.directoryExists;
3483+
const originalGetDirectories = host.compilerHost.getDirectories;
3484+
const originalRealpath = host.compilerHost.realpath;
3485+
3486+
3487+
if (!host.useSourceOfProjectReferenceRedirect) return noop;
3488+
3489+
// This implementation of fileExists checks if the file being requested is
3490+
// .d.ts file for the referenced Project.
3491+
// If it is it returns true irrespective of whether that file exists on host
3492+
host.compilerHost.fileExists = (file) => {
3493+
if (originalFileExists.call(host.compilerHost, file)) return true;
3494+
if (!host.getResolvedProjectReferences()) return false;
3495+
if (!isDeclarationFileName(file)) return false;
3496+
3497+
// Project references go to source file instead of .d.ts file
3498+
return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
3499+
};
3500+
3501+
if (originalDirectoryExists) {
3502+
// This implementation of directoryExists checks if the directory being requested is
3503+
// directory of .d.ts file for the referenced Project.
3504+
// If it is it returns true irrespective of whether that directory exists on host
3505+
host.compilerHost.directoryExists = path => {
3506+
if (originalDirectoryExists.call(host.compilerHost, path)) {
3507+
handleDirectoryCouldBeSymlink(path);
3508+
return true;
3509+
}
3510+
3511+
if (!host.getResolvedProjectReferences()) return false;
3512+
3513+
if (!mapOfDeclarationDirectories) {
3514+
mapOfDeclarationDirectories = createMap();
3515+
host.forEachResolvedProjectReference(ref => {
3516+
if (!ref) return;
3517+
const out = ref.commandLine.options.outFile || ref.commandLine.options.out;
3518+
if (out) {
3519+
mapOfDeclarationDirectories!.set(getDirectoryPath(host.toPath(out)), true);
3520+
}
3521+
else {
3522+
// Set declaration's in different locations only, if they are next to source the directory present doesnt change
3523+
const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir;
3524+
if (declarationDir) {
3525+
mapOfDeclarationDirectories!.set(host.toPath(declarationDir), true);
3526+
}
3527+
}
3528+
});
3529+
}
3530+
3531+
return fileOrDirectoryExistsUsingSource(path, /*isFile*/ false);
3532+
};
3533+
}
3534+
3535+
if (originalGetDirectories) {
3536+
// Call getDirectories only if directory actually present on the host
3537+
// This is needed to ensure that we arent getting directories that we fake about presence for
3538+
host.compilerHost.getDirectories = path =>
3539+
!host.getResolvedProjectReferences() || (originalDirectoryExists && originalDirectoryExists.call(host.compilerHost, path)) ?
3540+
originalGetDirectories.call(host.compilerHost, path) :
3541+
[];
3542+
}
3543+
3544+
// This is something we keep for life time of the host
3545+
if (originalRealpath) {
3546+
host.compilerHost.realpath = s =>
3547+
symlinkedFiles?.get(host.toPath(s)) ||
3548+
originalRealpath.call(host.compilerHost, s);
3549+
}
3550+
3551+
return onProgramCreateComplete;
3552+
3553+
3554+
function onProgramCreateComplete() {
3555+
host.compilerHost.fileExists = originalFileExists;
3556+
host.compilerHost.directoryExists = originalDirectoryExists;
3557+
host.compilerHost.getDirectories = originalGetDirectories;
3558+
// DO not revert realpath as it could be used later
3559+
}
3560+
3561+
function fileExistsIfProjectReferenceDts(file: string) {
3562+
const source = host.getSourceOfProjectReferenceRedirect(file);
3563+
return source !== undefined ?
3564+
isString(source) ? originalFileExists.call(host.compilerHost, source) : true :
3565+
undefined;
3566+
}
3567+
3568+
function directoryExistsIfProjectReferenceDeclDir(dir: string) {
3569+
const dirPath = host.toPath(dir);
3570+
const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`;
3571+
return forEachKey(
3572+
mapOfDeclarationDirectories!,
3573+
declDirPath => dirPath === declDirPath ||
3574+
// Any parent directory of declaration dir
3575+
startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) ||
3576+
// Any directory inside declaration dir
3577+
startsWith(dirPath, `${declDirPath}/`)
3578+
);
3579+
}
3580+
3581+
function handleDirectoryCouldBeSymlink(directory: string) {
3582+
if (!host.getResolvedProjectReferences()) return;
3583+
3584+
// Because we already watch node_modules, handle symlinks in there
3585+
if (!originalRealpath || !stringContains(directory, nodeModulesPathPart)) return;
3586+
if (!symlinkedDirectories) symlinkedDirectories = createMap();
3587+
const directoryPath = ensureTrailingDirectorySeparator(host.toPath(directory));
3588+
if (symlinkedDirectories.has(directoryPath)) return;
3589+
3590+
const real = normalizePath(originalRealpath.call(host.compilerHost, directory));
3591+
let realPath: Path;
3592+
if (real === directory ||
3593+
(realPath = ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) {
3594+
// not symlinked
3595+
symlinkedDirectories.set(directoryPath, false);
3596+
return;
3597+
}
3598+
3599+
symlinkedDirectories.set(directoryPath, {
3600+
real: ensureTrailingDirectorySeparator(real),
3601+
realPath
3602+
});
3603+
}
3604+
3605+
function fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean {
3606+
const fileOrDirectoryExistsUsingSource = isFile ?
3607+
(file: string) => fileExistsIfProjectReferenceDts(file) :
3608+
(dir: string) => directoryExistsIfProjectReferenceDeclDir(dir);
3609+
// Check current directory or file
3610+
const result = fileOrDirectoryExistsUsingSource(fileOrDirectory);
3611+
if (result !== undefined) return result;
3612+
3613+
if (!symlinkedDirectories) return false;
3614+
const fileOrDirectoryPath = host.toPath(fileOrDirectory);
3615+
if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false;
3616+
if (isFile && symlinkedFiles && symlinkedFiles.has(fileOrDirectoryPath)) return true;
3617+
3618+
// If it contains node_modules check if its one of the symlinked path we know of
3619+
return firstDefinedIterator(
3620+
symlinkedDirectories.entries(),
3621+
([directoryPath, symlinkedDirectory]) => {
3622+
if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined;
3623+
const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath));
3624+
if (isFile && result) {
3625+
if (!symlinkedFiles) symlinkedFiles = createMap();
3626+
// Store the real path for the file'
3627+
const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory());
3628+
symlinkedFiles.set(
3629+
fileOrDirectoryPath,
3630+
`${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`
3631+
);
3632+
}
3633+
return result;
3634+
}
3635+
) || false;
3636+
}
3637+
}
3638+
34633639
/*@internal*/
34643640
export function handleNoEmitOptions(program: ProgramToEmitFilesAndReportErrors, sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): EmitResult | undefined {
34653641
const options = program.getCompilerOptions();

src/compiler/resolutionCache.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ namespace ts {
5757
writeLog(s: string): void;
5858
getCurrentProgram(): Program | undefined;
5959
fileIsOpen(filePath: Path): boolean;
60+
getCompilerHost?(): CompilerHost | undefined;
6061
}
6162

6263
interface DirectoryWatchesOfFailedLookup {
@@ -364,7 +365,7 @@ namespace ts {
364365
resolution = resolutionInDirectory;
365366
}
366367
else {
367-
resolution = loader(name, containingFile, compilerOptions, resolutionHost, redirectedReference);
368+
resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference);
368369
perDirectoryResolution.set(name, resolution);
369370
}
370371
resolutionsInFile.set(name, resolution);

src/compiler/types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5700,7 +5700,6 @@ namespace ts {
57005700
/* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean;
57015701
createHash?(data: string): string;
57025702
getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;
5703-
/* @internal */ setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void;
57045703
/* @internal */ useSourceOfProjectReferenceRedirect?(): boolean;
57055704

57065705
// TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base

src/compiler/watchPublic.ts

+4
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ namespace ts {
114114
}
115115

116116
export interface WatchCompilerHost<T extends BuilderProgram> extends ProgramHost<T>, WatchHost {
117+
/** Instead of using output d.ts file from project reference, use its source file */
118+
useSourceOfProjectReferenceRedirect?(): boolean;
119+
117120
/** If provided, callback to invoke after every new program creation */
118121
afterProgramCreate?(program: T): void;
119122
}
@@ -280,6 +283,7 @@ namespace ts {
280283
// Members for ResolutionCacheHost
281284
compilerHost.toPath = toPath;
282285
compilerHost.getCompilationSettings = () => compilerOptions;
286+
compilerHost.useSourceOfProjectReferenceRedirect = maybeBind(host, host.useSourceOfProjectReferenceRedirect);
283287
compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.FailedLookupLocations);
284288
compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.TypeRoots);
285289
compilerHost.getCachedDirectoryStructureHost = () => cachedDirectoryStructureHost;

src/server/editorServices.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1986,7 +1986,7 @@ namespace ts.server {
19861986
const isDynamic = isDynamicFileName(fileName);
19871987
let path: Path;
19881988
// Use the project's fileExists so that it can use caching instead of reaching to disk for the query
1989-
if (!isDynamic && !project.fileExistsWithCache(newRootFile)) {
1989+
if (!isDynamic && !project.fileExists(newRootFile)) {
19901990
path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
19911991
const existingValue = projectRootFilesMap.get(path);
19921992
if (existingValue) {
@@ -2035,7 +2035,7 @@ namespace ts.server {
20352035
projectRootFilesMap.forEach((value, path) => {
20362036
if (!newRootScriptInfoMap.has(path)) {
20372037
if (value.info) {
2038-
project.removeFile(value.info, project.fileExistsWithCache(path), /*detachFromProject*/ true);
2038+
project.removeFile(value.info, project.fileExists(path), /*detachFromProject*/ true);
20392039
}
20402040
else {
20412041
projectRootFilesMap.delete(path);

0 commit comments

Comments
 (0)