-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Implement "headless plugins" in a new plugin host outside of frontend connections #13138
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** @type {import('eslint').Linter.Config} */ | ||
module.exports = { | ||
extends: [ | ||
'../../configs/build.eslintrc.json' | ||
], | ||
parserOptions: { | ||
tsconfigRootDir: __dirname, | ||
project: 'tsconfig.json' | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<div align='center'> | ||
|
||
<br /> | ||
|
||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' /> | ||
|
||
<h2>ECLIPSE THEIA - API PROVIDER SAMPLE</h2> | ||
|
||
<hr /> | ||
|
||
</div> | ||
|
||
## Description | ||
|
||
The `@theia/api-provider-sample` extension is a programming example showing how to define and provide a custom API object for _plugins_ to use. | ||
The purpose of the extension is to: | ||
- provide developers with realistic coding examples of providing custom API objects | ||
- provide easy-to-use and test examples for features when reviewing pull requests | ||
|
||
The extension is for reference and test purposes only and is not published on `npm` (`private: true`). | ||
|
||
### Greeting of the Day | ||
|
||
The sample defines a `gotd` API that plugins can import and use to obtain tailored messages with which to greet the world, for example in their activation function. | ||
|
||
The source code is laid out in the `src/` tree as follows: | ||
|
||
- `gotd.d.ts` — the TypeScript definition of the `gotd` API object that plugins import to interact with the "Greeting of the Day" service | ||
- `plugin/` — the API initialization script and the implementation of the API objects (`GreetingExt` and similar interfaces). | ||
All code in this directory runs exclusively in the separate plugin-host Node process, isolated from the main Theia process, together with either headless plugins or the backend of VS Code plugins. | ||
The `GreetingExtImpl` and similar classes communicate with the actual API implementation (`GreetingMainImpl` etc.) classes in the main Theia process via RPC | ||
- `node/` — the API classes implementing `GreetingMain` and similar interfaces and the Inversify bindings that register the API provider. | ||
All code in this directory runs in the main Theia Node process | ||
- `common/` — the RPC API Ext/Main interface definitions corresponding to the backend of the `gotd` plugin API | ||
|
||
## Additional Information | ||
|
||
- [Theia - GitHub](https://github.com/eclipse-theia/theia) | ||
- [Theia - Website](https://theia-ide.org/) | ||
|
||
## License | ||
|
||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) | ||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) | ||
|
||
## Trademark | ||
"Theia" is a trademark of the Eclipse Foundation | ||
https://www.eclipse.org/theia |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"private": true, | ||
"name": "@theia/api-provider-sample", | ||
"version": "1.45.0", | ||
"description": "Theia - Example code to demonstrate Theia API Provider Extensions", | ||
"dependencies": { | ||
"@theia/core": "1.45.0", | ||
"@theia/plugin-ext-headless": "1.45.0", | ||
"@theia/plugin-ext": "1.45.0" | ||
}, | ||
"theiaExtensions": [ | ||
{ | ||
"backend": "lib/node/gotd-backend-module" | ||
} | ||
], | ||
"keywords": [ | ||
"theia-extension" | ||
], | ||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/eclipse-theia/theia.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/eclipse-theia/theia/issues" | ||
}, | ||
"homepage": "https://github.com/eclipse-theia/theia", | ||
"files": [ | ||
"lib", | ||
"src" | ||
], | ||
"types": "src/gotd.d.ts", | ||
"scripts": { | ||
"lint": "theiaext lint", | ||
"build": "theiaext build", | ||
"watch": "theiaext watch", | ||
"clean": "theiaext clean" | ||
}, | ||
"devDependencies": { | ||
"@theia/ext-scripts": "1.45.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// ***************************************************************************** | ||
// Copyright (C) 2024 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 { createProxyIdentifier } from '@theia/plugin-ext/lib/common/rpc-protocol'; | ||
import type { greeting } from '../gotd'; | ||
import { Event } from '@theia/core'; | ||
|
||
export enum GreetingKind { | ||
DIRECT = 1, | ||
QUIRKY = 2, | ||
SNARKY = 3, | ||
} | ||
|
||
export interface GreeterData { | ||
readonly uuid: string; | ||
greetingKinds: greeting.GreetingKind[]; | ||
}; | ||
|
||
export const GreetingMain = Symbol('GreetingMain'); | ||
export interface GreetingMain { | ||
$getMessage(greeterId: string): Promise<string>; | ||
|
||
$createGreeter(): Promise<GreeterData>; | ||
$destroyGreeter(greeterId: GreeterData['uuid']): Promise<void>; | ||
|
||
$updateGreeter(data: GreeterData): void; | ||
} | ||
|
||
export const GreetingExt = Symbol('GreetingExt'); | ||
export interface GreetingExt { | ||
|
||
// | ||
// External protocol | ||
// | ||
|
||
registerGreeter(): Promise<string>; | ||
unregisterGreeter(uuid: string): Promise<void>; | ||
|
||
getMessage(greeterId: string): Promise<string>; | ||
getGreetingKinds(greeterId: string): readonly greeting.GreetingKind[]; | ||
setGreetingKindEnabled(greeterId: string, greetingKind: greeting.GreetingKind, enable: boolean): void; | ||
onGreetingKindsChanged(greeterId: string): Event<readonly greeting.GreetingKind[]>; | ||
|
||
// | ||
// Internal protocol | ||
// | ||
|
||
$greeterUpdated(data: GreeterData): void; | ||
|
||
} | ||
|
||
export const PLUGIN_RPC_CONTEXT = { | ||
GREETING_MAIN: createProxyIdentifier<GreetingMain>('GreetingMain'), | ||
}; | ||
|
||
export const MAIN_RPC_CONTEXT = { | ||
GREETING_EXT: createProxyIdentifier<GreetingExt>('GreetingExt'), | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// ***************************************************************************** | ||
// Copyright (C) 2024 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 | ||
// ***************************************************************************** | ||
|
||
// Strictly speaking, the 'greeting' namespace is an unnecessary level of organization | ||
// but it serves to illustrate how API namespaces are implemented in the backend. | ||
export namespace greeting { | ||
export function createGreeter(): Promise<greeting.Greeter>; | ||
|
||
export enum GreetingKind { | ||
DIRECT = 1, | ||
QUIRKY = 2, | ||
SNARKY = 3, | ||
} | ||
|
||
export interface Greeter extends Disposable { | ||
greetingKinds: readonly GreetingKind[]; | ||
|
||
getMessage(): Promise<string>; | ||
|
||
setGreetingKind(kind: GreetingKind, enable = true): void; | ||
|
||
onGreetingKindsChanged: Event<readonly GreetingKind[]>; | ||
} | ||
} | ||
|
||
export interface Event<T> { | ||
(listener: (e: T) => unknown, thisArg?: unknown): Disposable; | ||
} | ||
|
||
export interface Disposable { | ||
dispose(): void; | ||
} | ||
|
||
namespace Disposable { | ||
export function create(func: () => void): Disposable; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// ***************************************************************************** | ||
// Copyright (C) 2024 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 * as path from 'path'; | ||
import { injectable } from '@theia/core/shared/inversify'; | ||
import { ExtPluginApi, ExtPluginApiProvider } from '@theia/plugin-ext-headless'; | ||
|
||
@injectable() | ||
export class ExtPluginGotdApiProvider implements ExtPluginApiProvider { | ||
provideApi(): ExtPluginApi { | ||
// We can support both backend plugins and headless plugins, so we have only one | ||
// entry-point script. Moreover, the application build packages that script in | ||
// the `../backend/` directory from its source `../plugin/` location, alongside | ||
// the scripts for all other plugin API providers. | ||
const universalInitPath = path.join(__dirname, '../backend/gotd-api-init'); | ||
jfaltermeier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return { | ||
backendInitPath: universalInitPath, | ||
headlessInitPath: universalInitPath | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// ***************************************************************************** | ||
// Copyright (C) 2024 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 { ContainerModule } from '@theia/core/shared/inversify'; | ||
import { ExtPluginApiProvider } from '@theia/plugin-ext'; | ||
import { ExtPluginGotdApiProvider } from './ext-plugin-gotd-api-provider'; | ||
import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution'; | ||
import { GotdMainPluginApiProvider } from './gotd-main-plugin-provider'; | ||
import { GreetingMain } from '../common/plugin-api-rpc'; | ||
import { GreetingMainImpl } from './greeting-main-impl'; | ||
|
||
export default new ContainerModule(bind => { | ||
bind(Symbol.for(ExtPluginApiProvider)).to(ExtPluginGotdApiProvider).inSingletonScope(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we specify here that this Ext API provider should be providing an API object only in the headless plugin-host and not the traditional plugin-host? In other words, if this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I dove further into having a single Theia Extension provide different API namespaces for traditional plugins vs headless ones, I see that it's not really feasible. So, I think the moral of the story here is: in that situation, the Theia application developer should have two Theia Extensions--one that provides the traditional API, one that provides the headless API. This really isn't a burden and it keeps things simple (better organized). But I guess the question I posed above is relevant even in that approach. How do I specify that a ExtPluginApiProvider should be used in one plugin-host vs the other (traditional vs headless) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’ve experimented with this PR and I believe I've confirmed my suspicions. There is no scoping of API providers to a specific plugin-host (normal vs headless). A contributed API provider is instantiated and exercised in both types of plugin-hosts. This seems problematic to me. It means a headless plugin can do an import of a normal (non-headless) custom API module and the corresponding API provider running in the headless plugin-host will give it to him--even if it makes no sense. The headless plugin will end up with an API object whose calls will never complete because the Ext object has a proxy to something that has no Main on the other end. But “wait”, you say. “Why is a headless plugin trying to interact with the UI? That’s not what headless plugins are for.” Absolutely. The current implementation in this PR is not responsible for that nonsense. It is, however, responsible for allowing the nonsense to get too far. The import of the normal API object from a headless plugin should fail but it doesn’t. There is a case where this actually would be desirable, though. Technically speaking, an API object need not interact with anything on the mainland. An app developer could theoretically provide an API object whose implementation can be carried out entirely in the plugin-host–i.e., one that does not make use of RPC. E.g., a Math API object, as silly as that would be. In that case, there would be an argument for an API provider to allow itself to be available for both types of plugins: normal and headless. Should we support that? If so…sure; that’s fine. But we still need the scoping capability to prevent it in the 99% of situations, where it’s not going to make sense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The distinction of which plugin host an API provider may publish its API should now be possible with commit f100932. |
||
bind(MainPluginApiProvider).to(GotdMainPluginApiProvider).inSingletonScope(); | ||
bind(GreetingMain).to(GreetingMainImpl).inSingletonScope(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// ***************************************************************************** | ||
// Copyright (C) 2024 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 { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution'; | ||
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol'; | ||
import { inject, injectable } from '@theia/core/shared/inversify'; | ||
import { GreetingMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; | ||
|
||
@injectable() | ||
export class GotdMainPluginApiProvider implements MainPluginApiProvider { | ||
@inject(GreetingMain) | ||
protected readonly greetingMain: GreetingMain; | ||
|
||
initialize(rpc: RPCProtocol): void { | ||
rpc.set(PLUGIN_RPC_CONTEXT.GREETING_MAIN, this.greetingMain); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the separate plugin-host Node process
I wonder if the use of
the
will mislead the reader into thinking there is only one plugin-host Node process. Usinga
may equally mislead the reader in thinking that the code will run in a Node process dedicated to that code. Of course, neither is true.How to succinctly express that here...oof. I leave that up to you. 😅