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)
This commit is contained in:
Matt Bierner
2022-02-15 19:25:10 -08:00
parent 852b1c2b73
commit e2bbb6c329
3 changed files with 40 additions and 2 deletions
+21 -2
View File
@@ -1577,7 +1577,7 @@ namespace ts.server {
}
}
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | undefined) {
protected async enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | 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) {
+1
View File
@@ -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<any>;
}
}
+18
View File
@@ -1,4 +1,6 @@
/*@internal*/
/// <reference lib="dom" />
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