TypeScript/src/server/project.ts

901 lines
35 KiB
TypeScript
Raw Normal View History

/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts"/>
2016-06-22 02:31:54 +02:00
/// <reference path="scriptInfo.ts"/>
2016-06-25 02:27:18 +02:00
/// <reference path="lsHost.ts"/>
2016-08-12 20:04:43 +02:00
/// <reference path="typingsCache.ts"/>
/// <reference path="builder.ts"/>
2016-06-22 02:31:54 +02:00
namespace ts.server {
2016-06-22 02:31:54 +02:00
export enum ProjectKind {
Inferred,
Configured,
External
}
function remove<T>(items: T[], item: T) {
const index = items.indexOf(item);
if (index >= 0) {
items.splice(index, 1);
}
}
function countEachFileTypes(infos: ScriptInfo[]): { js: number, jsx: number, ts: number, tsx: number, dts: number } {
const result = { js: 0, jsx: 0, ts: 0, tsx: 0, dts: 0 };
for (const info of infos) {
switch (info.scriptKind) {
case ScriptKind.JS:
result.js += 1;
break;
case ScriptKind.JSX:
result.jsx += 1;
break;
case ScriptKind.TS:
fileExtensionIs(info.fileName, ".d.ts")
? result.dts += 1
: result.ts += 1;
break;
case ScriptKind.TSX:
result.tsx += 1;
break;
}
}
return result;
}
function hasOneOrMoreJsAndNoTsFiles(project: Project) {
const counts = countEachFileTypes(project.getScriptInfos());
return counts.js > 0 && counts.ts === 0 && counts.tsx === 0;
}
export function allRootFilesAreJsOrDts(project: Project): boolean {
const counts = countEachFileTypes(project.getRootScriptInfos());
return counts.ts === 0 && counts.tsx === 0;
}
export function allFilesAreJsOrDts(project: Project): boolean {
const counts = countEachFileTypes(project.getScriptInfos());
return counts.ts === 0 && counts.tsx === 0;
}
export interface ProjectFilesWithTSDiagnostics extends protocol.ProjectFiles {
projectErrors: Diagnostic[];
}
export class UnresolvedImportsMap {
readonly perFileMap = createFileMap<ReadonlyArray<string>>();
private version = 0;
public clear() {
this.perFileMap.clear();
this.version = 0;
}
public getVersion() {
return this.version;
}
public remove(path: Path) {
this.perFileMap.remove(path);
this.version++;
}
public get(path: Path) {
return this.perFileMap.get(path);
}
public set(path: Path, value: ReadonlyArray<string>) {
this.perFileMap.set(path, value);
this.version++;
}
}
2016-06-22 02:31:54 +02:00
export abstract class Project {
2016-06-28 00:29:04 +02:00
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
2016-06-22 02:31:54 +02:00
private lsHost: ServerLanguageServiceHost;
private program: ts.Program;
2016-06-22 02:31:54 +02:00
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
private languageService: LanguageService;
builder: Builder;
/**
* Set of files that was returned from the last call to getChangesSinceVersion.
*/
private lastReportedFileNames: Map<string>;
/**
* Last version that was reported.
*/
private lastReportedVersion = 0;
/**
* Current project structure version.
* This property is changed in 'updateGraph' based on the set of files in program
*/
private projectStructureVersion = 0;
/**
* Current version of the project state. It is changed when:
* - new root file was added/removed
* - edit happen in some file that is currently included in the project.
* This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project
*/
private projectStateVersion = 0;
private typingFiles: SortedReadonlyArray<string>;
2016-08-12 20:04:43 +02:00
protected projectErrors: Diagnostic[];
public typesVersion = 0;
public isNonTsProject() {
this.updateGraph();
return allFilesAreJsOrDts(this);
}
public isJsOnlyProject() {
this.updateGraph();
return hasOneOrMoreJsAndNoTsFiles(this);
}
public getCachedUnresolvedImportsPerFile_TestOnly() {
return this.cachedUnresolvedImportsPerFile;
}
2016-06-22 02:31:54 +02:00
constructor(
readonly projectKind: ProjectKind,
readonly projectService: ProjectService,
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
private compilerOptions: CompilerOptions,
public compileOnSaveEnabled: boolean) {
2016-06-22 02:31:54 +02:00
if (!this.compilerOptions) {
this.compilerOptions = ts.getDefaultCompilerOptions();
this.compilerOptions.allowNonTsExtensions = true;
this.compilerOptions.allowJs = true;
}
else if (hasExplicitListOfFiles) {
// If files are listed explicitly, allow all extensions
this.compilerOptions.allowNonTsExtensions = true;
}
if (this.projectKind === ProjectKind.Inferred) {
this.compilerOptions.noEmitOverwritenFiles = true;
}
2016-06-22 02:31:54 +02:00
if (languageServiceEnabled) {
this.enableLanguageService();
}
else {
this.disableLanguageService();
}
this.builder = createBuilder(this);
2016-06-22 02:31:54 +02:00
this.markAsDirty();
}
getProjectErrors() {
return this.projectErrors;
}
2016-08-12 20:04:43 +02:00
getLanguageService(ensureSynchronized = true): LanguageService {
if (ensureSynchronized) {
this.updateGraph();
}
return this.languageService;
}
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
if (!this.languageServiceEnabled) {
return [];
}
this.updateGraph();
return this.builder.getFilesAffectedBy(scriptInfo);
}
2016-06-22 02:31:54 +02:00
getProjectVersion() {
return this.projectStateVersion.toString();
2016-06-22 02:31:54 +02:00
}
enableLanguageService() {
const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
lsHost.setCompilationSettings(this.compilerOptions);
this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);
this.lsHost = lsHost;
this.languageServiceEnabled = true;
}
disableLanguageService() {
this.languageService = nullLanguageService;
this.lsHost = nullLanguageServiceHost;
this.languageServiceEnabled = false;
}
abstract getProjectName(): string;
abstract getProjectRootPath(): string | undefined;
abstract getTypingOptions(): TypingOptions;
2016-06-22 02:31:54 +02:00
getSourceFile(path: Path) {
if (!this.program) {
return undefined;
}
return this.program.getSourceFileByPath(path);
}
updateTypes() {
this.typesVersion++;
this.markAsDirty();
this.updateGraph();
}
2016-06-22 02:31:54 +02:00
close() {
2016-06-28 00:29:04 +02:00
if (this.program) {
// if we have a program - release all files that are enlisted in program
for (const f of this.program.getSourceFiles()) {
const info = this.projectService.getScriptInfo(f.fileName);
info.detachFromProject(this);
}
2016-06-22 02:31:54 +02:00
}
2016-06-28 00:29:04 +02:00
else {
// release all root files
for (const root of this.rootFiles) {
root.detachFromProject(this);
}
}
this.rootFiles = undefined;
this.rootFilesMap = undefined;
this.program = undefined;
// signal language service to release source files acquired from document registry
2016-06-22 02:31:54 +02:00
this.languageService.dispose();
}
getCompilerOptions() {
return this.compilerOptions;
}
hasRoots() {
return this.rootFiles && this.rootFiles.length > 0;
}
2016-06-22 02:31:54 +02:00
getRootFiles() {
return this.rootFiles && this.rootFiles.map(info => info.fileName);
2016-06-22 02:31:54 +02:00
}
2016-08-12 20:04:43 +02:00
getRootFilesLSHost() {
const result: string[] = [];
if (this.rootFiles) {
for (const f of this.rootFiles) {
result.push(f.fileName);
}
if (this.typingFiles) {
for (const f of this.typingFiles) {
result.push(f);
}
}
}
return result;
}
2016-07-06 00:51:39 +02:00
getRootScriptInfos() {
return this.rootFiles;
}
getScriptInfos() {
return map(this.program.getSourceFiles(), sourceFile => {
const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path);
if (!scriptInfo) {
Debug.assert(false, `scriptInfo for a file '${sourceFile.fileName}' is missing.`);
}
return scriptInfo;
});
}
getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) {
if (!this.languageServiceEnabled) {
return undefined;
}
return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
}
2016-06-22 02:31:54 +02:00
getFileNames() {
if (!this.program) {
return [];
}
2016-06-22 02:31:54 +02:00
if (!this.languageServiceEnabled) {
// if language service is disabled assume that all files in program are root files + default library
let rootFiles = this.getRootFiles();
if (this.compilerOptions) {
const defaultLibrary = getDefaultLibFilePath(this.compilerOptions);
if (defaultLibrary) {
(rootFiles || (rootFiles = [])).push(asNormalizedPath(defaultLibrary));
}
}
return rootFiles;
}
const sourceFiles = this.program.getSourceFiles();
return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName));
}
getAllEmittableFiles() {
if (!this.languageServiceEnabled) {
return [];
}
const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions);
const infos = this.getScriptInfos();
const result: string[] = [];
for (const info of infos) {
if (getBaseFileName(info.fileName) !== defaultLibraryFileName && shouldEmitFile(info)) {
result.push(info.fileName);
}
}
return result;
}
2016-06-22 02:31:54 +02:00
containsScriptInfo(info: ScriptInfo): boolean {
2016-06-28 00:29:04 +02:00
return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined);
2016-06-22 02:31:54 +02:00
}
containsFile(filename: NormalizedPath, requireOpen?: boolean) {
const info = this.projectService.getScriptInfoForNormalizedPath(filename);
if (info && (info.isOpen || !requireOpen)) {
return this.containsScriptInfo(info);
2016-06-22 02:31:54 +02:00
}
}
isRoot(info: ScriptInfo) {
return this.rootFilesMap && this.rootFilesMap.contains(info.path);
2016-06-22 02:31:54 +02:00
}
// add a root file to project
addRoot(info: ScriptInfo) {
if (!this.isRoot(info)) {
this.rootFiles.push(info);
this.rootFilesMap.set(info.path, info);
info.attachToProject(this);
2016-06-22 02:31:54 +02:00
this.markAsDirty();
}
}
removeFile(info: ScriptInfo, detachFromProject = true) {
this.removeRootFileIfNecessary(info);
this.lsHost.notifyFileRemoved(info);
this.cachedUnresolvedImportsPerFile.remove(info.path);
if (detachFromProject) {
info.detachFromProject(this);
}
2016-06-22 02:31:54 +02:00
this.markAsDirty();
}
markAsDirty() {
this.projectStateVersion++;
2016-06-22 02:31:54 +02:00
}
private extractUnresolvedImportsFromSourceFile(file: SourceFile, result: string[]) {
const cached = this.cachedUnresolvedImportsPerFile.get(file.path);
if (cached) {
// found cached result - use it and return
for (const f of cached) {
result.push(f);
}
return;
}
let unresolvedImports: string[];
if (file.resolvedModules) {
for (const name in file.resolvedModules) {
// pick unresolved non-relative names
if (!file.resolvedModules[name] && !isExternalModuleNameRelative(name)) {
// for non-scoped names extract part up-to the first slash
// for scoped names - extract up to the second slash
let trimmed = name.trim();
let i = trimmed.indexOf("/");
if (i !== -1 && trimmed.charCodeAt(0) === CharacterCodes.at) {
i = trimmed.indexOf("/", i + 1);
}
if (i !== -1) {
trimmed = trimmed.substr(0, i);
}
(unresolvedImports || (unresolvedImports = [])).push(trimmed);
result.push(trimmed);
}
}
}
this.cachedUnresolvedImportsPerFile.set(file.path, unresolvedImports || emptyArray);
}
2016-06-30 22:23:55 +02:00
/**
* Updates set of files that contribute to this project
* @returns: true if set of files in the project stays the same and false - otherwise.
*/
updateGraph(): boolean {
if (!this.languageServiceEnabled) {
return true;
}
this.lsHost.startRecordingFilesWithChangedResolutions();
let hasChanges = this.updateGraphWorker();
const changedFiles: ReadonlyArray<Path> = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray;
for (const file of changedFiles) {
// delete cached information for changed files
this.cachedUnresolvedImportsPerFile.remove(file);
}
// 1. no changes in structure, no changes in unresolved imports - do nothing
// 2. no changes in structure, unresolved imports were changed - collect unresolved imports for all files
// (can reuse cached imports for files that were not changed)
// 3. new files were added/removed, but compilation settings stays the same - collect unresolved imports for all new/modified files
// (can reuse cached imports for files that were not changed)
// 4. compilation settings were changed in the way that might affect module resolution - drop all caches and collect all data from the scratch
let unresolvedImports: SortedReadonlyArray<string>;
if (hasChanges || changedFiles.length) {
const result: string[] = [];
for (const sourceFile of this.program.getSourceFiles()) {
this.extractUnresolvedImportsFromSourceFile(sourceFile, result);
}
this.lastCachedUnresolvedImportsList = toSortedReadonlyArray(result);
}
unresolvedImports = this.lastCachedUnresolvedImportsList;
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, unresolvedImports, hasChanges);
if (this.setTypings(cachedTypings)) {
hasChanges = this.updateGraphWorker() || hasChanges;
}
2016-08-12 20:04:43 +02:00
if (hasChanges) {
this.projectStructureVersion++;
}
return !hasChanges;
2016-08-12 20:04:43 +02:00
}
private setTypings(typings: SortedReadonlyArray<string>): boolean {
if (arrayIsEqualTo(this.typingFiles, typings)) {
2016-08-12 20:04:43 +02:00
return false;
}
this.typingFiles = typings;
this.markAsDirty();
return true;
}
2016-08-12 20:04:43 +02:00
private updateGraphWorker() {
const oldProgram = this.program;
2016-06-22 02:31:54 +02:00
this.program = this.languageService.getProgram();
2016-08-12 20:04:43 +02:00
let hasChanges = false;
// bump up the version if
// - oldProgram is not set - this is a first time updateGraph is called
// - newProgram is different from the old program and structure of the old program was not reused.
if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) {
2016-08-12 20:04:43 +02:00
hasChanges = true;
if (oldProgram) {
for (const f of oldProgram.getSourceFiles()) {
if (this.program.getSourceFileByPath(f.path)) {
continue;
}
// new program does not contain this file - detach it from the project
const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName);
if (scriptInfoToDetach) {
scriptInfoToDetach.detachFromProject(this);
}
}
}
}
this.builder.onProjectUpdateGraph();
2016-08-12 20:04:43 +02:00
return hasChanges;
2016-06-22 02:31:54 +02:00
}
getScriptInfoLSHost(fileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
if (scriptInfo) {
scriptInfo.attachToProject(this);
}
return scriptInfo;
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
if (scriptInfo && !scriptInfo.isAttached(this)) {
return Errors.ThrowProjectDoesNotContainDocument(fileName, this);
}
2016-06-22 02:31:54 +02:00
return scriptInfo;
}
getScriptInfo(uncheckedFileName: string) {
return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
}
2016-06-22 02:31:54 +02:00
filesToString() {
if (!this.program) {
return "";
}
let strBuilder = "";
for (const file of this.program.getSourceFiles()) {
strBuilder += `${file.fileName}\n`;
}
return strBuilder;
}
setCompilerOptions(compilerOptions: CompilerOptions) {
if (compilerOptions) {
if (this.projectKind === ProjectKind.Inferred) {
compilerOptions.allowJs = true;
}
2016-06-22 02:31:54 +02:00
compilerOptions.allowNonTsExtensions = true;
if (changesAffectModuleResolution(this.compilerOptions, compilerOptions)) {
// reset cached unresolved imports if changes in compiler options affected module resolution
this.cachedUnresolvedImportsPerFile.clear();
this.lastCachedUnresolvedImportsList = undefined;
}
2016-06-22 02:31:54 +02:00
this.compilerOptions = compilerOptions;
this.lsHost.setCompilationSettings(compilerOptions);
this.markAsDirty();
}
}
reloadScript(filename: NormalizedPath, tempFileName?: NormalizedPath): boolean {
const script = this.projectService.getScriptInfoForNormalizedPath(filename);
2016-06-22 02:31:54 +02:00
if (script) {
Debug.assert(script.isAttached(this));
script.reloadFromFile(tempFileName);
return true;
2016-06-22 02:31:54 +02:00
}
return false;
2016-06-22 02:31:54 +02:00
}
getChangesSinceVersion(lastKnownVersion?: number): ProjectFilesWithTSDiagnostics {
this.updateGraph();
2016-06-22 02:31:54 +02:00
const info = {
projectName: this.getProjectName(),
version: this.projectStructureVersion,
isInferred: this.projectKind === ProjectKind.Inferred,
options: this.getCompilerOptions()
2016-06-22 02:31:54 +02:00
};
// check if requested version is the same that we have reported last time
2016-06-22 02:31:54 +02:00
if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) {
// if current structure version is the same - return info witout any changes
if (this.projectStructureVersion == this.lastReportedVersion) {
return { info, projectErrors: this.projectErrors };
2016-06-22 02:31:54 +02:00
}
// compute and return the difference
2016-06-22 02:31:54 +02:00
const lastReportedFileNames = this.lastReportedFileNames;
const currentFiles = arrayToMap(this.getFileNames(), x => x);
const added: string[] = [];
const removed: string[] = [];
for (const id in currentFiles) {
if (!hasProperty(lastReportedFileNames, id)) {
2016-06-22 02:31:54 +02:00
added.push(id);
}
}
for (const id in lastReportedFileNames) {
if (!hasProperty(currentFiles, id)) {
2016-06-22 02:31:54 +02:00
removed.push(id);
}
}
2016-06-22 02:31:54 +02:00
this.lastReportedFileNames = currentFiles;
this.lastReportedVersion = this.projectStructureVersion;
return { info, changes: { added, removed }, projectErrors: this.projectErrors };
2016-06-22 02:31:54 +02:00
}
else {
// unknown version - return everything
const projectFileNames = this.getFileNames();
this.lastReportedFileNames = arrayToMap(projectFileNames, x => x);
this.lastReportedVersion = this.projectStructureVersion;
return { info, files: projectFileNames, projectErrors: this.projectErrors };
2016-06-22 02:31:54 +02:00
}
}
getReferencedFiles(path: Path): Path[] {
if (!this.languageServiceEnabled) {
return [];
}
const sourceFile = this.getSourceFile(path);
if (!sourceFile) {
return [];
}
// We need to use a set here since the code can contain the same import twice,
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
const referencedFiles = createMap<boolean>();
if (sourceFile.imports && sourceFile.imports.length > 0) {
const checker: TypeChecker = this.program.getTypeChecker();
for (const importName of sourceFile.imports) {
const symbol = checker.getSymbolAtLocation(importName);
if (symbol && symbol.declarations && symbol.declarations[0]) {
const declarationSourceFile = symbol.declarations[0].getSourceFile();
if (declarationSourceFile) {
referencedFiles[declarationSourceFile.path] = true;
}
}
}
}
const currentDirectory = getDirectoryPath(path);
const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames);
// Handle triple slash references
if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) {
for (const referencedFile of sourceFile.referencedFiles) {
const referencedPath = toPath(referencedFile.fileName, currentDirectory, getCanonicalFileName);
referencedFiles[referencedPath] = true;
}
}
// Handle type reference directives
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
for (const typeName in sourceFile.resolvedTypeReferenceDirectiveNames) {
const resolvedTypeReferenceDirective = sourceFile.resolvedTypeReferenceDirectiveNames[typeName];
if (!resolvedTypeReferenceDirective) {
continue;
}
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
const typeFilePath = toPath(fileName, currentDirectory, getCanonicalFileName);
referencedFiles[typeFilePath] = true;
}
}
const allFileNames = map(Object.keys(referencedFiles), key => <Path>key);
return filter(allFileNames, file => this.projectService.host.fileExists(file));
}
// remove a root file from project
private removeRootFileIfNecessary(info: ScriptInfo): void {
if (this.isRoot(info)) {
remove(this.rootFiles, info);
this.rootFilesMap.remove(info.path);
}
}
2016-06-22 02:31:54 +02:00
}
export class InferredProject extends Project {
private static NextId = 1;
/**
* Unique name that identifies this particular inferred project
*/
private readonly inferredProjectName: string;
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions) {
super(ProjectKind.Inferred,
projectService,
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
compilerOptions,
/*compileOnSaveEnabled*/ false);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
InferredProject.NextId++;
}
getProjectName() {
return this.inferredProjectName;
}
getProjectRootPath() {
// Single inferred project does not have a project root.
if (this.projectService.useSingleInferredProject) {
return undefined;
}
const rootFiles = this.getRootFiles();
return getDirectoryPath(rootFiles[0]);
}
close() {
super.close();
for (const directory of this.directoriesWatchedForTsconfig) {
this.projectService.stopWatchingDirectory(directory);
}
}
getTypingOptions(): TypingOptions {
return {
enableAutoDiscovery: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
}
export class ConfiguredProject extends Project {
private typingOptions: TypingOptions;
2016-06-22 02:31:54 +02:00
private projectFileWatcher: FileWatcher;
private directoryWatcher: FileWatcher;
private directoriesWatchedForWildcards: Map<FileWatcher>;
2016-09-20 01:47:15 +02:00
private typeRootsWatchers: FileWatcher[];
2016-06-22 02:31:54 +02:00
/** Used for configured projects which may have multiple open roots */
openRefCount = 0;
constructor(readonly configFileName: NormalizedPath,
2016-06-22 02:31:54 +02:00
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
compilerOptions: CompilerOptions,
private wildcardDirectories: Map<WatchDirectoryFlags>,
languageServiceEnabled: boolean,
public compileOnSaveEnabled: boolean) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
2016-06-22 02:31:54 +02:00
}
getProjectRootPath() {
return getDirectoryPath(this.configFileName);
}
setProjectErrors(projectErrors: Diagnostic[]) {
this.projectErrors = projectErrors;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
this.typingOptions = newTypingOptions;
}
2016-08-12 20:04:43 +02:00
getTypingOptions() {
return this.typingOptions;
}
2016-06-22 02:31:54 +02:00
getProjectName() {
return this.configFileName;
}
watchConfigFile(callback: (project: ConfiguredProject) => void) {
this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this));
}
2016-09-20 01:47:15 +02:00
watchTypeRoots(callback: (project: ConfiguredProject, path: string) => void) {
2016-09-20 21:07:52 +02:00
const roots = this.getEffectiveTypeRoots();
2016-09-20 01:47:15 +02:00
const watchers: FileWatcher[] = [];
2016-09-20 21:07:52 +02:00
for (const root of roots) {
this.projectService.logger.info(`Add type root watcher for: ${root}`);
watchers.push(this.projectService.host.watchDirectory(root, path => callback(this, path), /*recursive*/ false));
2016-09-20 01:47:15 +02:00
}
this.typeRootsWatchers = watchers;
}
2016-06-22 02:31:54 +02:00
watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) {
if (this.directoryWatcher) {
return;
}
const directoryToWatch = getDirectoryPath(this.configFileName);
2016-07-12 04:44:23 +02:00
this.projectService.logger.info(`Add recursive watcher for: ${directoryToWatch}`);
2016-06-22 02:31:54 +02:00
this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true);
}
watchWildcards(callback: (project: ConfiguredProject, path: string) => void) {
if (!this.wildcardDirectories) {
return;
}
const configDirectoryPath = getDirectoryPath(this.configFileName);
this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => {
2016-06-22 02:31:54 +02:00
if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
2016-07-12 04:44:23 +02:00
this.projectService.logger.info(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`);
watchers[directory] = this.projectService.host.watchDirectory(
2016-06-22 02:31:54 +02:00
directory,
path => callback(this, path),
recursive
);
2016-06-22 02:31:54 +02:00
}
return watchers;
}, <Map<FileWatcher>>{});
2016-06-22 02:31:54 +02:00
}
stopWatchingDirectory() {
if (this.directoryWatcher) {
this.directoryWatcher.close();
this.directoryWatcher = undefined;
}
}
close() {
super.close();
if (this.projectFileWatcher) {
this.projectFileWatcher.close();
}
2016-09-20 01:47:15 +02:00
if (this.typeRootsWatchers) {
for (const watcher of this.typeRootsWatchers) {
watcher.close();
}
this.typeRootsWatchers = undefined;
}
for (const id in this.directoriesWatchedForWildcards) {
this.directoriesWatchedForWildcards[id].close();
}
2016-06-22 02:31:54 +02:00
this.directoriesWatchedForWildcards = undefined;
this.stopWatchingDirectory();
}
addOpenRef() {
this.openRefCount++;
}
deleteOpenRef() {
this.openRefCount--;
return this.openRefCount;
}
2016-09-20 01:47:15 +02:00
getEffectiveTypeRoots() {
2016-09-20 03:00:42 +02:00
return ts.getEffectiveTypeRoots(this.getCompilerOptions(), this.projectService.host) || [];
2016-09-20 01:47:15 +02:00
}
2016-06-22 02:31:54 +02:00
}
export class ExternalProject extends Project {
private typingOptions: TypingOptions;
2016-06-22 02:31:54 +02:00
constructor(readonly externalProjectName: string,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
languageServiceEnabled: boolean,
public compileOnSaveEnabled: boolean,
private readonly projectFilePath?: string) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
getProjectRootPath() {
if (this.projectFilePath) {
return getDirectoryPath(this.projectFilePath);
}
// if the projectFilePath is not given, we make the assumption that the project name
// is the path of the project file. AS the project name is provided by VS, we need to
// normalize slashes before using it as a file name.
return getDirectoryPath(normalizeSlashes(this.externalProjectName));
}
getTypingOptions() {
return this.typingOptions;
}
setProjectErrors(projectErrors: Diagnostic[]) {
this.projectErrors = projectErrors;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
if (!newTypingOptions) {
// set default typings options
newTypingOptions = {
enableAutoDiscovery: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
else {
if (newTypingOptions.enableAutoDiscovery === undefined) {
// if autoDiscovery was not specified by the caller - set it based on the content of the project
newTypingOptions.enableAutoDiscovery = allRootFilesAreJsOrDts(this);
}
if (!newTypingOptions.include) {
newTypingOptions.include = [];
}
if (!newTypingOptions.exclude) {
newTypingOptions.exclude = [];
}
}
this.typingOptions = newTypingOptions;
2016-06-22 02:31:54 +02:00
}
getProjectName() {
return this.externalProjectName;
}
}
}