Skip to content

Enable TS Server plugins on web #47377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 14, 2022
Merged

Conversation

mjbvz
Copy link
Contributor

@mjbvz mjbvz commented Jan 11, 2022

Fixes #47376
Fixes microsoft/vscode#140455

This PR adds support for loading TS Service plugins on web. There plugins are loaded slightly differently than on desktop:

  • We only load plugins that have a browser field in their package.json. This ensures we don't try activating non-web enabled plugins on web

  • Plugins should ship as es modules instead of as a common js module. The top level module should export a default function that activates the plugins.

  • Instead of using require to load the plugin, we use import(...)

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Jan 11, 2022
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)
@mjbvz mjbvz force-pushed the ts-plugins-on-web branch from 9695f53 to e2bbb6c Compare February 16, 2022 03:25
This more or less matches how node plugins expect the plugin module to be an init function
@mjbvz
Copy link
Contributor Author

mjbvz commented Feb 24, 2022

To test this locally:

  1. Checkout this branch in the TS repo and build it by running nix gulp local

  2. Checkout VS Code and run yarn

  3. In VS Code, comment out the whole the CopyPlugin block here (not strictly required if you don't plan on changing VS Code sources but useful as it prevents VS Code from overwriting the custom TS build we will link in with the next step):

    https://github.com/microsoft/vscode/blob/cde5781978134c0091d28a4f11b45b2f08412b4f/extensions/typescript-language-features/extension-browser.webpack.config.js#L62

  4. Symlink your local build of TS into VS Code:

ln -s /path/to/typescript/built/local/tsserver.js /path/to/vscode/extensions/typescript-language-features/dist/browser/typescript/tsserver.web.js
  1. In VS Code, run yarn watch to build the base editor
  2. In another terminal run yarn watch-web to build the built-in extensions for web
  3. After the build complete for the first time, In a third terminal, run ./scripts/code-web.sh to launch VS Code for web locally (it launches on http://localhost:8080 by default)

If you need to test a VS Code extension that contributes a plugin, follow these steps to get your local extension installed into the local web instance of VS Code: https://github.com/microsoft/vscode-docs/blob/vnext/api/extension-guides/web-extensions.md#test-your-web-extension-in-on-vscodedev

mjbvz and others added 2 commits March 7, 2022 18:42
- Use result value instead of try/catch (`ImportPluginResult`)
- Add awaits
- Add logging
@mjbvz mjbvz marked this pull request as ready for review May 6, 2022 22:07
@typescript-bot
Copy link
Collaborator

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@mjbvz
Copy link
Contributor Author

mjbvz commented May 6, 2022

Thanks for the hep here @rbuckton!

I just tested with the latest changes and think things look good for us on the VS Code side. Any other big concerns for this on the TS side?

@jakebailey
Copy link
Member

IIRC @RyanCavanaugh mentioned something about not leaving any evals in the TS codebase to avoid the wrath of the security people; should the eval get replaced with a throw if our intent is to just have importScripts?

We should throw instead when dynamic import is not implemented
@mjbvz mjbvz changed the title Prototyping TS Server plugins on web Enable TS Server plugins on web May 10, 2022
@mjbvz
Copy link
Contributor Author

mjbvz commented May 10, 2022

Ok I've updated the PR to throw instead of falling back to eval. Also confirmed that if we load tsserverWeb before tsserver, we are able to shim dynamicImport

mjbvz added a commit to mjbvz/vscode that referenced this pull request May 10, 2022
microsoft/TypeScript#47377

To run plugins on web, we need to shim out `dynamicImport`. This is done in a file call `tsserverWeb.js`, which is added by the linked PR
@frank-dspeed
Copy link

awsome work

Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going through project.ts, editorServices.ts, and related files to determine what issues we'll run into by introducing asynchrony. All of the code paths currently expect things to execute synchronously, so making any of them async means there's a possibility of out of order updates or reading stale data.

@frank-dspeed
Copy link

@rbuckton good catch but going async with plugins is the only way to go and then awaiting it all plugins are async by design as they can be ESM and that it can be ESM also says that it needs to be async by design no matter what else as ESM is async by design there is no way around that.

@rbuckton
Copy link
Member

rbuckton commented May 25, 2022

@mjbvz, I've pushed up an update that ensures dynamically imported plugins are processed in the correct order. It essentially splits enablePlugin into two phases for asynchronously imported plugins. The first phase performs all of the dynamic imports in the order the plugins were requested. The second phase invokes the default export of each plugin module in the original order they were requested.

Asynchronous plugins will be loaded after the first request that creates their related Project. Once all asynchronously imported plugins have been loaded, the project services will send a ProjectsUpdatedInBackgroundEvent event to the client. This means that there is a small window where commands executed against a project will be executed without any effects from plugins.

@rbuckton rbuckton force-pushed the ts-plugins-on-web branch from f1e8d5c to 0c78b83 Compare May 25, 2022 01:55
@frank-dspeed
Copy link

@rbuckton can we mittiagte the time where the plugins are not loaded via catching all commands that would run against a none existing plugin and then execute them in order as soon as the event that all got loaded is fired

we dedupe the commands before that?

@rbuckton rbuckton force-pushed the ts-plugins-on-web branch from 0c78b83 to 186cec9 Compare May 25, 2022 18:58
@rbuckton
Copy link
Member

@rbuckton can we mittiagte the time where the plugins are not loaded via catching all commands that would run against a none existing plugin and then execute them in order as soon as the event that all got loaded is fired

we dedupe the commands before that?

I'm not entirely sure whether that would be necessary. New command invocations would run against the project with its plugins after they're loaded. Command invocations that affect resting state (such as diagnostics, semantic highlighting) would likely re-run when they receive the ProjectsUpdatedInBackgroundEvent. It would also mean existing clients would need to be resilient to receiving unexpected updates for already-completed command invocations.

@rbuckton
Copy link
Member

@sheetalkamat could you take a look at this as well?

@rbuckton rbuckton requested a review from sheetalkamat May 25, 2022 23:08
Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving for the changes that @mjbvz made, but I'd like to have @sheetalkamat or someone else with familiarity with tsserver to review the changes I've made before this is merged.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, although my knowledge is not that deep. @sheetalkamat should take a look too.

Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
@@ -16,5 +18,6 @@ declare namespace ts.server {
gc?(): void;
trace?(s: string): void;
require?(initialPath: string, moduleName: string): RequireResult;
importServicePlugin?(root: string, moduleName: string): Promise<ImportPluginResult>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need different name here? Especially if in future we have compile time plugins that get used on compile on save etc ?
May be just import like require ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjbvz and I discussed that earlier. This isn't as generic as import because there is specific logic for importing a browser plugin inside that function.

@@ -1,3 +1,4 @@
/* eslint-disable boolean-trivia */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed and cant be fixed? If it cant be fixed can you pls not disable this for the whole file. instead wherever its needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule is flagged on every expect(...).eq(true) (or false) in the file. It felt like enough individual exceptions that it was worth disabling for the whole file, and labeling every call with .eq(/*value*/ true) seemed like overkill.

const importPromise = project.beginEnablePluginAsync(pluginConfigEntry, searchPaths, pluginConfigOverrides);
this.pendingPluginEnablements ??= new Map();
let promises = this.pendingPluginEnablements.get(project);
if (!promises) this.pendingPluginEnablements.set(project, promises = []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle this map when project closes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it should matter. This map is built up during the course of a single request, and then is set to undefined as soon as the request completes processing (in enableRequestedPluginsAsync) so it won't hold a reference to the project for very long. The Promise for the plugin import actually has a longer lifetime than the key in the map, and removing the key early would just mean that we may fail to observe a rejected promise later.

@rbuckton
Copy link
Member

rbuckton commented Jun 4, 2022

I still have a few more PR feedback items to go through and some additional tests that @sheetalkamat mentioned. I'll put the rest of that together next week.

@rbuckton
Copy link
Member

@sheetalkamat can you take another look?

@rbuckton rbuckton force-pushed the ts-plugins-on-web branch from 8a3fcdb to 9bc39d1 Compare June 11, 2022 02:53
@rbuckton rbuckton merged commit 3fc5f96 into microsoft:main Jun 14, 2022
mjbvz added a commit to mjbvz/vscode that referenced this pull request Aug 2, 2022
microsoft/TypeScript#47377

To run plugins on web, we need to shim out `dynamicImport`. This is done in a file call `tsserverWeb.js`, which is added by the linked PR
mjbvz added a commit to microsoft/vscode that referenced this pull request Aug 2, 2022
* Add patch for enabling new TS plugins on web approach

microsoft/TypeScript#47377

To run plugins on web, we need to shim out `dynamicImport`. This is done in a file call `tsserverWeb.js`, which is added by the linked PR

* Update for new files names
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enable TS Server plugins on web Explore enabling TS Server plugins on serverless
8 participants