1 | # Engine Core
|
2 |
|
3 | This is the core library used to create a new plugin engine.
|
4 |
|
5 | | Name | Latest Version |
|
6 | | -----------------------------------------------| :------------------: |
|
7 | | [@remixproject/engine](.) | [![badge](https://img.shields.io/npm/v/@remixproject/engine.svg?style=flat-square)](https://www.npmjs.com/package/@remixproject/engine) |
|
8 |
|
9 | Use this library if you want to create an engine **for a new environment**.
|
10 |
|
11 | If you want to create an engine for an existing envrionment, use the specific library. For example :
|
12 | - Engine on the web : [@remixproject/engine-web](../web)
|
13 | - Engine on node : [@remixproject/engine-node](../node)
|
14 | - Engine on vscode : [@remixproject/engine-vscode](../vscode)
|
15 |
|
16 | ## Tutorial
|
17 |
|
18 | 1. [Getting Started](doc/tutorial/1-getting-started.md)
|
19 | 2. [Plugin Communication](doc/tutorial/2-plugin-communication.md)
|
20 | 3. [Host a Plugin with UI](doc/tutorial/3-hosted-plugin.md)
|
21 | 4. [External Plugins](doc/tutorial/4-external-plugins.md)
|
22 | 5. [Plugin Service](doc/tutorial/5-plugin-service.md)
|
23 |
|
24 | ## API
|
25 |
|
26 | | API | Description |
|
27 | | ----------------------------| :----------------------------------: |
|
28 | | [Engine](./api/engine.md) | Register plugins & redirect messages |
|
29 | | [Manager](./api/manager.md) | Activate & Deactive plugins |
|
30 |
|
31 |
|
32 | ## Connector
|
33 |
|
34 | The plugin connector is the main component of `@remixproject/engine`, it defines how an external plugin can connect to the engine. Checkout the [documentation](./doc/connector).
|
35 |
|
36 | --------------
|
37 |
|
38 | ## Getting started
|
39 | ```
|
40 | npm install @remixproject/engine
|
41 | ```
|
42 |
|
43 | The engine works a with two classes :
|
44 | - `PluginManager`: manage activation/deactivation
|
45 | - `Engine`: manage registration & communication
|
46 |
|
47 | ```typescript
|
48 | import { PluginManager, Engine, Plugin } from '@remixproject/engine'
|
49 |
|
50 | const manager = new PluginManager()
|
51 | const engine = new Engine()
|
52 | const plugin = new Plugin({ name: 'plugin-name' })
|
53 |
|
54 | // Wait for the manager to be loaded
|
55 | await engine.onload()
|
56 |
|
57 | // Register plugins
|
58 | engine.register([manager, plugin])
|
59 |
|
60 | // Activate plugins
|
61 | manager.activatePlugin('plugin-name')
|
62 | ```
|
63 |
|
64 | ### Registration
|
65 | The registration makes the plugin available for activation in the engine.
|
66 |
|
67 | To register a plugin you need:
|
68 | - `Profile`: The ID card of your plugin.
|
69 | - `Plugin`: A class that expose the logic of the plugin.
|
70 |
|
71 | ```typescript
|
72 | const profile = {
|
73 | name: 'console',
|
74 | methods: ['print']
|
75 | }
|
76 |
|
77 | class Console extends Plugin {
|
78 | constructor() {
|
79 | super(profile)
|
80 | }
|
81 | print(msg: string) {
|
82 | console.log(msg)
|
83 | }
|
84 | }
|
85 | const consolePlugin = new Console()
|
86 |
|
87 | // Register plugins
|
88 | engine.register(consolePlugin)
|
89 | ```
|
90 |
|
91 | > In the future, this part will be manage by a `Marketplace` plugin.
|
92 |
|
93 | ### Activation
|
94 | The activation process is managed by the `PluginManager`.
|
95 |
|
96 | Activating a plugin makes it visible to other plugins. Now they can communicate.
|
97 |
|
98 | ```typescript
|
99 | manager.activatePlugin('console')
|
100 | ```
|
101 |
|
102 | > The `PluginManager` is a plugin itself.
|
103 |
|
104 | ### Communication
|
105 | `Plugin` exposes a simple interface for communicate between plugins :
|
106 |
|
107 | - `call`: Call a method exposed by another plugin (This returns always a Promise).
|
108 | - `on`: Listen on event emitted by another plugin.
|
109 | - `emit`: Emit an event broadcasted to all listeners.
|
110 |
|
111 | This code will call the method `print` from the plugin `console` with the parameter `'My message'`.
|
112 | ```typescript
|
113 | plugin.call('console', 'print', 'My message')
|
114 | ```
|
115 |
|
116 | ### Full code example
|
117 | ```typescript
|
118 | import { PluginManager, Engine, Plugin } from '@remixproject/engine'
|
119 | const profile = {
|
120 | name: 'console',
|
121 | methods: ['print']
|
122 | }
|
123 |
|
124 | class Console extends Plugin {
|
125 | constructor() {
|
126 | super(profile)
|
127 | }
|
128 | print(msg: string) {
|
129 | console.log(msg)
|
130 | }
|
131 | }
|
132 |
|
133 | const manager = new PluginManager()
|
134 | const engine = new Engine()
|
135 | const emptyPlugin = new Plugin({ name: 'empty' })
|
136 | const consolePlugin = new Console()
|
137 |
|
138 | // Register plugins
|
139 | engine.register([manager, plugin, consolePlugin])
|
140 |
|
141 | // Activate plugins
|
142 | manager.activatePlugin(['empty', 'console'])
|
143 |
|
144 | // Plugin communication
|
145 | emptyPlugin.call('console', 'print', 'My message')
|
146 | ```
|
147 |
|
148 | --------------
|
149 |
|
150 | ## Permission
|
151 | The Engine comes with a permission system to protect the user from hostile plugins.
|
152 | There are two levels:
|
153 | - **Global**: at the `PluginManager` level.
|
154 | - **Local**: at the `Plugin` level.
|
155 |
|
156 | ### Global Permission
|
157 | Communication between plugins goes through the `PluginManager`'s permission system :
|
158 |
|
159 | ```typescript
|
160 | canActivatePlugin(from: Profile, to: Profile): Promise<boolean>
|
161 | ```
|
162 | Used when a plugin attempts to activate another one. By default when plugin "A" calls plugin "B", if "B" is not deactivated, "A" will attempt to active it before performing the call.
|
163 |
|
164 | ```typescript
|
165 | canDeactivatePlugin(from: Profile, to: Profile): Promise<boolean>
|
166 | ```
|
167 | Used when a plugin attempts to deactivate another one. By default only the `manager` and the plugin itself can deactivate a plugin.
|
168 |
|
169 | ```typescript
|
170 | canCall(from: Profile, to: Profile, method: string, message: string): Promise<boolean>
|
171 | ```
|
172 | Used by a plugin to protect a method (see Local Permission below).
|
173 |
|
174 | **Tip**: Each method returns a `Promise`. It's good practice to ask the user's permission through a GUI.
|
175 |
|
176 |
|
177 | ### Local Permission
|
178 | A plugin can protect some critical API by asking for user's permission:
|
179 |
|
180 | ```typescript
|
181 | askUserPermission(method: string, message: string): Promise<boolean>
|
182 | ```
|
183 | This method will call the `canCall` method from the `PluginManager` under the hood with the right params.
|
184 |
|
185 | In this example, a FileSystem plugin protects the `write` method :
|
186 | ```typescript
|
187 | class FileSystemPlugin extends Plugin {
|
188 |
|
189 | async write() {
|
190 | const from = this.currentRequest
|
191 | const canCall = await this.askUserPermission('write')
|
192 | if (!canCall) {
|
193 | throw new Error('You do not have the permission to call method "canCall" from "fs"')
|
194 | }
|
195 | }
|
196 | }
|
197 | ```
|
198 |
|
199 | ### ⚠️ When currentRequest is Mistaken ⚠️
|
200 | The permission system heavily relies on a queue of calls managed by the `Engine` and the property `currentRequest`.
|
201 | If you're calling a method from the plugin directly (without using the `Engine`) it will bypass the permission system. In this case, the results of `currentRequest` may **NOT** be correct.
|
202 |
|
203 | Example :
|
204 | ```typescript
|
205 | const fs = new FileSystemPlugin()
|
206 | const manager = new PluginManager()
|
207 | ...
|
208 | fs.call('manager', 'activatePlugin', 'terminal') // At this point `currentRequest` in manager is "fs"
|
209 | manager.deactivatePlugin('editor') // This will fail
|
210 | ```
|
211 |
|
212 | In the code above :
|
213 | 1. call to "activatePlugin" to enter the queue of the manager.
|
214 | 2. manager's `currentRequest` is "fs".
|
215 | 3. manager calls its own `deactivatePlugin` method.
|
216 | 4. **as the call doesn't use the Engine, it doesn't enter in the queue**: so `currentRequest` is still "fs".
|
217 | 5. `deactivatePlugin` checks the `currentRequest`. So now `currentRequest` incorrectly thinks that "fs" is trying to deactivate "terminal" and will not allow it. |
\ | No newline at end of file |