Skip to content
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

Browser-only static extension support #14776

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
70e7153
Initial setup and experiments with OPFS
robertjndw Sep 28, 2024
a28a085
Add missing methods and event emitter
robertjndw Sep 28, 2024
6578c25
Remove references to browserfs and replace it with OPFS
robertjndw Oct 3, 2024
e928eec
Improve coding style and remove logging
robertjndw Oct 4, 2024
28d70de
Merge branch 'eclipse-theia:master' into feat/switch-to-opfs
robertjndw Oct 7, 2024
05e26ad
Update CHANGELOG.md
robertjndw Oct 7, 2024
4fa4158
Add yarn.lock file and AsyncIterable to base tsconfig
robertjndw Oct 16, 2024
263815e
Implement feedback from PR and fix directory renaming bug
robertjndw Oct 16, 2024
e24978c
Adapt eslintrc config
robertjndw Oct 17, 2024
8fa511b
Fix linting issues
robertjndw Oct 17, 2024
39ab4f2
Add empty files for the plugin-ext for browser-only
robertjndw Oct 23, 2024
969c31b
Add initial implementation; still WIP
robertjndw Nov 18, 2024
459dbce
Add browser-only terminal frontend implementation
robertjndw Nov 26, 2024
3a693bb
Remove unused pluginDirectory property from PluginLocalOptions
robertjndw Nov 26, 2024
d0e4de7
Add example mock-plugin-metadata file
robertjndw Nov 26, 2024
c4620b0
Add script to move plugins for static browser-only implementation
robertjndw Nov 26, 2024
82d34b0
Update getDefaultShell method to return an empty string instead of 'n…
robertjndw Nov 27, 2024
3bf06fe
Add mock plugin metadata and update prepare-plugins script for browse…
robertjndw Dec 5, 2024
42a50f4
Fix lifecycle methods and activation events
robertjndw Dec 12, 2024
fbd9da8
Filter deployed plugins by specified plugin IDs in getDeployedPlugins…
robertjndw Jan 7, 2025
a1bc7e8
Rename mock plugin metadata to static plugin metadata
robertjndw Jan 7, 2025
82baacd
Refactor plugin ID handling to use PluginIdentifiers for consistency
robertjndw Jan 9, 2025
8fea9e5
Add documentation for Browser-Only static extension support and updat…
robertjndw Jan 25, 2025
f1ea8c0
Add TypeScript plugins and enhance ADOPTING.md
robertjndw Jan 25, 2025
4853c5c
Merge branch 'master' of github.com:robertjndw/theia into feat/browse…
robertjndw Jan 27, 2025
a46f214
Update commands in ADOPTING.md and package.json to use npm
robertjndw Jan 27, 2025
26837bf
Code cleanup
robertjndw Jan 27, 2025
3394ee3
Fix linting error
robertjndw Jan 27, 2025
6cacac4
More linting fixes
robertjndw Jan 27, 2025
42be38e
Ignore metadata file for linting
robertjndw Jan 27, 2025
d4b40fb
Apply suggestions to ADOPTING.md from review
robertjndw Jan 31, 2025
fff759b
Update packages/terminal/src/browser-only/terminal-frontend-only-modu…
robertjndw Jan 31, 2025
ec01e70
Update copyright information
robertjndw Jan 31, 2025
0b00454
Remove backendinit from example-static-plugin-metadata.ts
robertjndw Jan 31, 2025
e35cbc9
Remove backendInitPath from ADOPTING.md
robertjndw Jan 31, 2025
358f37a
Clarify the process for unpacking and installing extensions in ADOPTI…
robertjndw Jan 31, 2025
17f3871
Add check to skip existing plugin directories in prepare-plugins.js
robertjndw Jan 31, 2025
4c9fe81
Fix prepare-plugins.js
robertjndw Jan 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions ADOPTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## Browser-Only

### Static Extension Support

Currently, Theia Browser-Only supports the static configuration of extensions. This means only the extensions that are statically configured at build time will be available in the browser. Theia Browser-Only does not support dynamic extension loading at runtime.

