Language service extensibility

This commit is contained in:
Ryan Cavanaugh
2017-02-14 13:35:16 -08:00
parent 81f4e38643
commit aec310996c
14 changed files with 325 additions and 12 deletions

View File

@@ -91,19 +91,38 @@ namespace ts.server {
}
}
export interface PluginCreateInfo {
project: Project;
languageService: LanguageService;
languageServiceHost: LanguageServiceHost;
serverHost: ServerHost;
config: any;
}
export interface PluginModule {
create(createInfo: PluginCreateInfo): LanguageService;
getExternalFiles?(proj: Project): string[];
}
export interface PluginModuleFactory {
(mod: { typescript: typeof ts }): PluginModule;
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: LSHost;
private program: ts.Program;
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
private readonly languageService: LanguageService;
// wrapper over the real language service that will suppress all semantic operations
protected languageService: LanguageService;
public languageServiceEnabled = true;
protected readonly lsHost: LSHost;
builder: Builder;
/**
* Set of files names that were updated since the last call to getChangesSinceVersion.
@@ -150,6 +169,17 @@ namespace ts.server {
return this.cachedUnresolvedImportsPerFile;
}
public static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void): {} {
const resolvedPath = normalizeSlashes(host.resolvePath(combinePaths(initialDir, "node_modules")));
log(`Loading ${moduleName} from ${initialDir} (resolved to ${resolvedPath})`);
const result = host.require(resolvedPath, moduleName);
if (result.error) {
log(`Failed to load module: ${JSON.stringify(result.error)}`);
return undefined;
}
return result.module;
}
constructor(
private readonly projectName: string,
readonly projectKind: ProjectKind,
@@ -237,6 +267,10 @@ namespace ts.server {
abstract getProjectRootPath(): string | undefined;
abstract getTypeAcquisition(): TypeAcquisition;
getExternalFiles(): string[] {
return [];
}
getSourceFile(path: Path) {
if (!this.program) {
return undefined;
@@ -804,10 +838,12 @@ namespace ts.server {
private typeRootsWatchers: FileWatcher[];
readonly canonicalConfigFilePath: NormalizedPath;
private plugins: PluginModule[] = [];
/** Used for configured projects which may have multiple open roots */
openRefCount = 0;
constructor(configFileName: NormalizedPath,
constructor(private configFileName: NormalizedPath,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
@@ -817,12 +853,64 @@ namespace ts.server {
public compileOnSaveEnabled: boolean) {
super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName));
this.enablePlugins();
}
getConfigFilePath() {
return this.getProjectName();
}
enablePlugins() {
const host = this.projectService.host;
const options = this.getCompilerOptions();
const log = (message: string) => {
this.projectService.logger.info(message);
};
if (!(options.plugins && options.plugins.length)) {
this.projectService.logger.info("No plugins exist");
// No plugins
return;
}
if (!host.require) {
this.projectService.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded");
return;
}
for (const pluginConfigEntry of options.plugins) {
const searchPath = getDirectoryPath(this.configFileName);
const resolvedModule = <PluginModuleFactory>Project.resolveModule(pluginConfigEntry.name, searchPath, host, log);
if (resolvedModule) {
this.enableProxy(resolvedModule, pluginConfigEntry);
}
}
}
private enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: PluginImport) {
try {
if (typeof pluginModuleFactory !== "function") {
this.projectService.logger.info(`Skipped loading plugin ${configEntry.name} because it did expose a proper factory function`);
return;
}
const info: PluginCreateInfo = {
config: configEntry,
project: this,
languageService: this.languageService,
languageServiceHost: this.lsHost,
serverHost: this.projectService.host
};
const pluginModule = pluginModuleFactory({ typescript: ts });
this.languageService = pluginModule.create(info);
this.plugins.push(pluginModule);
}
catch (e) {
this.projectService.logger.info(`Plugin activation failed: ${e}`);
}
}
getProjectRootPath() {
return getDirectoryPath(this.getConfigFilePath());
}
@@ -839,6 +927,21 @@ namespace ts.server {
return this.typeAcquisition;
}
getExternalFiles(): string[] {
const items: string[] = [];
for (const plugin of this.plugins) {
if (typeof plugin.getExternalFiles === "function") {
try {
items.push(...plugin.getExternalFiles(this));
}
catch (e) {
this.projectService.logger.info(`A plugin threw an exception in getExternalFiles: ${e}`);
}
}
}
return items;
}
watchConfigFile(callback: (project: ConfiguredProject) => void) {
this.projectFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), _ => callback(this));
}

View File

@@ -2267,6 +2267,7 @@ namespace ts.server.protocol {
outDir?: string;
outFile?: string;
paths?: MapLike<string[]>;
plugins?: PluginImport[];
preserveConstEnums?: boolean;
project?: string;
reactNamespace?: string;

View File

@@ -99,6 +99,8 @@ namespace ts.server {
birthtime: Date;
}
type RequireResult = { module: {}, error: undefined } | { module: undefined, error: {} };
const readline: {
createInterface(options: ReadLineOptions): NodeJS.EventEmitter;
} = require("readline");
@@ -593,6 +595,16 @@ namespace ts.server {
sys.gc = () => global.gc();
}
sys.require = (initialDir: string, moduleName: string): RequireResult => {
const result = nodeModuleNameResolver(moduleName, initialDir + "/program.ts", { moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true }, sys, undefined, /*jsOnly*/ true);
try {
return { module: require(result.resolvedModule.resolvedFileName), error: undefined };
}
catch (e) {
return { module: undefined, error: e };
}
};
let cancellationToken: ServerCancellationToken;
try {
const factory = require("./cancellationToken");

View File

@@ -9,6 +9,7 @@ declare namespace ts.server {
data: any;
}
type RequireResult = { module: {}, error: undefined } | { module: undefined, error: {} };
export interface ServerHost extends System {
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any;
clearTimeout(timeoutId: any): void;
@@ -16,6 +17,7 @@ declare namespace ts.server {
clearImmediate(timeoutId: any): void;
gc?(): void;
trace?(s: string): void;
require?(initialPath: string, moduleName: string): RequireResult;
}
export interface SortedReadonlyArray<T> extends ReadonlyArray<T> {