Skip to content

Commit 42d4658

Browse files
Improve documentation on the addition of Plugin API in the plugin host
Fixes #13067
1 parent de4c424 commit 42d4658

File tree

1 file changed

+194
-38
lines changed

1 file changed

+194
-38
lines changed
Lines changed: 194 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,90 @@
1-
# This document describes how to add new plugin api namespace
1+
# How to add a new plugin API namespace
22

3-
New Plugin API namespace should be packaged as Theia extension
3+
This document describes how to add new plugin API namespace in the plugin host.
4+
Depending on the plugin host we can either provide a frontend or backend API extension:
45

5-
## Provide your API or namespace
6+
- In the backend plugin host that runs in the Node environment in a separate process, we adapt the module loading to return a custom API object instead of loading a module with a particular name.
7+
- In the frontend plugin host that runs in the browser environment via a web worker, we import the API scripts and put it in the global context.
68

7-
This API developed in the way that you provide your API as separate npm package.
8-
In that package you can declare your api.
9-
Example `foo.d.ts`:
9+
In this document we focus on the implementation of a backend plugin API.
10+
However, both APIs can be provided by implementing and binding an `ExtPluginApiProvider` which should be packaged as a Theia extension.
1011

11-
```typescript
12-
declare module '@bar/foo' {
13-
export namespace fooBar {
14-
export function getFoo(): Foo;
15-
}
16-
}
17-
```
12+
## Declare your plugin API provider
1813

19-
## Declare `ExtPluginApiProvider` implementation
14+
The plugin API provider is executed on the respective plugin host to add your custom API namespace.
15+
16+
Example Foo Plugin API provider:
2017

2118
```typescript
2219
@injectable()
23-
export class FooPluginApiProvider implements ExtPluginApiProvider {
20+
export class FooExtPluginApiProvider implements ExtPluginApiProvider {
2421
provideApi(): ExtPluginApi {
2522
return {
2623
frontendExtApi: {
2724
initPath: '/path/to/foo/api/implementation.js',
2825
initFunction: 'fooInitializationFunction',
2926
initVariable: 'foo_global_variable'
3027
},
31-
backendInitPath: path.join(__dirname, 'path/to/backend/foo/implementation.js')
28+
backendInitPath: path.join(__dirname, 'foo-init')
3229
};
3330
}
3431
}
3532
```
3633

37-
## Then you need to register `FooPluginApiProvider`, add next sample in your backend module
34+
Register your Plugin API provider in a backend module:
35+
36+
```typescript
37+
bind(FooExtPluginApiProvider).toSelf().inSingletonScope();
38+
bind(Symbol.for(ExtPluginApiProvider)).toService(FooExtPluginApiProvider);
39+
```
40+
41+
## Define your API
3842

39-
Example:
43+
To ease the usage of your API, it should be developed as separate npm package that can be easily imported without any additional dependencies, cf, the VS Code API or the Theia Plugin API.
44+
45+
Example `foo.d.ts`:
4046

4147
```typescript
42-
bind(FooPluginApiProvider).toSelf().inSingletonScope();
43-
bind(Symbol.for(ExtPluginApiProvider)).toService(FooPluginApiProvider);
48+
declare module '@bar/foo' {
49+
export namespace fooBar {
50+
export function getFoo(): Promise<Foo>;
51+
}
52+
}
4453
```
4554

46-
## Next you need to implement `ExtPluginApiBackendInitializationFn`, which should handle `@bar/foo` module loading and instantiate `@foo/bar` API object, `path/to/backend/foo/implementation.js` example :
55+
## Implement your plugin API provider
56+
57+
In our example, we aim to provide a new API object for the backend.
58+
Theia expects that the `backendInitPath` that we specified in our API provider is a function called `provideApi` that follows the `ExtPluginApiBackendInitializationFn` signature.
59+
60+
Example `foo-init.ts`:
4761

