forked from sourcegraph/javascript-typescript-langserver
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugins.ts
More file actions
198 lines (177 loc) · 7.29 KB
/
plugins.ts
File metadata and controls
198 lines (177 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import * as fs from 'mz/fs'
import * as path from 'path'
import * as ts from 'typescript'
import { Logger, NoopLogger } from './logging'
import { combinePaths } from './match-files'
import { PluginSettings } from './request-type'
import { toUnixPath } from './util'
// Based on types and logic from TypeScript server/project.ts @
// https://github.com/Microsoft/TypeScript/blob/711e890e59e10aa05a43cb938474a3d9c2270429/src/server/project.ts
/**
* A plugin exports an initialization function, injected with
* the current typescript instance
*/
export type PluginModuleFactory = (mod: { typescript: typeof ts }) => PluginModule
export type EnableProxyFunc = (pluginModuleFactory: PluginModuleFactory, pluginConfigEntry: ts.PluginImport) => void
/**
* A plugin presents this API when initialized
*/
export interface PluginModule {
create(createInfo: PluginCreateInfo): ts.LanguageService
getExternalFiles?(proj: Project): string[]
}
/**
* All of tsserver's environment exposed to plugins
*/
export interface PluginCreateInfo {
project: Project
languageService: ts.LanguageService
languageServiceHost: ts.LanguageServiceHost
serverHost: ServerHost
config: any
}
/**
* The portion of tsserver's Project API exposed to plugins
*/
export interface Project {
projectService: { logger: Logger }
getCurrentDirectory(): string
}
/**
* A local filesystem-based ModuleResolutionHost for plugin loading.
*/
export class LocalModuleResolutionHost implements ts.ModuleResolutionHost {
public fileExists(fileName: string): boolean {
return fs.existsSync(fileName)
}
public readFile(fileName: string): string {
return fs.readFileSync(fileName, 'utf8')
}
}
/**
* The portion of tsserver's ServerHost API exposed to plugins
*/
export type ServerHost = object
/**
* The result of a node require: a module or an error.
*/
type RequireResult = { module: {}; error: undefined } | { module: undefined; error: {} }
export class PluginLoader {
private allowLocalPluginLoads = false
private globalPlugins: string[] = []
private pluginProbeLocations: string[] = []
constructor(
private rootFilePath: string,
private fs: ts.ModuleResolutionHost,
pluginSettings?: PluginSettings,
private logger = new NoopLogger(),
private resolutionHost = new LocalModuleResolutionHost(),
private requireModule: (moduleName: string) => any = require
) {
if (pluginSettings) {
this.allowLocalPluginLoads = pluginSettings.allowLocalPluginLoads || false
this.globalPlugins = pluginSettings.globalPlugins || []
this.pluginProbeLocations = pluginSettings.pluginProbeLocations || []
}
}
public loadPlugins(options: ts.CompilerOptions, applyProxy: EnableProxyFunc): void {
// Search our peer node_modules, then any globally-specified probe paths
// ../../.. to walk from X/node_modules/javascript-typescript-langserver/lib/project-manager.js to X/node_modules/
const searchPaths = [combinePaths(__filename, '../../..'), ...this.pluginProbeLocations]
// Corresponds to --allowLocalPluginLoads, opt-in to avoid remote code execution.
if (this.allowLocalPluginLoads) {
const local = this.rootFilePath
this.logger.info(`Local plugin loading enabled; adding ${local} to search paths`)
searchPaths.unshift(local)
}
let pluginImports: ts.PluginImport[] = []
if (options.plugins) {
pluginImports = options.plugins as ts.PluginImport[]
}
// Enable tsconfig-specified plugins
if (options.plugins) {
for (const pluginConfigEntry of pluginImports) {
this.enablePlugin(pluginConfigEntry, searchPaths, applyProxy)
}
}
if (this.globalPlugins) {
// Enable global plugins with synthetic configuration entries
for (const globalPluginName of this.globalPlugins) {
// Skip already-locally-loaded plugins
if (!pluginImports || pluginImports.some(p => p.name === globalPluginName)) {
continue
}
// Provide global: true so plugins can detect why they can't find their config
this.enablePlugin({ name: globalPluginName, global: true } as ts.PluginImport, searchPaths, applyProxy)
}
}
}
/**
* Tries to load and enable a single plugin
* @param pluginConfigEntry
* @param searchPaths
*/
private enablePlugin(
pluginConfigEntry: ts.PluginImport,
searchPaths: string[],
enableProxy: EnableProxyFunc
): void {
for (const searchPath of searchPaths) {
const resolvedModule = this.resolveModule(pluginConfigEntry.name, searchPath) as PluginModuleFactory
if (resolvedModule) {
enableProxy(resolvedModule, pluginConfigEntry)
return
}
}
this.logger.error(`Couldn't find ${pluginConfigEntry.name} anywhere in paths: ${searchPaths.join(',')}`)
}
/**
* Load a plugin using a node require
* @param moduleName
* @param initialDir
*/
private resolveModule(moduleName: string, initialDir: string): {} | undefined {
const resolvedPath = toUnixPath(path.resolve(combinePaths(initialDir, 'node_modules')))
this.logger.info(`Loading ${moduleName} from ${initialDir} (resolved to ${resolvedPath})`)
const result = this.requirePlugin(resolvedPath, moduleName)
if (result.error) {
this.logger.error(`Failed to load module: ${JSON.stringify(result.error)}`)
return undefined
}
return result.module
}
/**
* Resolves a loads a plugin function relative to initialDir
* @param initialDir
* @param moduleName
*/
private requirePlugin(initialDir: string, moduleName: string): RequireResult {
try {
const modulePath = this.resolveJavaScriptModule(moduleName, initialDir, this.fs)
return { module: this.requireModule(modulePath), error: undefined }
} catch (error) {
return { module: undefined, error }
}
}
/**
* Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations.
* No way to do this with `require()`: https://github.com/nodejs/node/issues/5963
* Throws an error if the module can't be resolved.
* stolen from moduleNameResolver.ts because marked as internal
*/
private resolveJavaScriptModule(moduleName: string, initialDir: string, host: ts.ModuleResolutionHost): string {
// TODO: this should set jsOnly=true to the internal resolver, but this parameter is not exposed on a public api.
const result = ts.nodeModuleNameResolver(
moduleName,
initialDir.replace('\\', '/') + '/package.json' /* containingFile */,
{ moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true },
this.resolutionHost,
undefined
)
if (!result.resolvedModule) {
// this.logger.error(result.failedLookupLocations);
throw new Error(`Could not resolve JS module ${moduleName} starting at ${initialDir}.`)
}
return result.resolvedModule.resolvedFileName
}
}