Skip to content

Move useSourceOfProjectReferenceRedirect to program so other hosts can use it too, enabling it for WatchHost #37370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 189 additions & 13 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,17 @@ namespace ts {
let projectReferenceRedirects: Map<ResolvedProjectReference | false> | undefined;
let mapFromFileToProjectReferenceRedirects: Map<Path> | undefined;
let mapFromToProjectReferenceRedirectSource: Map<SourceOfProjectReferenceRedirect> | undefined;
const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect && host.useSourceOfProjectReferenceRedirect();

const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() &&
!options.disableSourceOfProjectReferenceRedirect;
const onProgramCreateComplete = updateHostForUseSourceOfProjectReferenceRedirect({
compilerHost: host,
useSourceOfProjectReferenceRedirect,
toPath,
getResolvedProjectReferences,
getSourceOfProjectReferenceRedirect,
forEachResolvedProjectReference
});

const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
// We set `structuralIsReused` to `undefined` because `tryReuseStructureFromOldProgram` calls `tryReuseStructureFromOldProgram` which checks
Expand All @@ -821,12 +831,6 @@ namespace ts {
if (!resolvedProjectReferences) {
resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile);
}
if (host.setResolvedProjectReferenceCallbacks) {
host.setResolvedProjectReferenceCallbacks({
getSourceOfProjectReferenceRedirect,
forEachResolvedProjectReference
});
}
if (rootNames.length) {
for (const parsedRef of resolvedProjectReferences) {
if (!parsedRef) continue;
Expand Down Expand Up @@ -970,6 +974,7 @@ namespace ts {
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
};

onProgramCreateComplete();
verifyCompilerOptions();
performance.mark("afterProgram");
performance.measure("Program", "beforeProgram", "afterProgram");
Expand Down Expand Up @@ -1248,12 +1253,6 @@ namespace ts {
}
if (projectReferences) {
resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile);
if (host.setResolvedProjectReferenceCallbacks) {
host.setResolvedProjectReferenceCallbacks({
getSourceOfProjectReferenceRedirect,
forEachResolvedProjectReference
});
}
}

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

interface SymlinkedDirectory {
Copy link
Member Author

Choose a reason for hiding this comment

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

All the new code is just moved from ConfiguredProject to program as part of this change.. Rest is threading through things

real: string;
realPath: Path;
}

interface HostForUseSourceOfProjectReferenceRedirect {
compilerHost: CompilerHost;
useSourceOfProjectReferenceRedirect: boolean;
toPath(fileName: string): Path;
getResolvedProjectReferences(): readonly (ResolvedProjectReference | undefined)[] | undefined;
getSourceOfProjectReferenceRedirect(fileName: string): SourceOfProjectReferenceRedirect | undefined;
forEachResolvedProjectReference<T>(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined;
}

function updateHostForUseSourceOfProjectReferenceRedirect(host: HostForUseSourceOfProjectReferenceRedirect) {
let mapOfDeclarationDirectories: Map<true> | undefined;
let symlinkedDirectories: Map<SymlinkedDirectory | false> | undefined;
let symlinkedFiles: Map<string> | undefined;

const originalFileExists = host.compilerHost.fileExists;
const originalDirectoryExists = host.compilerHost.directoryExists;
const originalGetDirectories = host.compilerHost.getDirectories;
const originalRealpath = host.compilerHost.realpath;


if (!host.useSourceOfProjectReferenceRedirect) return noop;

// This implementation of fileExists checks if the file being requested is
// .d.ts file for the referenced Project.
// If it is it returns true irrespective of whether that file exists on host
host.compilerHost.fileExists = (file) => {
if (originalFileExists.call(host.compilerHost, file)) return true;
if (!host.getResolvedProjectReferences()) return false;
if (!isDeclarationFileName(file)) return false;

// Project references go to source file instead of .d.ts file
return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
};

if (originalDirectoryExists) {
// This implementation of directoryExists checks if the directory being requested is
// directory of .d.ts file for the referenced Project.
// If it is it returns true irrespective of whether that directory exists on host
host.compilerHost.directoryExists = path => {
if (originalDirectoryExists.call(host.compilerHost, path)) {
handleDirectoryCouldBeSymlink(path);
return true;
}

if (!host.getResolvedProjectReferences()) return false;

if (!mapOfDeclarationDirectories) {
mapOfDeclarationDirectories = createMap();
host.forEachResolvedProjectReference(ref => {
if (!ref) return;
const out = ref.commandLine.options.outFile || ref.commandLine.options.out;
if (out) {
mapOfDeclarationDirectories!.set(getDirectoryPath(host.toPath(out)), true);
}
else {
// Set declaration's in different locations only, if they are next to source the directory present doesnt change
const declarationDir = ref.commandLine.options.declarationDir || ref.commandLine.options.outDir;
if (declarationDir) {
mapOfDeclarationDirectories!.set(host.toPath(declarationDir), true);
}
}
});
}

return fileOrDirectoryExistsUsingSource(path, /*isFile*/ false);
};
}

if (originalGetDirectories) {
// Call getDirectories only if directory actually present on the host
// This is needed to ensure that we arent getting directories that we fake about presence for
host.compilerHost.getDirectories = path =>
!host.getResolvedProjectReferences() || (originalDirectoryExists && originalDirectoryExists.call(host.compilerHost, path)) ?
originalGetDirectories.call(host.compilerHost, path) :
[];
}

// This is something we keep for life time of the host
if (originalRealpath) {
host.compilerHost.realpath = s =>
symlinkedFiles?.get(host.toPath(s)) ||
originalRealpath.call(host.compilerHost, s);
}

return onProgramCreateComplete;


function onProgramCreateComplete() {
host.compilerHost.fileExists = originalFileExists;
host.compilerHost.directoryExists = originalDirectoryExists;
host.compilerHost.getDirectories = originalGetDirectories;
// DO not revert realpath as it could be used later
}

function fileExistsIfProjectReferenceDts(file: string) {
const source = host.getSourceOfProjectReferenceRedirect(file);
return source !== undefined ?
isString(source) ? originalFileExists.call(host.compilerHost, source) : true :
undefined;
}

function directoryExistsIfProjectReferenceDeclDir(dir: string) {
const dirPath = host.toPath(dir);
const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`;
return forEachKey(
mapOfDeclarationDirectories!,
declDirPath => dirPath === declDirPath ||
// Any parent directory of declaration dir
startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) ||
// Any directory inside declaration dir
startsWith(dirPath, `${declDirPath}/`)
);
}

function handleDirectoryCouldBeSymlink(directory: string) {
if (!host.getResolvedProjectReferences()) return;

// Because we already watch node_modules, handle symlinks in there
if (!originalRealpath || !stringContains(directory, nodeModulesPathPart)) return;
if (!symlinkedDirectories) symlinkedDirectories = createMap();
const directoryPath = ensureTrailingDirectorySeparator(host.toPath(directory));
if (symlinkedDirectories.has(directoryPath)) return;

const real = normalizePath(originalRealpath.call(host.compilerHost, directory));
let realPath: Path;
if (real === directory ||
(realPath = ensureTrailingDirectorySeparator(host.toPath(real))) === directoryPath) {
// not symlinked
symlinkedDirectories.set(directoryPath, false);
return;
}

symlinkedDirectories.set(directoryPath, {
real: ensureTrailingDirectorySeparator(real),
realPath
});
}

function fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean {
const fileOrDirectoryExistsUsingSource = isFile ?
(file: string) => fileExistsIfProjectReferenceDts(file) :
(dir: string) => directoryExistsIfProjectReferenceDeclDir(dir);
// Check current directory or file
const result = fileOrDirectoryExistsUsingSource(fileOrDirectory);
if (result !== undefined) return result;

if (!symlinkedDirectories) return false;
const fileOrDirectoryPath = host.toPath(fileOrDirectory);
if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false;
if (isFile && symlinkedFiles && symlinkedFiles.has(fileOrDirectoryPath)) return true;

// If it contains node_modules check if its one of the symlinked path we know of
return firstDefinedIterator(
symlinkedDirectories.entries(),
([directoryPath, symlinkedDirectory]) => {
if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined;
const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath));
if (isFile && result) {
if (!symlinkedFiles) symlinkedFiles = createMap();
// Store the real path for the file'
const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, host.compilerHost.getCurrentDirectory());
symlinkedFiles.set(
fileOrDirectoryPath,
`${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`
);
}
return result;
}
) || false;
}
}

/*@internal*/
export function handleNoEmitOptions(program: ProgramToEmitFilesAndReportErrors, sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): EmitResult | undefined {
const options = program.getCompilerOptions();
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ namespace ts {
writeLog(s: string): void;
getCurrentProgram(): Program | undefined;
fileIsOpen(filePath: Path): boolean;
getCompilerHost?(): CompilerHost | undefined;
}

interface DirectoryWatchesOfFailedLookup {
Expand Down Expand Up @@ -364,7 +365,7 @@ namespace ts {
resolution = resolutionInDirectory;
}
else {
resolution = loader(name, containingFile, compilerOptions, resolutionHost, redirectedReference);
resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference);
perDirectoryResolution.set(name, resolution);
}
resolutionsInFile.set(name, resolution);
Expand Down
1 change: 0 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5700,7 +5700,6 @@ namespace ts {
/* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean;
createHash?(data: string): string;
getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;
/* @internal */ setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void;
/* @internal */ useSourceOfProjectReferenceRedirect?(): boolean;

// TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ namespace ts {
}

export interface WatchCompilerHost<T extends BuilderProgram> extends ProgramHost<T>, WatchHost {
/** Instead of using output d.ts file from project reference, use its source file */
useSourceOfProjectReferenceRedirect?(): boolean;

/** If provided, callback to invoke after every new program creation */
afterProgramCreate?(program: T): void;
}
Expand Down Expand Up @@ -280,6 +283,7 @@ namespace ts {
// Members for ResolutionCacheHost
compilerHost.toPath = toPath;
compilerHost.getCompilationSettings = () => compilerOptions;
compilerHost.useSourceOfProjectReferenceRedirect = maybeBind(host, host.useSourceOfProjectReferenceRedirect);
compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.FailedLookupLocations);
compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.TypeRoots);
compilerHost.getCachedDirectoryStructureHost = () => cachedDirectoryStructureHost;
Expand Down
4 changes: 2 additions & 2 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1986,7 +1986,7 @@ namespace ts.server {
const isDynamic = isDynamicFileName(fileName);
let path: Path;
// Use the project's fileExists so that it can use caching instead of reaching to disk for the query
if (!isDynamic && !project.fileExistsWithCache(newRootFile)) {
if (!isDynamic && !project.fileExists(newRootFile)) {
path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
const existingValue = projectRootFilesMap.get(path);
if (existingValue) {
Expand Down Expand Up @@ -2035,7 +2035,7 @@ namespace ts.server {
projectRootFilesMap.forEach((value, path) => {
if (!newRootScriptInfoMap.has(path)) {
if (value.info) {
project.removeFile(value.info, project.fileExistsWithCache(path), /*detachFromProject*/ true);
project.removeFile(value.info, project.fileExists(path), /*detachFromProject*/ true);
}
else {
projectRootFilesMap.delete(path);
Expand Down
Loading