From e2bbb6c3297b3968207e9b4af0902ad8e8f516ac Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 15 Feb 2022 19:17:32 -0800 Subject: [PATCH] Prototype TS plugins on web This prototype allows service plugins to be loaded on web TSServer Main changes: - Adds a new host entryPoint called `importServicePlugin` for overriding how plugins can be loaded. This may be async - Implement `importServicePlugin` for webServer - The web server plugin implementation looks for a `browser` field in the plugin's `package.json` - It then uses `import(...)` to load the plugin (the plugin source must be compiled to support being loaded as a module) --- src/server/project.ts | 23 +++++++++++++++++++++-- src/server/types.ts | 1 + src/webServer/webServer.ts | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index e6170d086ce..7a963ddf03f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1577,7 +1577,7 @@ namespace ts.server { } } - protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined) { + protected async enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined) { 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`); @@ -1589,8 +1589,27 @@ namespace ts.server { const logError = (message: string) => { (errorLogs || (errorLogs = [])).push(message); }; - const resolvedModule = firstDefined(searchPaths, searchPath => + + let resolvedModule: any | undefined; + if (this.projectService.host.importServicePlugin) { + for (const searchPath of searchPaths) { + try { + resolvedModule = await this.projectService.host.importServicePlugin(searchPath, pluginConfigEntry.name); + } + catch (e) { + // TODO: log this? + continue; + } + if (resolvedModule) { + break; + } + } + } + else { + resolvedModule = firstDefined(searchPaths, searchPath => Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log, logError) as PluginModuleFactory | undefined); + } + if (resolvedModule) { const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name); if (configurationOverride) { diff --git a/src/server/types.ts b/src/server/types.ts index 671e854440d..87ba7f2329d 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -16,5 +16,6 @@ declare namespace ts.server { gc?(): void; trace?(s: string): void; require?(initialPath: string, moduleName: string): RequireResult; + importServicePlugin?(root: string, moduleName: string): Promise; } } diff --git a/src/webServer/webServer.ts b/src/webServer/webServer.ts index 31f0a47915b..aa363e640f7 100644 --- a/src/webServer/webServer.ts +++ b/src/webServer/webServer.ts @@ -1,4 +1,6 @@ /*@internal*/ +/// + namespace ts.server { export interface HostWithWriteMessage { writeMessage(s: any): void; @@ -137,6 +139,22 @@ namespace ts.server { /* eslint-enable no-restricted-globals */ require: () => ({ module: undefined, error: new Error("Not implemented") }), + importServicePlugin: async (root: string, moduleName: string) => { + const packageRoot = combinePaths(root, "node_modules", moduleName); + + const packageJsonResponse = await fetch(combinePaths(packageRoot, "package.json")); + const packageJson = await packageJsonResponse.json(); + const browser = packageJson.browser; + if (!browser) { + throw 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 eval(`import(${JSON.stringify(scriptPath)})`); + }, exit: notImplemented, // Debugging related