Enable TS Server plugins on web (#47377)

* 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)

* use default export from plugins

This more or less matches how node plugins expect the plugin module to be an init function

* Allow configure plugin requests against any web servers in partial semantic mode

* Addressing some comments

- Use result value instead of try/catch (`ImportPluginResult`)
- Add awaits
- Add logging

* add tsserverWeb to patch in dynamic import

* Remove eval

We should throw instead when dynamic import is not implemented

* Ensure dynamically imported plugins are loaded in the correct order

* Add tests for async service plugin timing

* Update src/server/editorServices.ts

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>

* Partial PR feedback

* Rename tsserverWeb to dynamicImportCompat

* Additional PR feedback

Co-authored-by: Ron Buckton <ron.buckton@microsoft.com>
Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
This commit is contained in:
Matt Bierner
2022-06-14 12:35:53 -07:00
committed by GitHub
parent 29dffc3079
commit 3fc5f968ca
14 changed files with 519 additions and 30 deletions

View File

@@ -1,4 +1,7 @@
/*@internal*/
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
namespace ts.server {
export interface HostWithWriteMessage {
writeMessage(s: any): void;
@@ -109,11 +112,34 @@ namespace ts.server {
}
}
export declare const dynamicImport: ((id: string) => Promise<any>) | undefined;
// Attempt to load `dynamicImport`
if (typeof importScripts === "function") {
try {
// NOTE: importScripts is synchronous
importScripts("dynamicImportCompat.js");
}
catch {
// ignored
}
}
export function createWebSystem(host: WebHost, args: string[], getExecutingFilePath: () => string): ServerHost {
const returnEmptyString = () => "";
const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(getExecutingFilePath()))));
// Later we could map ^memfs:/ to do something special if we want to enable more functionality like module resolution or something like that
const getWebPath = (path: string) => startsWith(path, directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined;
const dynamicImport = async (id: string): Promise<any> => {
// Use syntactic dynamic import first, if available
if (server.dynamicImport) {
return server.dynamicImport(id);
}
throw new Error("Dynamic import not implemented");
};
return {
args,
newLine: "\r\n", // This can be configured by clients
@@ -136,7 +162,32 @@ namespace ts.server {
clearImmediate: handle => clearTimeout(handle),
/* eslint-enable no-restricted-globals */
require: () => ({ module: undefined, error: new Error("Not implemented") }),
importServicePlugin: async (initialDir: string, moduleName: string): Promise<ModuleImportResult> => {
const packageRoot = combinePaths(initialDir, moduleName);
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) {
return { module: undefined, error: new Error("Could not load plugin. No 'browser' field found in package.json.") };
}
const scriptPath = combinePaths(packageRoot, browser);
try {
const { default: module } = await dynamicImport(scriptPath);
return { module, error: undefined };
}
catch (e) {
return { module: undefined, error: e };
}
},
exit: notImplemented,
// Debugging related