When it comes to extension support, there are some restrictions to consider:
1. The extensions must be compatible to run in a browser environment. This means that ideally, the extension is already a web extension according to the [VS Code Web Extension API](https://code.visualstudio.com/api/extension-guides/web-extensions).
2. The extensions must not rely on Node.js APIs or other APIs that are not available in the browser.

There are two ways to retrieve extensions:
1. VSIX packages: Copy the `.vsix` files to the directory specified in the `package.json` file by the `theiaPluginsDir` property. (e.g. `"theiaPluginsDir": "plugins"`)
2. Open VSX Link: Specify a list of `id:link` mappings in the `package.json` file by the `theiaPlugins` property (e.g. `"theiaPlugins": { "vscodevim.vim": "https://open-vsx.org/api/vscodevim/vim/1.29.0/file/vscodevim.vim-1.29.0.vsix" }`). Extensions can be found on the [Open VSX Registry](https://open-vsx.org/) by searching for the extension and copying the link linked to the Download button.
When using the Theia CLI command `theia download:plugins`, the Theia CLI will download the `.vsix` files from the specified links and install them in the directory specified by the `theiaPluginsDir` property.

After the extensions are downloaded, they need to be unpacked and placed in the **`lib/frontend/hostedPlugin`** directory so that they can be used by Theia Browser-Only.

To achieve this, the `.vsix` files must be extracted, and their contents copied into the correct directory structure. This process involves:
1. Unpacking the `.vsix` files.
2. Copying the extracted contents into the **`lib/frontend/hostedPlugin`** directory. Make sure to copy the `extension` folder from the VSIX file and name it according to the extension's id (typically the publisher name and the extension name separated by a dot).

This process can be automated using a script. For an example implementation, you can refer to the [prepare-plugins.js script](examples/browser-only/prepare-plugins.js), which demonstrates how to extract and place the `.vsix` files in the appropriate location.

As Theia need to know which extensions are available, the `pluginMetadata` inside the `PluginLocalOptions` needs to be updated with the metadata of the extensions. This can be achieved by specifying the metadata and binding it to `PluginLocalOptions` (see this [example initialization](examples/api-samples/src/browser-only/plugin-sample/example-plugin-initialization.ts)).

As of now, the metadata is required to be specified manually. In the future, it is planned to automate this process.
An example of the format of meta can be found [here](examples/api-samples/src/browser-only/plugin-sample/example-static-plugin-metadata.ts). In the following section we will give tips on how to generate this metadata such that it adheres to the `DeployedPlugin` interface (see [`plugin-protocol.ts`](packages/plugin-ext/src/common/plugin-protocol.ts)).

#### Creating the static metadata
Most of the information to create the model in metadata can be found in the `package.json` file of the extension.

The general structure of each entry should be as follows:
```typescript
{
"metadata": {
...
},
"type": 0, // should always be set to 0
"contributes": {
...
},
},
```

The `metadata` object should contain the following fields:
```typescript
metadata: {
Copy link
Member

Choose a reason for hiding this comment

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

Looking at this description, it feels like almost all of these properties could be derived relatively easily just by parsing the package.json of the extension. Is there anything which is not hard coded and can't be derived from there?

'host': 'main', // Should always be set to 'main'
'model': {
'packagePath': '/home/user/theia/examples/browser-only/plugins/theia.helloworld-web-sample/extension', // Deprecated
Copy link
Member

Choose a reason for hiding this comment

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

What do you mean with "Deprecated"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to /theia/packages/plugin-ext/src/common/plugin-protocol.ts:573 this property is deprecated but in /theia/packages/plugin-ext/src/common/plugin-protocol.ts:60 it is not 🤔

'packageUri': 'file:///home/user/theia/examples/browser-only/plugins/theia.helloworld-web-sample/extension', // The absolute path to the extension's location inside the 'theiaPluginsDir' directory; prefixed with 'file://' protocol
'id': 'theia.helloworld-web-sample', // the extension's id; typically the publisher name and the extension name separated by a dot
'name': 'helloworld-web-sample', // the extension's name as specified in the 'package.json' file
'publisher': 'theia', // the extension's publisher as specified in the 'package.json' file
'version': '0.0.1', // the extension's version as specified in the 'package.json' file
'displayName': 'theia.helloworld-web-sample', // the extension's display name as specified in the 'package.json' file
'description': '', // the extension's description as specified in the 'package.json' file
'engine': {
'type': 'vscode', // incase of vscode web extension; if Theia plugin, set to 'theiaPlugin'
'version': '^1.74.0' // the version of the engine the extension is compatible with; specified in the 'engines' field of the 'package.json' file
},
'entryPoint': {
'frontend': 'dist/web/extension.js' // specified by the 'browser' field in the 'package.json' file for VScode web extensions or 'frontend' field for Theia plugins
}
iconUrl: 'hostedPlugin/theia_helloworld_web_sample/media%2Ficon.png', // optional: the path to the extension's icon; prefixed with 'hostedPlugin' and URL encoded
l10n: undefined,
readmeUrl: 'hostedPlugin/theia_helloworld_web_sample/.%2FREADME.md', // optional: the path to the extension's README file; prefixed with 'hostedPlugin' and URL encoded
licenseUrl: 'hostedPlugin/theia_helloworld_web_sample/.%2FLICENSE', // optional: the path to the extension's LICENSE file; prefixed with 'hostedPlugin' and URL encoded
},
'lifecycle': {
'startMethod': 'activate', // the method to call when the extension is activated; typically 'activate' for VS Code extensions and 'start' for Theia plugins
'stopMethod': 'deactivate', // the method to call when the extension is deactivated; typically 'deactivate' for VS Code extensions and 'stop' for Theia plugins
'frontendModuleName': 'theia_helloworld_web_sample', // the id specified above but with underscores instead of dots and dashes similar to iconUrl, readmeUrl, and licenseUrl
'frontendInitPath': 'plugin-vscode-init-fe.js', // the path to the frontend initialization script; only required for VS Code extensions
},
'outOfSync': false, // should always be set to false
'isUnderDevelopment': false // should always be set to false
},
```

For the `contributes` object, one can copy the `contributes` object from the `package.json` file of the extension. This object contains the contributions the extension makes to the Theia application. This includes 'activationEvents', 'commands', 'configuration', 'debuggers' etc. Details on the structure of the `contributes` object can be found in the [plugin-protocol file under PluginContribution](packages/plugin-ext/src/common/plugin-protocol.ts).

Once this static metadata is complete, one can build and start the Theia Browser-Only application. The extensions should now be available in the browser. To verify that the extensions are correctly packaged with the Theia Browser-Only application, one can check the `lib/frontend/hostedPlugin` directory. The extensions should be present in this directory with the correct `frontendModuleName`.
3 changes: 3 additions & 0 deletions examples/api-samples/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
ignorePatterns: [
"./src/browser-only/plugin-sample/example-static-plugin-metadata.ts" // Ignoring this file as it only contains static metadata
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { bindOPFSInitialization } from './filesystem/example-filesystem-initialization';
import { bindPluginInitialization } from './plugin-sample/example-plugin-initialization';

export default new ContainerModule((
bind: interfaces.Bind,
Expand All @@ -24,4 +25,5 @@ export default new ContainerModule((
rebind: interfaces.Rebind,
) => {
bindOPFSInitialization(bind, rebind);
bindPluginInitialization(bind, rebind);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2023 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// ****************************************************************************

import { interfaces } from '@theia/core/shared/inversify';
import { PluginLocalOptions } from '@theia/plugin-ext/lib/hosted/browser-only/frontend-hosted-plugin-server';
import { staticMetadata } from './example-static-plugin-metadata';

export const bindPluginInitialization = (bind: interfaces.Bind, rebind: interfaces.Rebind): void => {
const pluginLocalOptions = {
pluginMetadata: staticMetadata,
};
bind(PluginLocalOptions).toConstantValue(pluginLocalOptions);
};
Loading
Loading