diff --git a/src/server/project.ts b/src/server/project.ts index 7a963ddf03f..b8cf6476b7a 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1545,7 +1545,7 @@ namespace ts.server { return !!this.program && this.program.isSourceOfProjectReferenceRedirect(fileName); } - protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { + protected async enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined): Promise { const host = this.projectService.host; if (!host.require) { @@ -1555,9 +1555,9 @@ namespace ts.server { // Search any globally-specified probe paths, then our peer node_modules const searchPaths = [ - ...this.projectService.pluginProbeLocations, - // ../../.. to walk from X/node_modules/typescript/lib/tsserver.js to X/node_modules/ - combinePaths(this.projectService.getExecutingFilePath(), "../../.."), + ...this.projectService.pluginProbeLocations, + // ../../.. to walk from X/node_modules/typescript/lib/tsserver.js to X/node_modules/ + combinePaths(this.projectService.getExecutingFilePath(), "../../.."), ]; if (this.projectService.globalPlugins) { @@ -1572,12 +1572,12 @@ namespace ts.server { // Provide global: true so plugins can detect why they can't find their config this.projectService.logger.info(`Loading global plugin ${globalPluginName}`); - this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths, pluginConfigOverrides); + await this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths, pluginConfigOverrides); } } } - protected async enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined) { + protected async enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined): Promise { this.projectService.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`); if (!pluginConfigEntry.name || parsePackageName(pluginConfigEntry.name).rest) { this.projectService.logger.info(`Skipped loading plugin ${pluginConfigEntry.name || JSON.stringify(pluginConfigEntry)} because only package name is allowed plugin name`); @@ -1593,21 +1593,20 @@ namespace ts.server { let resolvedModule: any | undefined; if (this.projectService.host.importServicePlugin) { for (const searchPath of searchPaths) { - try { - resolvedModule = await this.projectService.host.importServicePlugin(searchPath, pluginConfigEntry.name); + const result = await this.projectService.host.importServicePlugin(searchPath, pluginConfigEntry.name); + if (result.error) { + logError(result.error.toString()); } - catch (e) { - // TODO: log this? - continue; - } - if (resolvedModule) { + else { + resolvedModule = result.module; break; } + } } else { resolvedModule = firstDefined(searchPaths, searchPath => - Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log, logError) as PluginModuleFactory | undefined); + Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log, logError) as PluginModuleFactory | undefined); } if (resolvedModule) { @@ -2290,7 +2289,7 @@ namespace ts.server { } /*@internal*/ - enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: ESMap | undefined) { + async enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: ESMap | undefined): Promise { const host = this.projectService.host; if (!host.require) { @@ -2311,11 +2310,11 @@ namespace ts.server { // Enable tsconfig-specified plugins if (options.plugins) { for (const pluginConfigEntry of options.plugins) { - this.enablePlugin(pluginConfigEntry, searchPaths, pluginConfigOverrides); + await this.enablePlugin(pluginConfigEntry, searchPaths, pluginConfigOverrides); } } - this.enableGlobalPlugins(options, pluginConfigOverrides); + return this.enableGlobalPlugins(options, pluginConfigOverrides); } /** diff --git a/src/server/types.ts b/src/server/types.ts index 87ba7f2329d..7e8a2de9d35 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -6,6 +6,8 @@ declare namespace ts.server { } export type RequireResult = { module: {}, error: undefined } | { module: undefined, error: { stack?: string, message?: string } }; + export type ImportPluginResult = { module: {}, error: undefined } | { module: undefined, error: { stack?: string, message: string } }; + export interface ServerHost extends System { watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher; watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher; @@ -16,6 +18,6 @@ declare namespace ts.server { gc?(): void; trace?(s: string): void; require?(initialPath: string, moduleName: string): RequireResult; - importServicePlugin?(root: string, moduleName: string): Promise; + importServicePlugin?(root: string, moduleName: string): Promise; } } diff --git a/src/webServer/webServer.ts b/src/webServer/webServer.ts index a7478df66b5..cf24134da4b 100644 --- a/src/webServer/webServer.ts +++ b/src/webServer/webServer.ts @@ -139,21 +139,34 @@ namespace ts.server { /* eslint-enable no-restricted-globals */ require: () => ({ module: undefined, error: new Error("Not implemented") }), - importServicePlugin: async (root: string, moduleName: string) => { + importServicePlugin: async (root: string, moduleName: string): Promise => { const packageRoot = combinePaths(root, "node_modules", moduleName); - const packageJsonResponse = await fetch(combinePaths(packageRoot, "package.json")); - const packageJson = await packageJsonResponse.json(); + let packageJson: any | undefined; + try { + const packageJsonResponse = await fetch(combinePaths(packageRoot, "package.json")); + packageJson = await packageJsonResponse.json(); + } + catch (e) { + return { module: undefined, error: new Error("Could not load plugin. Could not load 'package.json'.") }; + } + const browser = packageJson.browser; if (!browser) { - throw new Error("Could not load plugin. No 'browser' field found in package.json."); + return { module: undefined, error: new Error("Could not load plugin. No 'browser' field found in package.json.") }; } const scriptPath = combinePaths(packageRoot, browser); // TODO: TS rewrites `import(...)` to `require`. Use eval to bypass this // eslint-disable-next-line no-eval - return (await eval(`import(${JSON.stringify(scriptPath)})`)).default; + try { + const module = (await eval(`import(${JSON.stringify(scriptPath)})`)).default; + return { module, error: undefined }; + } + catch (e) { + return { module: undefined, error: e }; + } }, exit: notImplemented,