mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-15 12:51:30 -05:00
Adds support for inferred project isolation by projectRootPath
This commit is contained in:
@@ -323,6 +323,7 @@ namespace ts.server {
|
||||
logger: Logger;
|
||||
cancellationToken: HostCancellationToken;
|
||||
useSingleInferredProject: boolean;
|
||||
useInferredProjectPerProjectRoot: boolean;
|
||||
typingsInstaller: ITypingsInstaller;
|
||||
eventHandler?: ProjectServiceEventHandler;
|
||||
throttleWaitMilliseconds?: number;
|
||||
@@ -364,7 +365,7 @@ namespace ts.server {
|
||||
readonly openFiles: ScriptInfo[] = [];
|
||||
|
||||
private compilerOptionsForInferredProjects: CompilerOptions;
|
||||
private compileOnSaveForInferredProjects: boolean;
|
||||
private compilerOptionsForInferredProjectsPerProjectRoot = createMap<CompilerOptions>();
|
||||
private readonly projectToSizeMap: Map<number> = createMap<number>();
|
||||
private readonly directoryWatchers: DirectoryWatchers;
|
||||
private readonly throttledOperations: ThrottledOperations;
|
||||
@@ -382,6 +383,7 @@ namespace ts.server {
|
||||
public readonly logger: Logger;
|
||||
public readonly cancellationToken: HostCancellationToken;
|
||||
public readonly useSingleInferredProject: boolean;
|
||||
public readonly useInferredProjectPerProjectRoot: boolean;
|
||||
public readonly typingsInstaller: ITypingsInstaller;
|
||||
public readonly throttleWaitMilliseconds?: number;
|
||||
private readonly eventHandler?: ProjectServiceEventHandler;
|
||||
@@ -398,6 +400,7 @@ namespace ts.server {
|
||||
this.logger = opts.logger;
|
||||
this.cancellationToken = opts.cancellationToken;
|
||||
this.useSingleInferredProject = opts.useSingleInferredProject;
|
||||
this.useInferredProjectPerProjectRoot = opts.useInferredProjectPerProjectRoot;
|
||||
this.typingsInstaller = opts.typingsInstaller || nullTypingsInstaller;
|
||||
this.throttleWaitMilliseconds = opts.throttleWaitMilliseconds;
|
||||
this.eventHandler = opts.eventHandler;
|
||||
@@ -463,17 +466,33 @@ namespace ts.server {
|
||||
project.updateGraph();
|
||||
}
|
||||
|
||||
setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions): void {
|
||||
this.compilerOptionsForInferredProjects = convertCompilerOptions(projectCompilerOptions);
|
||||
setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void {
|
||||
// ignore this settings if we are not creating inferred projects per project root.
|
||||
if (projectRootPath && !this.useInferredProjectPerProjectRoot) return;
|
||||
|
||||
const compilerOptionsForInferredProjects = convertCompilerOptions(projectCompilerOptions);
|
||||
|
||||
// always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside
|
||||
// previously we did not expose a way for user to change these settings and this option was enabled by default
|
||||
this.compilerOptionsForInferredProjects.allowNonTsExtensions = true;
|
||||
this.compileOnSaveForInferredProjects = projectCompilerOptions.compileOnSave;
|
||||
for (const proj of this.inferredProjects) {
|
||||
proj.setCompilerOptions(this.compilerOptionsForInferredProjects);
|
||||
proj.compileOnSaveEnabled = projectCompilerOptions.compileOnSave;
|
||||
compilerOptionsForInferredProjects.allowNonTsExtensions = true;
|
||||
|
||||
if (projectRootPath) {
|
||||
this.compilerOptionsForInferredProjectsPerProjectRoot.set(projectRootPath, compilerOptionsForInferredProjects);
|
||||
}
|
||||
this.updateProjectGraphs(this.inferredProjects);
|
||||
else {
|
||||
this.compilerOptionsForInferredProjects = compilerOptionsForInferredProjects;
|
||||
}
|
||||
|
||||
const updatedProjects: Project[] = [];
|
||||
for (const project of this.inferredProjects) {
|
||||
if (project.projectRootPath === projectRootPath || (project.projectRootPath && !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath))) {
|
||||
project.setCompilerOptions(compilerOptionsForInferredProjects);
|
||||
project.compileOnSaveEnabled = compilerOptionsForInferredProjects.compileOnSave;
|
||||
updatedProjects.push(project);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateProjectGraphs(updatedProjects);
|
||||
}
|
||||
|
||||
stopWatchingDirectory(directory: string) {
|
||||
@@ -713,7 +732,7 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void {
|
||||
private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean, projectRootPath?: string): void {
|
||||
const externalProject = this.findContainingExternalProject(info.fileName);
|
||||
if (externalProject) {
|
||||
// file is already included in some external project - do nothing
|
||||
@@ -741,30 +760,30 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
if (info.containingProjects.length === 0) {
|
||||
// create new inferred project p with the newly opened file as root
|
||||
// or add root to existing inferred project if 'useOneInferredProject' is true
|
||||
const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info);
|
||||
if (!this.useSingleInferredProject) {
|
||||
// if useOneInferredProject is not set then try to fixup ownership of open files
|
||||
// check 'defaultProject !== inferredProject' is necessary to handle cases
|
||||
// when creation inferred project for some file has added other open files into this project (i.e. as referenced files)
|
||||
// we definitely don't want to delete the project that was just created
|
||||
// get (or create) an inferred project using the newly opened file as a root.
|
||||
const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info, projectRootPath);
|
||||
if (!this.useSingleInferredProject && !inferredProject.projectRootPath) {
|
||||
// if useSingleInferredProject is false and the inferred project is not associated
|
||||
// with a project root, then try to repair the ownership of open files.
|
||||
for (const f of this.openFiles) {
|
||||
if (f.containingProjects.length === 0 || !inferredProject.containsScriptInfo(f)) {
|
||||
// this is orphaned file that we have not processed yet - skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const fContainingProject of f.containingProjects) {
|
||||
if (fContainingProject.projectKind === ProjectKind.Inferred &&
|
||||
fContainingProject.isRoot(f) &&
|
||||
fContainingProject !== inferredProject) {
|
||||
|
||||
for (const containingProject of f.containingProjects) {
|
||||
// We verify 'containingProject !== inferredProject' to handle cases
|
||||
// where the inferred project for some file has added other open files
|
||||
// into this project (i.e. as referenced files) as we don't want to
|
||||
// delete the project that was just created
|
||||
if (containingProject.projectKind === ProjectKind.Inferred &&
|
||||
containingProject !== inferredProject &&
|
||||
containingProject.isRoot(f)) {
|
||||
// open file used to be root in inferred project,
|
||||
// this inferred project is different from the one we've just created for current file
|
||||
// and new inferred project references this open file.
|
||||
// We should delete old inferred project and attach open file to the new one
|
||||
this.removeProject(fContainingProject);
|
||||
this.removeProject(containingProject);
|
||||
f.attachToProject(inferredProject);
|
||||
}
|
||||
}
|
||||
@@ -1285,11 +1304,65 @@ namespace ts.server {
|
||||
return configFileErrors;
|
||||
}
|
||||
|
||||
createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) {
|
||||
const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length;
|
||||
const project = useExistingProject
|
||||
? this.inferredProjects[0]
|
||||
: new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects);
|
||||
private getOrCreateInferredProjectForProjectRootPathIfEnabled(root: ScriptInfo, projectRootPath: string | undefined): InferredProject | undefined {
|
||||
if (!this.useInferredProjectPerProjectRoot) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (projectRootPath) {
|
||||
// if we have an explicit project root path, find (or create) the matching inferred project.
|
||||
for (const project of this.inferredProjects) {
|
||||
if (project.projectRootPath === projectRootPath) {
|
||||
return project;
|
||||
}
|
||||
}
|
||||
return this.createInferredProject(/*isSingleInferredProject*/ false, projectRootPath);
|
||||
}
|
||||
|
||||
// we don't have an explicit root path, so we should try to find an inferred project that best matches the file.
|
||||
let bestMatch: InferredProject;
|
||||
for (const project of this.inferredProjects) {
|
||||
// ignore single inferred projects (handled elsewhere)
|
||||
if (!project.projectRootPath) continue;
|
||||
// ignore inferred projects that don't contain the root's path
|
||||
if (!containsPath(project.projectRootPath, root.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue;
|
||||
// ignore inferred projects that are higher up in the project root.
|
||||
// TODO(rbuckton): Should we add the file as a root to these as well?
|
||||
if (bestMatch && bestMatch.projectRootPath.length > project.projectRootPath.length) continue;
|
||||
bestMatch = project;
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
private getOrCreateSingleInferredProjectIfEnabled(): InferredProject | undefined {
|
||||
if (!this.useSingleInferredProject) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.inferredProjects.length > 0 && this.inferredProjects[0].projectRootPath === undefined) {
|
||||
return this.inferredProjects[0];
|
||||
}
|
||||
|
||||
return this.createInferredProject(/*isSingleInferredProject*/ true);
|
||||
}
|
||||
|
||||
private createInferredProject(isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject {
|
||||
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
|
||||
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath);
|
||||
if (isSingleInferredProject) {
|
||||
this.inferredProjects.unshift(project);
|
||||
}
|
||||
else {
|
||||
this.inferredProjects.push(project);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
createInferredProjectWithRootFileIfNecessary(root: ScriptInfo, projectRootPath?: string) {
|
||||
const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(root, projectRootPath) ||
|
||||
this.getOrCreateSingleInferredProjectIfEnabled() ||
|
||||
this.createInferredProject();
|
||||
|
||||
project.addRoot(root);
|
||||
|
||||
@@ -1300,9 +1373,6 @@ namespace ts.server {
|
||||
|
||||
project.updateGraph();
|
||||
|
||||
if (!useExistingProject) {
|
||||
this.inferredProjects.push(project);
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
@@ -1476,7 +1546,7 @@ namespace ts.server {
|
||||
|
||||
// at this point if file is the part of some configured/external project then this project should be created
|
||||
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
|
||||
this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true);
|
||||
this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true, projectRootPath);
|
||||
// Delete the orphan files here because there might be orphan script infos (which are not part of project)
|
||||
// when some file/s were closed which resulted in project removal.
|
||||
// It was then postponed to cleanup these script infos so that they can be reused if
|
||||
|
||||
@@ -836,6 +836,7 @@ namespace ts.server {
|
||||
* the file and its imports/references are put into an InferredProject.
|
||||
*/
|
||||
export class InferredProject extends Project {
|
||||
public readonly projectRootPath: string | undefined;
|
||||
|
||||
private static readonly newName = (() => {
|
||||
let nextId = 1;
|
||||
@@ -875,7 +876,7 @@ namespace ts.server {
|
||||
// Used to keep track of what directories are watched for this project
|
||||
directoriesWatchedForTsconfig: string[] = [];
|
||||
|
||||
constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions) {
|
||||
constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, projectRootPath?: string) {
|
||||
super(InferredProject.newName(),
|
||||
ProjectKind.Inferred,
|
||||
projectService,
|
||||
@@ -884,6 +885,7 @@ namespace ts.server {
|
||||
/*languageServiceEnabled*/ true,
|
||||
compilerOptions,
|
||||
/*compileOnSaveEnabled*/ false);
|
||||
this.projectRootPath = projectRootPath;
|
||||
}
|
||||
|
||||
addRoot(info: ScriptInfo) {
|
||||
|
||||
@@ -1304,6 +1304,13 @@ namespace ts.server.protocol {
|
||||
* Compiler options to be used with inferred projects.
|
||||
*/
|
||||
options: ExternalProjectCompilerOptions;
|
||||
|
||||
/**
|
||||
* Specifies the project root path used to scope commpiler options.
|
||||
* This message is ignored if this property has been specified and the server is not
|
||||
* configured to create an inferred project per project root.
|
||||
*/
|
||||
projectRootPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ts.server {
|
||||
canUseEvents: boolean;
|
||||
installerEventPort: number;
|
||||
useSingleInferredProject: boolean;
|
||||
useInferredProjectPerProjectRoot: boolean;
|
||||
disableAutomaticTypingAcquisition: boolean;
|
||||
globalTypingsCacheLocation: string;
|
||||
logger: Logger;
|
||||
@@ -410,6 +411,7 @@ namespace ts.server {
|
||||
host,
|
||||
cancellationToken,
|
||||
useSingleInferredProject,
|
||||
useInferredProjectPerProjectRoot,
|
||||
typingsInstaller: typingsInstaller || nullTypingsInstaller,
|
||||
byteLength: Buffer.byteLength,
|
||||
hrtime: process.hrtime,
|
||||
@@ -765,6 +767,7 @@ namespace ts.server {
|
||||
const allowLocalPluginLoads = hasArgument("--allowLocalPluginLoads");
|
||||
|
||||
const useSingleInferredProject = hasArgument("--useSingleInferredProject");
|
||||
const useInferredProjectPerProjectRoot = hasArgument("--useInferredProjectPerProjectRoot");
|
||||
const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition");
|
||||
const telemetryEnabled = hasArgument(Arguments.EnableTelemetry);
|
||||
|
||||
@@ -774,6 +777,7 @@ namespace ts.server {
|
||||
installerEventPort: eventPort,
|
||||
canUseEvents: eventPort === undefined,
|
||||
useSingleInferredProject,
|
||||
useInferredProjectPerProjectRoot,
|
||||
disableAutomaticTypingAcquisition,
|
||||
globalTypingsCacheLocation: getGlobalTypingsCacheLocation(),
|
||||
typingSafeListLocation,
|
||||
|
||||
@@ -251,6 +251,7 @@ namespace ts.server {
|
||||
host: ServerHost;
|
||||
cancellationToken: ServerCancellationToken;
|
||||
useSingleInferredProject: boolean;
|
||||
useInferredProjectPerProjectRoot: boolean;
|
||||
typingsInstaller: ITypingsInstaller;
|
||||
byteLength: (buf: string, encoding?: string) => number;
|
||||
hrtime: (start?: number[]) => number[];
|
||||
@@ -311,6 +312,7 @@ namespace ts.server {
|
||||
logger: this.logger,
|
||||
cancellationToken: this.cancellationToken,
|
||||
useSingleInferredProject: opts.useSingleInferredProject,
|
||||
useInferredProjectPerProjectRoot: opts.useInferredProjectPerProjectRoot,
|
||||
typingsInstaller: this.typingsInstaller,
|
||||
throttleWaitMilliseconds,
|
||||
eventHandler: this.eventHandler,
|
||||
@@ -744,7 +746,7 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void {
|
||||
this.projectService.setCompilerOptionsForInferredProjects(args.options);
|
||||
this.projectService.setCompilerOptionsForInferredProjects(args.options, args.projectRootPath);
|
||||
}
|
||||
|
||||
private getProjectInfo(args: protocol.ProjectInfoRequestArgs): protocol.ProjectInfo {
|
||||
|
||||
Reference in New Issue
Block a user