4862
```typescript
49-
export const provideApi: ExtPluginApiBackendInitializationFn = (rpc: RPCProtocol, pluginManager: PluginManager) => {
50-
cheApiFactory = createAPIFactory(rpc);
51-
plugins = pluginManager;
63+
import * as fooBarAPI from '@bar/foo';
64+
65+
// Factory to create an API object for each plugin.
66+
let apiFactory: (plugin: Plugin) => typeof fooBarAPI;
67+
68+
// Map key is the plugin ID. Map value is the FooBar API object.
69+
const pluginsApiImpl = new Map<string, typeof fooBarAPI>();
70+
71+
// Singleton API object to use as a last resort.
72+
let defaultApi: typeof fooBarAPI;
73+
74+
// Have we hooked into the module loader yet?
75+
let hookedModuleLoader = false;
5276

53-
if (!isLoadOverride) {
77+
let plugins: PluginManager;
78+
79+
// Theia expects an exported 'provideApi' function
80+
export const provideApi: ExtPluginApiBackendInitializationFn = (rpc: RPCProtocol, manager: PluginManager) => {
81+
apiFactory = createAPIFactory(rpc);
82+
plugins = manager;
83+
84+
if (!hookedModuleLoader) {
5485
overrideInternalLoad();
55-
isLoadOverride = true;
86+
hookedModuleLoader = true;
5687
}
57-
5888
};
5989

6090
function overrideInternalLoad(): void {
@@ -63,22 +93,24 @@ function overrideInternalLoad(): void {
6393

6494
module._load = function (request: string, parent: any, isMain: {}) {
6595
if (request !== '@bar/foo') {
96+
// Pass the request to the next implementation down the chain
6697
return internalLoad.apply(this, arguments);
6798
}
6899

100+
// create custom API object and return that as a result of loading '@bar/foo'
69101
const plugin = findPlugin(parent.filename);
70102
if (plugin) {
71103
let apiImpl = pluginsApiImpl.get(plugin.model.id);
72104
if (!apiImpl) {
73-
apiImpl = cheApiFactory(plugin);
105+
apiImpl = apiFactory(plugin);
74106
pluginsApiImpl.set(plugin.model.id, apiImpl);
75107
}
76108
return apiImpl;
77109
}
78110

79111
if (!defaultApi) {
80112
console.warn(`Could not identify plugin for '@bar/foo' require call from ${parent.filename}`);
81-
defaultApi = cheApiFactory(emptyPlugin);
113+
defaultApi = apiFactory(emptyPlugin);
82114
}
83115

84116
return defaultApi;
@@ -90,23 +122,147 @@ function findPlugin(filePath: string): Plugin | undefined {
90122
}
91123
```
92124

93-
## Next you need to implement `createAPIFactory` factory function
125+
## Implement your API object
94126

95-
Example:
127+
We create a dedicated API object for each individual plugin as part of the module loading process.
128+
Each API object is returned as part of the module loading process if a script imports `@bar/foo` and should therefore match the API definition that we provided in the `*.d.ts` file.
129+
Multiple imports will not lead to the creation of multiple API objects as we cache it in our custom `overrideInternalLoad` function.
130+
131+
Example `foo-init.ts` (continued):
96132

