Skip to content

feat(browser-only): add static extension support - #17966

Open
robertjndw wants to merge 9 commits into
eclipse-theia:masterfrom
robertjndw:feat/browser-only-extension-support-v2
Open

feat(browser-only): add static extension support#17966
robertjndw wants to merge 9 commits into
eclipse-theia:masterfrom
robertjndw:feat/browser-only-extension-support-v2

Conversation

@robertjndw

Copy link
Copy Markdown
Contributor

What it does

Reopens #14776 on a rebased branch. That PR has since been split in two: the build-time half, preparing the plugins via @theia/plugin-utils, landed separately in #17758, so what is left here is the runtime side, i.e. actually running those prepared plugins in a browser-only application.

@theia/plugin-ext and @theia/terminal now contribute frontendOnly modules:

  • FrontendHostedPluginServer serves the plugins the build prepared, from lib/frontend/hostedPlugin/list.json. Adopters who keep their plugin metadata somewhere the build can't see can bind PluginLocalOptions and supply it themselves.
  • FrontendPluginServer keeps the plugin key-value storage behind globalState/workspaceState in the browser storage. Install, uninstall, enable and disable reject, since there is no backend to deploy anything at runtime. Writes are serialized through the Web Locks API so two tabs don't clobber each other, with a per-realm mutex as fallback where that API isn't available.
  • FrontendPluginPathService resolves the plugin log and storage directories under the config directory, and prunes old session log folders the way the backend does.
  • HostedPluginFileSystemProvider serves the hostedPlugin: scheme read-only over HTTP, for the assets that are read through the FileService: color themes, icon themes and icon fonts.
  • BrowserOnlyTerminalFrontendContribution stubs out the terminal. It rebinds TerminalFrontendContribution rather than only TerminalService, because the terminal module binds all of its contribution points via toService on that class, so rebinding just the service would miss most callers.

A few plugin host fixes were needed along the way that aren't browser-only specific:

  • A single plugin failing to prepare (a 404 on its package.json, say) rejected the whole Promise.all in the web worker host and took every other plugin down with it. Failures are now isolated and logged per plugin.
  • $start reports back which plugins the host skipped, so the frontend stops counting them as started and can pick them up again on a later load cycle.
  • Plugins with no entry point at all, contributing only grammars, themes and the like, ended up with a rawModel getter that throws. They do show up in theia.extensions, where reading packageJSON must not throw, so they now get a real manifest.
  • PluginIconService and PluginIconThemeService can resolve icon fonts from hostedPlugin: URIs, not just from extracted VSIX paths.
  • VSXExtension resolves the registry URI lazily instead of in @postConstruct. It was requested eagerly for every extension in the list, which in browser-only meant one request per extension that was known to fail.

One breaking change: the browser-only EnvVariablesServer stub returned '' from getConfigDirUri(). new URI('') resolves to file:///, so in practice all application state was written to the OPFS root, next to the user's own folders, and the UTF-8 encoding override applied to the entire file tree. It now returns file:///.theia, matching the backend's default configuration folder. What that means for existing deployments is written up in doc/Migration.md.

How to test

The Typescript language tools are a good real-world check, they exercise a plugin with a frontend entry point as well as declarative contributions:

  1. npm run download:plugins
  2. npm run build:browser-only
  3. npm run start:browser-only, then open localhost:3000
  4. The extensions view should list the downloaded plugins as builtin
  5. Create a .ts file. Syntax highlighting and completion should work
  6. Switch to a theme contributed by one of the plugins and check that its icons and icon fonts load
  7. Reload the page. Anything a plugin wrote to globalState should still be there

Opening a terminal should tell you terminals aren't supported instead of failing with an error, and the application should start without terminal errors in the log.

Follow-ups

  • Plugins still have to be bundled at build time. Installing from OpenVSX at runtime isn't possible without a backend, so the extensions view stays read-only here.
  • Some VS Code web extensions still fail with module is undefined, thrown from inside their own bundled code in statements like module.exports = i. Deployment and loading themselves work, so this looks like a bundling issue on the extension side. Worth investigating separately.
  • Existing browser-only deployments are not migrated to the new /.theia config directory automatically. If that turns out to matter for adopters, a one-time migration on first run could be added.

