mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-18 07:29:16 -05:00
Language service extensibility
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -2267,6 +2267,7 @@ namespace ts.server.protocol {
|
||||
outDir?: string;
|
||||
outFile?: string;
|
||||
paths?: MapLike<string[]>;
|
||||
plugins?: PluginImport[];
|
||||
preserveConstEnums?: boolean;
|
||||
project?: string;
|
||||
reactNamespace?: string;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user