97133
```typescript
98-
import * as fooApi from '@bar/foo';
99134
export function createAPIFactory(rpc: RPCProtocol): ApiFactory {
100-
const fooBarImpl = new FooBarImpl(rpc);
101-
return function (plugin: Plugin): typeof fooApi {
102-
const FooBar: typeof fooApi.fooBar = {
103-
getFoo(): fooApi.Foo{
104-
return fooBarImpl.getFooImpl();
135+
const fooExtImpl = new FooExtImpl(rpc);
136+
return function (plugin: Plugin): typeof fooBarAPI {
137+
const FooBar: typeof fooBarAPI.fooBar = {
138+
getFoo(): Promise<fooBarAPI.Foo> {
139+
return fooExtImpl.getFooImpl();
105140
}
106141
}
107-
return <typeof fooApi>{
142+
return <typeof fooBarAPI>{
108143
fooBar : FooBar
109144
};
110145
}
146+
}
147+
```
148+
149+
In the example above the API object creates a local object that will fulfill the API contract.
150+
The implementation details are hidden by the object and it could be a local implementation that only lives inside the plugin host but it could also be an implementation that uses the `RPCProtocol` to communicate with the main application to retrieve additional information.
151+
152+
### Implementing Main-Ext communication
153+
154+
In this document, we will only highlight the individual parts needed to establish the communication between the main application and the external plugin host.
155+
For a more elaborate example of an API that communicates with the main application, please have a look at the definition of the [Theia Plugin API](https://github.com/eclipse-theia/theia/blob/master/doc/Plugin-API.md).
156+
157+
First, we need to establish the communication on the RPC protocol by providing an implementation for our own side and generating a proxy for the opposite side.
158+
Proxies are identified using dedicated identifiers so we set them up first, together with the expected interfaces.
159+
`Ext` and `Main` interfaces contain the functions called over RCP and must start with `$`.
160+
Due to the asynchronous nature of the communication over RPC, the result should always be a `Promise` or `PromiseLike`.
161+
162+
```typescript
163+
export interface FooMain {
164+
$getFooImpl(): Promise<Foo>;
165+
}
166+
167+
export interface FooExt {
168+
// placeholder for callbacks for the main application to the extension
169+
}
170+
171+
// Plugin host will obtain a proxy using these IDs, main application will register an implementation for it.
172+
export const PLUGIN_RPC_CONTEXT = {
173+
FOO_MAIN: <ProxyIdentifier<FooMain>>createProxyIdentifier<FooMain>('FooMain')
174+
};
175+
176+
// Main application will obtain a proxy using these IDs, plugin host will register an implementation for it.
177+
export const MAIN_RPC_CONTEXT = {
178+
FOO_EXT: <ProxyIdentifier<FooExt>>createProxyIdentifier<FooExt>('FooExt')
179+
};
180+
```
181+
182+
On the plugin host side we can register our implementation and retrieve the proxy as part of our `createAPIFactory` implementation:
183+
184+
```typescript
185+
export class FooExtImpl implements FooExt {
186+
// Main application RCP counterpart
187+
private proxy: FooMain;
188+
189+
constructor(rpc: RPCProtocol) {
190+
rpc.set(MAIN_RPC_CONTEXT.FOO_EXT, this); // register ourselves
191+
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.FOO_MAIN); // retrieve proxy
192+
}
193+
194+
getFooImpl(): Promise<Foo> {
195+
return this.proxy.$getFooImpl();
196+
}
197+
}
198+
```
199+
200+
On the main side we need to implement the counterpart of the ExtPluginApiProvider, the `MainPluginApiProvider`:
201+
202+
```typescript
203+
@injectable()
204+
export class FooMainImpl implements FooMain {
205+
protected proxy: FooExt;
206+
207+
init(rpc: RPCProtocol) {
208+
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FOO_EXT);
209+
}
210+
211+
async $getFooImpl(): Promise<Foo> {
212+
return new Foo();
213+
}
214+
}
215+
216+
@injectable()
217+
export class FooMainPluginApiProvider implements MainPluginApiProvider {
218+
@inject(MessageService) protected messageService: MessageService;
219+
220+
initialize(rpc: RPCProtocol, container: interfaces.Container): void {
221+
this.messageService.info('We were called from an extension!');
222+
// create a new FooMainImpl as it is not bound as singleton
223+
const fooMainImpl = container.get(FooMainImpl);
224+
fooMainImpl.init(rpc);
225+
rpc.set(PLUGIN_RPC_CONTEXT.CAPTURE_MAIN, fooMainImpl);
226+
}
227+
}
228+
229+
export default new ContainerModule(bind => {
230+
bind(FooMainImpl).toSelf();
231+
bind(MainPluginApiProvider).to(FooMainPluginApiProvider).inSingletonScope();
232+
});
233+
```
234+
235+
In this example, we can already see the big advantage of going to the main application side as we have full access to our Theia services.
236+
237+
## Usage in a plugin
238+
239+
When using the API in a plugin the user can simply use the API as follows:
240+
241+
```typescript
242+
import * as foo from '@bar/foo';
243+
244+
foo.fooBar.getFoo();
245+
```
246+
247+
## Packaging
248+
249+
When bundling our application with the generated `gen-webpack.node.config.js` we need to make sure that our initialization function is bundled as a `commonjs2` library so it can be dynamically loaded.
250+
251+
```typescript
252+
const configs = require('./gen-webpack.config.js');
253+
const nodeConfig = require('./gen-webpack.node.config.js');
254+
255+
if (nodeConfig.config.entry) {
256+
/**
257+
* Add our initialization function. If unsure, look at the already generated entries for
258+
* the nodeConfig where an entry is added for the default 'backend-init-theia' initialization.
259+
*/
260+
nodeConfig.config.entry['foo-init'] = {
261+
import: require.resolve('@namespace/package/lib/node/foo-init'),
262+
library: { type: 'commonjs2' }
263+
};
264+
}
265+
266+
module.exports = [...configs, nodeConfig.config];
111267

112268
```

0 commit comments

Comments
 (0)