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

This commit is contained in:
Sheetal Nandi 2020-03-12 13:11:11 -07:00 committed by GitHub
parent 1f710167de
commit a76a16696d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 378 additions and 223 deletions

View File

@ -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
@ -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;
@ -970,6 +974,7 @@ namespace ts {
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
};
onProgramCreateComplete();
verifyCompilerOptions();
performance.mark("afterProgram");
performance.measure("Program", "beforeProgram", "afterProgram");
@ -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
@ -3460,6 +3459,183 @@ namespace ts {
}
}
interface SymlinkedDirectory {
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();

View File

@ -57,6 +57,7 @@ namespace ts {
writeLog(s: string): void;
getCurrentProgram(): Program | undefined;
fileIsOpen(filePath: Path): boolean;
getCompilerHost?(): CompilerHost | undefined;
}
interface DirectoryWatchesOfFailedLookup {
@ -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);

View File

@ -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

View File

@ -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;
}
@ -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;

View File

@ -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) {
@ -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);

View File

@ -260,7 +260,7 @@ namespace ts.server {
protected watchOptions: WatchOptions | undefined,
directoryStructureHost: DirectoryStructureHost,
currentDirectory: string | undefined,
customRealpath?: (s: string) => string) {
) {
this.directoryStructureHost = directoryStructureHost;
this.currentDirectory = this.projectService.getNormalizedAbsolutePath(currentDirectory || "");
this.getCanonicalFileName = this.projectService.toCanonicalFileName;
@ -286,10 +286,7 @@ namespace ts.server {
else if (host.trace) {
this.trace = s => host.trace!(s);
}
if (host.realpath) {
this.realpath = customRealpath || (path => host.realpath!(path));
}
this.realpath = maybeBind(host, host.realpath);
// Use the current directory as resolution root only if the project created using current directory string
this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true);
@ -427,11 +424,6 @@ namespace ts.server {
}
fileExists(file: string): boolean {
return this.fileExistsWithCache(file);
}
/* @internal */
fileExistsWithCache(file: string): boolean {
// As an optimization, don't hit the disks for files we already know don't exist
// (because we're watching for their creation).
const path = this.toPath(file);
@ -1746,12 +1738,6 @@ namespace ts.server {
}
}
/*@internal*/
interface SymlinkedDirectory {
real: string;
realPath: Path;
}
/**
* If a file is opened, the server will look for a tsconfig (or jsconfig)
* and if successful create a ConfiguredProject for it.
@ -1763,10 +1749,6 @@ namespace ts.server {
configFileWatcher: FileWatcher | undefined;
private directoriesWatchedForWildcards: Map<WildcardDirectoryWatcher> | undefined;
readonly canonicalConfigFilePath: NormalizedPath;
private projectReferenceCallbacks: ResolvedProjectReferenceCallbacks | undefined;
private mapOfDeclarationDirectories: Map<true> | undefined;
private symlinkedDirectories: Map<SymlinkedDirectory | false> | undefined;
private symlinkedFiles: Map<string> | undefined;
/* @internal */
pendingReload: ConfigFileProgramReloadLevel | undefined;
@ -1802,6 +1784,9 @@ namespace ts.server {
/*@internal*/
sendLoadingProjectFinish = false;
/*@internal*/
private compilerHost?: CompilerHost;
/*@internal*/
constructor(configFileName: NormalizedPath,
projectService: ProjectService,
@ -1818,161 +1803,23 @@ namespace ts.server {
/*watchOptions*/ undefined,
cachedDirectoryStructureHost,
getDirectoryPath(configFileName),
projectService.host.realpath && (s => this.getRealpath(s))
);
this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName));
}
/* @internal */
setResolvedProjectReferenceCallbacks(projectReferenceCallbacks: ResolvedProjectReferenceCallbacks) {
this.projectReferenceCallbacks = projectReferenceCallbacks;
setCompilerHost(host: CompilerHost) {
this.compilerHost = host;
}
/* @internal */
useSourceOfProjectReferenceRedirect = () => !!this.languageServiceEnabled &&
!this.getCompilerOptions().disableSourceOfProjectReferenceRedirect;
private fileExistsIfProjectReferenceDts(file: string) {
const source = this.projectReferenceCallbacks!.getSourceOfProjectReferenceRedirect(file);
return source !== undefined ?
isString(source) ? super.fileExists(source) : true :
undefined;
getCompilerHost(): CompilerHost | undefined {
return this.compilerHost;
}
/**
* 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
*/
fileExists(file: string): boolean {
if (super.fileExists(file)) return true;
if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false;
if (!isDeclarationFileName(file)) return false;
// Project references go to source file instead of .d.ts file
return this.fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
}
private directoryExistsIfProjectReferenceDeclDir(dir: string) {
const dirPath = this.toPath(dir);
const dirPathWithTrailingDirectorySeparator = `${dirPath}${directorySeparator}`;
return forEachKey(
this.mapOfDeclarationDirectories!,
declDirPath => dirPath === declDirPath ||
// Any parent directory of declaration dir
startsWith(declDirPath, dirPathWithTrailingDirectorySeparator) ||
// Any directory inside declaration dir
startsWith(dirPath, `${declDirPath}/`)
);
}
/**
* 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
*/
directoryExists(path: string): boolean {
if (super.directoryExists(path)) {
this.handleDirectoryCouldBeSymlink(path);
return true;
}
if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return false;
if (!this.mapOfDeclarationDirectories) {
this.mapOfDeclarationDirectories = createMap();
this.projectReferenceCallbacks.forEachResolvedProjectReference(ref => {
if (!ref) return;
const out = ref.commandLine.options.outFile || ref.commandLine.options.out;
if (out) {
this.mapOfDeclarationDirectories!.set(getDirectoryPath(this.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) {
this.mapOfDeclarationDirectories!.set(this.toPath(declarationDir), true);
}
}
});
}
return this.fileOrDirectoryExistsUsingSource(path, /*isFile*/ false);
}
/**
* Call super.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
*/
getDirectories(path: string): string[] {
return !this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks || super.directoryExists(path) ?
super.getDirectories(path) :
[];
}
private realpathIfSymlinkedProjectReferenceDts(s: string): string | undefined {
return this.symlinkedFiles && this.symlinkedFiles.get(this.toPath(s));
}
private getRealpath(s: string): string {
return this.realpathIfSymlinkedProjectReferenceDts(s) ||
this.projectService.host.realpath!(s);
}
private handleDirectoryCouldBeSymlink(directory: string) {
if (!this.useSourceOfProjectReferenceRedirect() || !this.projectReferenceCallbacks) return;
// Because we already watch node_modules, handle symlinks in there
if (!this.realpath || !stringContains(directory, nodeModulesPathPart)) return;
if (!this.symlinkedDirectories) this.symlinkedDirectories = createMap();
const directoryPath = ensureTrailingDirectorySeparator(this.toPath(directory));
if (this.symlinkedDirectories.has(directoryPath)) return;
const real = normalizePath(this.projectService.host.realpath!(directory));
let realPath: Path;
if (real === directory ||
(realPath = ensureTrailingDirectorySeparator(this.toPath(real))) === directoryPath) {
// not symlinked
this.symlinkedDirectories.set(directoryPath, false);
return;
}
this.symlinkedDirectories.set(directoryPath, {
real: ensureTrailingDirectorySeparator(real),
realPath
});
}
private fileOrDirectoryExistsUsingSource(fileOrDirectory: string, isFile: boolean): boolean {
const fileOrDirectoryExistsUsingSource = isFile ?
(file: string) => this.fileExistsIfProjectReferenceDts(file) :
(dir: string) => this.directoryExistsIfProjectReferenceDeclDir(dir);
// Check current directory or file
const result = fileOrDirectoryExistsUsingSource(fileOrDirectory);
if (result !== undefined) return result;
if (!this.symlinkedDirectories) return false;
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
if (!stringContains(fileOrDirectoryPath, nodeModulesPathPart)) return false;
if (isFile && this.symlinkedFiles && this.symlinkedFiles.has(fileOrDirectoryPath)) return true;
// If it contains node_modules check if its one of the symlinked path we know of
return firstDefinedIterator(
this.symlinkedDirectories.entries(),
([directoryPath, symlinkedDirectory]) => {
if (!symlinkedDirectory || !startsWith(fileOrDirectoryPath, directoryPath)) return undefined;
const result = fileOrDirectoryExistsUsingSource(fileOrDirectoryPath.replace(directoryPath, symlinkedDirectory.realPath));
if (isFile && result) {
if (!this.symlinkedFiles) this.symlinkedFiles = createMap();
// Store the real path for the file'
const absolutePath = getNormalizedAbsolutePath(fileOrDirectory, this.currentDirectory);
this.symlinkedFiles.set(
fileOrDirectoryPath,
`${symlinkedDirectory.real}${absolutePath.replace(new RegExp(directoryPath, "i"), "")}`
);
}
return result;
}
) || false;
/* @internal */
useSourceOfProjectReferenceRedirect() {
return this.languageServiceEnabled;
}
/* @internal */
@ -2009,10 +1856,6 @@ namespace ts.server {
this.isInitialLoadPending = returnFalse;
const reloadLevel = this.pendingReload;
this.pendingReload = ConfigFileProgramReloadLevel.None;
this.projectReferenceCallbacks = undefined;
this.mapOfDeclarationDirectories = undefined;
this.symlinkedDirectories = undefined;
this.symlinkedFiles = undefined;
let result: boolean;
switch (reloadLevel) {
case ConfigFileProgramReloadLevel.Partial:
@ -2029,6 +1872,7 @@ namespace ts.server {
default:
result = super.updateGraph();
}
this.compilerHost = undefined;
this.projectService.sendProjectLoadingFinishEvent(this);
this.projectService.sendProjectTelemetry(this);
return result;
@ -2146,11 +1990,8 @@ namespace ts.server {
this.stopWatchingWildCards();
this.projectErrors = undefined;
this.configFileSpecs = undefined;
this.projectReferenceCallbacks = undefined;
this.mapOfDeclarationDirectories = undefined;
this.symlinkedDirectories = undefined;
this.symlinkedFiles = undefined;
this.openFileWatchTriggered.clear();
this.compilerHost = undefined;
super.close();
}

View File

@ -1274,12 +1274,10 @@ namespace ts {
if (host.resolveTypeReferenceDirectives) {
compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args);
}
if (host.setResolvedProjectReferenceCallbacks) {
compilerHost.setResolvedProjectReferenceCallbacks = callbacks => host.setResolvedProjectReferenceCallbacks!(callbacks);
}
if (host.useSourceOfProjectReferenceRedirect) {
compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!();
}
host.setCompilerHost?.(compilerHost);
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
const options: CreateProgramOptions = {

View File

@ -274,7 +274,7 @@ namespace ts {
/* @internal */
getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache;
/* @internal */
setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void;
setCompilerHost?(host: CompilerHost): void;
/* @internal */
useSourceOfProjectReferenceRedirect?(): boolean;
}

View File

@ -138,8 +138,9 @@
"unittests/tscWatch/incremental.ts",
"unittests/tscWatch/programUpdates.ts",
"unittests/tscWatch/resolutionCache.ts",
"unittests/tscWatch/watchEnvironment.ts",
"unittests/tscWatch/sourceOfProjectReferenceRedirect.ts",
"unittests/tscWatch/watchApi.ts",
"unittests/tscWatch/watchEnvironment.ts",
"unittests/tsserver/applyChangesToOpenFiles.ts",
"unittests/tsserver/cachingFileSystemInformation.ts",
"unittests/tsserver/cancellationToken.ts",

View File

@ -282,7 +282,7 @@ namespace ts.tscWatch {
scenario: string;
subScenario: string;
commandLineArgs: readonly string[];
changes: TscWatchCompileChange[];
changes: readonly TscWatchCompileChange[];
}
export interface TscWatchCompile extends TscWatchCompileBase {
sys: () => WatchedSystem;

View File

@ -0,0 +1,158 @@
namespace ts.tscWatch {
import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile;
describe("unittests:: tsc-watch:: watchAPI:: with sourceOfProjectReferenceRedirect", () => {
interface VerifyWatchInput {
files: readonly TestFSWithWatch.FileOrFolderOrSymLink[];
config: string;
expectedProgramFiles: readonly string[];
}
function verifyWatch(
{ files, config, expectedProgramFiles }: VerifyWatchInput,
alreadyBuilt: boolean
) {
const sys = createWatchedSystem(files);
if (alreadyBuilt) {
const solutionBuilder = createSolutionBuilder(sys, [config], {});
solutionBuilder.build();
solutionBuilder.close();
sys.clearOutput();
}
const host = createWatchCompilerHostOfConfigFile(config, {}, /*watchOptionsToExtend*/ undefined, sys);
host.useSourceOfProjectReferenceRedirect = returnTrue;
const watch = createWatchProgram(host);
checkProgramActualFiles(watch.getCurrentProgram().getProgram(), expectedProgramFiles);
}
function verifyScenario(input: () => VerifyWatchInput) {
it("when solution is not built", () => {
verifyWatch(input(), /*alreadyBuilt*/ false);
});
it("when solution is already built", () => {
verifyWatch(input(), /*alreadyBuilt*/ true);
});
}
describe("with simple project", () => {
verifyScenario(() => {
const baseConfig = getFileFromProject("demo", "tsconfig-base.json");
const coreTs = getFileFromProject("demo", "core/utilities.ts");
const coreConfig = getFileFromProject("demo", "core/tsconfig.json");
const animalTs = getFileFromProject("demo", "animals/animal.ts");
const dogTs = getFileFromProject("demo", "animals/dog.ts");
const indexTs = getFileFromProject("demo", "animals/index.ts");
const animalsConfig = getFileFromProject("demo", "animals/tsconfig.json");
return {
files: [{ path: libFile.path, content: libContent }, baseConfig, coreTs, coreConfig, animalTs, dogTs, indexTs, animalsConfig],
config: animalsConfig.path,
expectedProgramFiles: [libFile.path, indexTs.path, dogTs.path, animalTs.path, coreTs.path]
};
});
});
describe("when references are monorepo like with symlinks", () => {
interface Packages {
bPackageJson: File;
aTest: File;
bFoo: File;
bBar: File;
bSymlink: SymLink;
}
function verifySymlinkScenario(packages: () => Packages) {
describe("when preserveSymlinks is turned off", () => {
verifySymlinkScenarioWorker(packages, {});
});
describe("when preserveSymlinks is turned on", () => {
verifySymlinkScenarioWorker(packages, { preserveSymlinks: true });
});
}
function verifySymlinkScenarioWorker(packages: () => Packages, extraOptions: CompilerOptions) {
verifyScenario(() => {
const { bPackageJson, aTest, bFoo, bBar, bSymlink } = packages();
const aConfig = config("A", extraOptions, ["../B"]);
const bConfig = config("B", extraOptions);
return {
files: [libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink],
config: aConfig.path,
expectedProgramFiles: [libFile.path, aTest.path, bFoo.path, bBar.path]
};
});
}
function config(packageName: string, extraOptions: CompilerOptions, references?: string[]): File {
return {
path: `${projectRoot}/packages/${packageName}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
outDir: "lib",
rootDir: "src",
composite: true,
...extraOptions
},
include: ["src"],
...(references ? { references: references.map(path => ({ path })) } : {})
})
};
}
function file(packageName: string, fileName: string, content: string): File {
return {
path: `${projectRoot}/packages/${packageName}/src/${fileName}`,
content
};
}
function verifyMonoRepoLike(scope = "") {
describe("when packageJson has types field", () => {
verifySymlinkScenario(() => ({
bPackageJson: {
path: `${projectRoot}/packages/B/package.json`,
content: JSON.stringify({
main: "lib/index.js",
types: "lib/index.d.ts"
})
},
aTest: file("A", "index.ts", `import { foo } from '${scope}b';
import { bar } from '${scope}b/lib/bar';
foo();
bar();
`),
bFoo: file("B", "index.ts", `export function foo() { }`),
bBar: file("B", "bar.ts", `export function bar() { }`),
bSymlink: {
path: `${projectRoot}/node_modules/${scope}b`,
symLink: `${projectRoot}/packages/B`
}
}));
});
describe("when referencing file from subFolder", () => {
verifySymlinkScenario(() => ({
bPackageJson: {
path: `${projectRoot}/packages/B/package.json`,
content: "{}"
},
aTest: file("A", "test.ts", `import { foo } from '${scope}b/lib/foo';
import { bar } from '${scope}b/lib/bar/foo';
foo();
bar();
`),
bFoo: file("B", "foo.ts", `export function foo() { }`),
bBar: file("B", "bar/foo.ts", `export function bar() { }`),
bSymlink: {
path: `${projectRoot}/node_modules/${scope}b`,
symLink: `${projectRoot}/packages/B`
}
}));
});
}
describe("when package is not scoped", () => {
verifyMonoRepoLike();
});
describe("when package is scoped", () => {
verifyMonoRepoLike("@issue/");
});
});
});
}

View File

@ -4758,6 +4758,8 @@ declare namespace ts {
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[];
}
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;
}
@ -8922,37 +8924,10 @@ declare namespace ts.server {
private typeAcquisition;
private directoriesWatchedForWildcards;
readonly canonicalConfigFilePath: NormalizedPath;
private projectReferenceCallbacks;
private mapOfDeclarationDirectories;
private symlinkedDirectories;
private symlinkedFiles;
/** Ref count to the project when opened from external project */
private externalProjectRefCount;
private projectErrors;
private projectReferences;
private fileExistsIfProjectReferenceDts;
/**
* 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
*/
fileExists(file: string): boolean;
private directoryExistsIfProjectReferenceDeclDir;
/**
* 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
*/
directoryExists(path: string): boolean;
/**
* Call super.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
*/
getDirectories(path: string): string[];
private realpathIfSymlinkedProjectReferenceDts;
private getRealpath;
private handleDirectoryCouldBeSymlink;
private fileOrDirectoryExistsUsingSource;
/**
* If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph
* @returns: true if set of files in the project stays the same and false - otherwise.

View File

@ -4758,6 +4758,8 @@ declare namespace ts {
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[];
}
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;
}