Breaking changes

  • This PR introduces breaking changes and requires careful review. If yes, the breaking changes section in the changelog has been updated.

Attribution

Review checklist

Reminder for reviewers

robertjndw and others added 8 commits August 27, 2026 22:25
…ckend

Adds `toPluginUri` for plugin assets read through the `FileService` (color
themes, icon themes, icon fonts), which need a scheme to resolve against, and
`encodePluginAssetPath` to re-encode an already decoded path so names with `#`,
`?` or `%` survive a round trip.

Moves the plugin path constants out of `main/node` so the browser-only frontend
can share them, and adds the `frontend` plugin host id.

Signed-off-by: Robert Jandow <robert@jandow.de>
One plugin whose manifest cannot be loaded - a 404 on `package.json`, say -
used to reject the whole `Promise.all` and take every other plugin down with
it. Each plugin is now prepared in isolation and a failure only skips that one.

Because the host can now skip a plugin, `$start` reports back which ones it
skipped so the frontend stops reporting them as started, activated or stopped.
Skipped plugins go back to `LOADED` so a later load cycle can retry them.

Also gives a plugin with no entry point a real manifest: it contributes only
grammars, themes and the like, but still turns up in `theia.extensions`, where
reading `packageJSON` must not throw.

Signed-off-by: Robert Jandow <robert@jandow.de>
Adds the browser-only counterparts of the backend plugin services, bound
through a new `frontendOnly` entry point:

- `FrontendHostedPluginServer` / `FrontendPluginServer` serve the statically
  deployed plugins bundled into the application.
- `FrontendPluginPathService` derives the log, global and workspace storage
  paths that the backend would otherwise hand out, guarding the shared session
  log folder with a Web Lock.
- `HostedPluginFileSystemProvider` reads `hostedPlugin:` assets over HTTP.

Declarative-only plugins are deployed on the `frontend` host: without a
backend, the host inferred for them never answers and every RPC to it hangs,
saving included.

Signed-off-by: Robert Jandow <robert@jandow.de>
With a backend, icon fonts and icon themes sit in the extracted VSIX and their
paths are cut at the `extension` segment. Browser-only addresses them as
`hostedPlugin:/<id>/<path>`, where everything past the plugin id already is the
path relative to the plugin root, so both shapes now resolve.

Encodes those paths per segment as well, so `/` stays literal and asset names
containing `#`, `?` or `%` are not mangled.

Signed-off-by: Robert Jandow <robert@jandow.de>
…ookups

There is no backend to run a shell in, so `TerminalFrontendContribution` is
rebound to a browser-only subclass. Rebinding the contribution rather than only
`TerminalService` redirects every contribution point bound with
`toService(TerminalFrontendContribution)` - UI commands, profiles, and the
`TerminalService` token used by plugins, tasks and debug.

`VSXExtension` also resolved the registry URI eagerly in `@postConstruct`,
firing one doomed request per extension in the list. It is now resolved lazily
in `getRegistryLink`, the only method that needs it.

Signed-off-by: Robert Jandow <robert@jandow.de>
Co-authored-by: Stefan Dirix <sdirix@eclipsesource.com>
Signed-off-by: Robert Jandow <robert@jandow.de>
@github-project-automation github-project-automation Bot moved this to Waiting on reviewers in PR Backlog Aug 27, 2026
Signed-off-by: Robert Jandow <robert@jandow.de>
@robertjndw robertjndw changed the title Browser-only static extension support feat(browser-only): add static extension support Aug 27, 2026
@robertjndw

Copy link
Copy Markdown
Contributor Author

@sdirix took me ages, but finally managed to find some time to fix the regression and update the original code 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting on reviewers

Development

Successfully merging this pull request may close these issues.

1 participant