@flowscripter/dynamic-plugin-framework 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-bun-dependencies.yml +9 -0
- package/.github/workflows/lint-pr-message.yml +11 -0
- package/.github/workflows/release-bun-library.yml +8 -0
- package/.github/workflows/validate-bun-library-pr.yml +8 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/bun.lock +27 -0
- package/index.ts +11 -0
- package/package.json +15 -0
- package/src/api/plugin/ExtensionDescriptor.ts +22 -0
- package/src/api/plugin/ExtensionFactory.ts +13 -0
- package/src/api/plugin/Plugin.ts +16 -0
- package/src/api/plugin_manager/ExtensionInfo.ts +19 -0
- package/src/api/plugin_manager/PluginManager.ts +38 -0
- package/src/plugin_manager/DefaultPluginManager.ts +114 -0
- package/src/plugin_manager/plugin_repository/ExtensionEntry.ts +33 -0
- package/src/plugin_manager/plugin_repository/PluginRepository.ts +30 -0
- package/src/plugin_manager/plugin_repository/UrlListPluginRepository.ts +105 -0
- package/src/plugin_manager/plugin_repository/UrlPluginSource.ts +34 -0
- package/src/plugin_manager/registry/ExtensionPointRegistry.ts +25 -0
- package/src/plugin_manager/registry/ExtensionRegistry.ts +38 -0
- package/src/plugin_manager/registry/InMemoryExtensionPointRegistry.ts +29 -0
- package/src/plugin_manager/registry/InMemoryExtensionRegistry.ts +69 -0
- package/src/plugin_manager/util/PluginLoader.ts +93 -0
- package/tests/fixtures/Constants.ts +15 -0
- package/tests/fixtures/InvalidPlugin1.ts +1 -0
- package/tests/fixtures/InvalidPlugin2.ts +5 -0
- package/tests/fixtures/InvalidPlugin3.ts +5 -0
- package/tests/fixtures/InvalidPlugin4.ts +6 -0
- package/tests/fixtures/InvalidPlugin5.ts +8 -0
- package/tests/fixtures/ValidPlugin1.ts +34 -0
- package/tests/plugin_manager/DefaultPluginManager_test.ts +96 -0
- package/tests/plugin_manager/plugin_repository/UrlListPluginRepository_test.ts +61 -0
- package/tests/plugin_manager/plugin_repository/UrlPluginSource_test.ts +48 -0
- package/tests/plugin_manager/registry/InMemoryExtensionPointRegistry_test.ts +41 -0
- package/tests/plugin_manager/registry/InMemoryExtensionRegistry_test.ts +72 -0
- package/tests/plugin_manager/util/PluginLoader_test.ts +107 -0
- package/tsconfig.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Flowscripter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# dynamic-plugin-framework
|
|
2
|
+
|
|
3
|
+
[](https://github.com/flowscripter/dynamic-plugin-framework/releases)
|
|
4
|
+
[](https://github.com/flowscripter/dynamic-plugin-framework/actions/workflows/release-bun-library.yml)
|
|
5
|
+
[](https://codecov.io/gh/flowscripter/dynamic-plugin-framework)
|
|
6
|
+
[](https://flowscripter.github.io/dynamic-plugin-framework/index.html)
|
|
7
|
+
[](https://github.com/flowscripter/dynamic-plugin-framework/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
> Dynamic plugin framework for Bun based on Javascript Modules and import()
|
|
10
|
+
> function
|
|
11
|
+
|
|
12
|
+
[//]: # (TODO: Remove this when plugin indexing and discovery available.)
|
|
13
|
+
|
|
14
|
+
**STILL IN DEVELOPMENT**
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
This project provides a framework for defining plugins which can be dynamically
|
|
19
|
+
discovered and imported into a running process.
|
|
20
|
+
|
|
21
|
+
#### Key Features
|
|
22
|
+
|
|
23
|
+
- Universal support for both Bun and browser runtimes
|
|
24
|
+
- Dynamic plugin import using
|
|
25
|
+
[Javascript dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
|
|
26
|
+
- ES2015 module based
|
|
27
|
+
- Written in Typescript
|
|
28
|
+
|
|
29
|
+
#### Key Concepts
|
|
30
|
+
|
|
31
|
+
The framework's key concepts are borrowed from the Eclipse Project's extension
|
|
32
|
+
framework. The key concepts are:
|
|
33
|
+
|
|
34
|
+
- A `Plugin` provides one or more `Extension` implementations.
|
|
35
|
+
- Each `Extension` implementation declares an `ExtensionPoint` identifier and an
|
|
36
|
+
`ExtensionFactory` via an `ExtensionDescriptor`.
|
|
37
|
+
- A `HostApplication` instantiates a `PluginManager`.
|
|
38
|
+
- The `HostApplication` can register one or more `ExtensionPoint` identifiers
|
|
39
|
+
that the `PluginManager` should be aware of.
|
|
40
|
+
- The `PluginManager` scans one or more `PluginRepository` implementations to
|
|
41
|
+
find and register `Plugin` objects for any `ExtensionPoint` identifiers it is
|
|
42
|
+
aware of.
|
|
43
|
+
- The `HostApplication` uses the `PluginManager` to query for and select an
|
|
44
|
+
`Extension` for a desired `ExtensionPoint` identifier.
|
|
45
|
+
- The `PluginManager` uses the associated `ExtensionFactory` to instantiate the
|
|
46
|
+
selected `Extension`.
|
|
47
|
+
|
|
48
|
+
The following high level class diagram illustrates these relationships:
|
|
49
|
+
|
|
50
|
+
```mermaid
|
|
51
|
+
classDiagram
|
|
52
|
+
direction LR
|
|
53
|
+
|
|
54
|
+
class HostApplication {
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class ExtensionPoint1 {
|
|
58
|
+
<<interface>>
|
|
59
|
+
EXTENSION_POINT_1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class Plugin {
|
|
63
|
+
<<interface>>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class PluginManager {
|
|
67
|
+
<<interface>>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class PluginRepository {
|
|
71
|
+
<<interface>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class ExtensionDescriptor {
|
|
75
|
+
<<interface>>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class ExtensionFactory {
|
|
79
|
+
<<interface>>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class Plugin1 {
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class Extension1Descriptor {
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class Extension1Factory {
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class Extension1 {
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
PluginRepository --> "0..*" Plugin
|
|
95
|
+
PluginManager --> "1..*" PluginRepository: scans
|
|
96
|
+
Plugin --> "1..*" ExtensionDescriptor
|
|
97
|
+
ExtensionDescriptor --> ExtensionFactory
|
|
98
|
+
PluginManager --> "0..*" Plugin : registers
|
|
99
|
+
PluginManager ..> ExtensionFactory : invokes
|
|
100
|
+
Extension1Factory ..|> ExtensionFactory
|
|
101
|
+
Extension1Descriptor ..|> ExtensionDescriptor
|
|
102
|
+
Plugin1 ..|> Plugin
|
|
103
|
+
Extension1 ..|> ExtensionPoint1
|
|
104
|
+
Plugin1 --> Extension1Descriptor
|
|
105
|
+
Extension1Descriptor --> Extension1Factory
|
|
106
|
+
Extension1Factory ..> Extension1 : creates
|
|
107
|
+
Extension1Descriptor ..> ExtensionPoint1: declares
|
|
108
|
+
HostApplication --> PluginManager
|
|
109
|
+
HostApplication ..> ExtensionPoint1: defines
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The following sequence diagram illustrates the key steps for a `HostApplication`
|
|
113
|
+
to use a `PluginManager` for discovery and registration of `Plugin` instances:
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
%%{init: { "sequence": { "mirrorActors":false }}}%%
|
|
117
|
+
sequenceDiagram
|
|
118
|
+
HostApplication->>PluginManager:<<static import>>
|
|
119
|
+
Note over PluginRepository1,PluginRepository2: May use an index or scan plugins directly
|
|
120
|
+
HostApplication->>PluginManager:registerExtensions(EXTENSION_POINT_1)
|
|
121
|
+
activate PluginManager
|
|
122
|
+
PluginManager->>PluginRepository1:scanForExtensions(EXTENSION_POINT_1)
|
|
123
|
+
activate PluginRepository1
|
|
124
|
+
PluginRepository1-->>PluginManager:ExtensionEntry[]
|
|
125
|
+
deactivate PluginRepository1
|
|
126
|
+
PluginManager->>PluginRepository2:scanForExtensions(EXTENSION_POINT_1)
|
|
127
|
+
activate PluginRepository2
|
|
128
|
+
PluginRepository2-->>PluginManager:ExtensionEntry[]
|
|
129
|
+
deactivate PluginRepository2
|
|
130
|
+
PluginManager-->>HostApplication:ExtensionInfo[]
|
|
131
|
+
deactivate PluginManager
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Once registration has been performed, the `HostApplication` may query the
|
|
135
|
+
`PluginManager` for `Extensions` of known `ExtensionPoints` and then instantiate
|
|
136
|
+
them:
|
|
137
|
+
|
|
138
|
+
```mermaid
|
|
139
|
+
%%{init: { "sequence": { "mirrorActors":false, diagramMarginY: 0 }}}%%
|
|
140
|
+
sequenceDiagram
|
|
141
|
+
HostApplication->>PluginManager:getRegisteredExtensions(EXTENSION_POINT_1)
|
|
142
|
+
activate PluginManager
|
|
143
|
+
PluginManager->>PluginManager:filterExtensions
|
|
144
|
+
PluginManager->>HostApplication:ExtensionInfo[]
|
|
145
|
+
deactivate PluginManager
|
|
146
|
+
HostApplication->>HostApplication:select Extension1
|
|
147
|
+
HostApplication->>PluginManager:instantiate(extension1Handle)
|
|
148
|
+
activate PluginManager
|
|
149
|
+
PluginManager->>PluginRepository1:getExtensionDescriptorFromExtensionEntry(extensionEntry)
|
|
150
|
+
PluginRepository1->>Plugin1:<<dynamic import>>
|
|
151
|
+
Plugin1-->>PluginManager:ExtensionDescriptor
|
|
152
|
+
PluginManager->>Extension1Factory:create()
|
|
153
|
+
activate Extension1Factory
|
|
154
|
+
Extension1Factory->>Extension1:<<new>>
|
|
155
|
+
Extension1Factory-->>PluginManager:Extension1
|
|
156
|
+
deactivate Extension1Factory
|
|
157
|
+
PluginManager-->>HostApplication:Extension1
|
|
158
|
+
deactivate PluginManager
|
|
159
|
+
HostApplication->>Extension1:extensionPoint1Method
|
|
160
|
+
activate Extension1
|
|
161
|
+
deactivate Extension1
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
As `ExtensionPoints` are simply Typescript objects, for the purposes of testing
|
|
165
|
+
or validation, it is possible to bypass the framework altogether and import an
|
|
166
|
+
`Extension` and use it directly:
|
|
167
|
+
|
|
168
|
+
```mermaid
|
|
169
|
+
%%{init: { "sequence": { "mirrorActors":false }}}%%
|
|
170
|
+
sequenceDiagram
|
|
171
|
+
HostApplication->>Extension1:<<static import>>
|
|
172
|
+
HostApplication->>Extension1:extensionPoint1Method
|
|
173
|
+
activate Extension1
|
|
174
|
+
deactivate Extension1
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Examples
|
|
178
|
+
|
|
179
|
+
The following example projects are available which support execution in both a
|
|
180
|
+
terminal and a browser:
|
|
181
|
+
|
|
182
|
+
- [Plugin](https://github.com/flowscripter/example-plugin)
|
|
183
|
+
- [Host Application](https://github.com/flowscripter/example-host-application)
|
|
184
|
+
- [Host Webapp](https://github.com/flowscripter/example-host-webapp)
|
|
185
|
+
|
|
186
|
+
## API
|
|
187
|
+
|
|
188
|
+
The following diagram provides an overview of the `Plugin` API:
|
|
189
|
+
|
|
190
|
+
```mermaid
|
|
191
|
+
classDiagram
|
|
192
|
+
class Plugin {
|
|
193
|
+
<<interface>>
|
|
194
|
+
pluginData: any
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class ExtensionDescriptor {
|
|
198
|
+
<<interface>>
|
|
199
|
+
extensionPoint
|
|
200
|
+
extensionData: any
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class ExtensionFactory {
|
|
204
|
+
<<interface>>
|
|
205
|
+
create(hostData: any) Extension
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Plugin --> "1..*" ExtensionDescriptor: extensionDescriptors
|
|
209
|
+
ExtensionDescriptor --> ExtensionFactory: factory
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The following diagram provides an overview of the `PluginManager` API:
|
|
213
|
+
|
|
214
|
+
```mermaid
|
|
215
|
+
classDiagram
|
|
216
|
+
class PluginManager {
|
|
217
|
+
<<interface>>
|
|
218
|
+
registerExtensions(extensionPoint)
|
|
219
|
+
getRegisteredExtensions(extensionPoint) Set<ExtensionInfo>
|
|
220
|
+
instantiate(extensionHandle, hostData: Map<string, string>) Extension
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class ExtensionInfo {
|
|
224
|
+
<<interface>>
|
|
225
|
+
extensionHandle
|
|
226
|
+
extensionData: Map<string, string>
|
|
227
|
+
pluginData: Map<string, string>
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
API docs for the library:
|
|
232
|
+
|
|
233
|
+
[API Documentation](https://flowscripter.github.io/dynamic-plugin-framework/index.html)
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
Install dependencies:
|
|
238
|
+
|
|
239
|
+
`bun install`
|
|
240
|
+
|
|
241
|
+
Test:
|
|
242
|
+
|
|
243
|
+
`bun test`
|
|
244
|
+
|
|
245
|
+
**NOTE**: The following tasks use Deno as it excels at these and Bun does not
|
|
246
|
+
currently provide such functionality:
|
|
247
|
+
|
|
248
|
+
Format:
|
|
249
|
+
|
|
250
|
+
`deno fmt`
|
|
251
|
+
|
|
252
|
+
Lint:
|
|
253
|
+
|
|
254
|
+
`deno lint index.ts src/ tests/`
|
|
255
|
+
|
|
256
|
+
Generate HTML API Documentation:
|
|
257
|
+
|
|
258
|
+
`deno doc --html --name=dynamic-plugin-framework index.ts`
|
|
259
|
+
|
|
260
|
+
The following diagram provides an overview of the main internal classes:
|
|
261
|
+
|
|
262
|
+
```mermaid
|
|
263
|
+
classDiagram
|
|
264
|
+
class PluginManager {
|
|
265
|
+
<<interface>>
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
class DefaultPluginManager {
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class ExtensionPointRegistry {
|
|
272
|
+
<<interface>>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
class ExtensionRegistry {
|
|
276
|
+
<<interface>>
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
class PluginRepository {
|
|
280
|
+
<<interface>>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class UrlListPluginRepository {
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
class InMemoryExtensionRegistry {
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
class InMemoryExtensionPointRegistry {
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
PluginManager <|.. DefaultPluginManager
|
|
293
|
+
ExtensionPointRegistry <|.. InMemoryExtensionPointRegistry
|
|
294
|
+
ExtensionRegistry <|.. InMemoryExtensionRegistry
|
|
295
|
+
DefaultPluginManager --> ExtensionPointRegistry
|
|
296
|
+
DefaultPluginManager --> ExtensionRegistry
|
|
297
|
+
DefaultPluginManager --> "1..*" PluginRepository
|
|
298
|
+
PluginRepository <|.. UrlListPluginRepository
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT © Flowscripter
|
package/bun.lock
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "@flowscripter/dynamic-plugin-framework",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "^1.2.2",
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"typescript": "^5.7.3",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"packages": {
|
|
15
|
+
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
|
|
16
|
+
|
|
17
|
+
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
|
|
18
|
+
|
|
19
|
+
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
|
20
|
+
|
|
21
|
+
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
|
|
22
|
+
|
|
23
|
+
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
|
24
|
+
|
|
25
|
+
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
|
26
|
+
}
|
|
27
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { default as ExtensionFactory } from "./src/api/plugin/ExtensionFactory.ts";
|
|
2
|
+
export type { default as ExtensionDescriptor } from "./src/api/plugin/ExtensionDescriptor.ts";
|
|
3
|
+
export type { default as Plugin } from "./src/api/plugin/Plugin.ts";
|
|
4
|
+
export type { default as ExtensionInfo } from "./src/api/plugin_manager/ExtensionInfo.ts";
|
|
5
|
+
export type { default as PluginManager } from "./src/api/plugin_manager/PluginManager.ts";
|
|
6
|
+
export type { default as PluginRepository } from "./src/plugin_manager/plugin_repository/PluginRepository.ts";
|
|
7
|
+
export type { default as ExtensionEntry } from "./src/plugin_manager/plugin_repository/ExtensionEntry.ts";
|
|
8
|
+
export type { default as ExtensionRegistry } from "./src/plugin_manager/registry/ExtensionRegistry.ts";
|
|
9
|
+
export type { default as ExtensionPointRegistry } from "./src/plugin_manager/registry/ExtensionPointRegistry.ts";
|
|
10
|
+
export { default as DefaultPluginManager } from "./src/plugin_manager/DefaultPluginManager.ts";
|
|
11
|
+
export { default as UrlListPluginRepository } from "./src/plugin_manager/plugin_repository/UrlListPluginRepository.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowscripter/dynamic-plugin-framework",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "1.2.0",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/bun": "^1.2.2"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"typescript": "^5.7.3"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type ExtensionFactory from "./ExtensionFactory.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provides details of an Extension implementing an Extension Point.
|
|
5
|
+
*/
|
|
6
|
+
export default interface ExtensionDescriptor {
|
|
7
|
+
/**
|
|
8
|
+
* The implemented Extension Point
|
|
9
|
+
*/
|
|
10
|
+
readonly extensionPoint: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optional data provided by the Extension to the host application
|
|
14
|
+
*/
|
|
15
|
+
readonly extensionData?: ReadonlyMap<string, string>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An {@link ExtensionFactory} which can be used to create an instance of an Extension which implements
|
|
19
|
+
* the Extension Point.
|
|
20
|
+
*/
|
|
21
|
+
readonly factory: ExtensionFactory;
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A factory interface to create an Extension implementing an Extension Point.
|
|
3
|
+
*/
|
|
4
|
+
export default interface ExtensionFactory {
|
|
5
|
+
/**
|
|
6
|
+
* Construct and return an instance of an Extension implementing an Extension Point.
|
|
7
|
+
*
|
|
8
|
+
* @param hostData optional host application data to pass into the the Extension constructor.
|
|
9
|
+
*
|
|
10
|
+
* @return an Extension instance implementing an Extension Point.
|
|
11
|
+
*/
|
|
12
|
+
create(hostData?: Map<string, string>): Promise<unknown>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type ExtensionDescriptor from "./ExtensionDescriptor.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interface for a Plugin implementation.
|
|
5
|
+
*/
|
|
6
|
+
export default interface Plugin {
|
|
7
|
+
/**
|
|
8
|
+
* Array of {@link ExtensionDescriptor} instances describing all Extensions provided by the plugin.
|
|
9
|
+
*/
|
|
10
|
+
readonly extensionDescriptors: ReadonlyArray<ExtensionDescriptor>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optional data provided by the Plugin to the host application
|
|
14
|
+
*/
|
|
15
|
+
readonly pluginData?: ReadonlyMap<string, string>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides information for a particular Extension which has been registered with a {@link PluginManager}.
|
|
3
|
+
*/
|
|
4
|
+
export default interface ExtensionInfo {
|
|
5
|
+
/**
|
|
6
|
+
* Handle provided by a {@link PluginManager} to reference an Extension
|
|
7
|
+
*/
|
|
8
|
+
readonly extensionHandle: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Optional data provided by the {@link ExtensionDescriptor} associated with the Extension
|
|
12
|
+
*/
|
|
13
|
+
readonly extensionData?: ReadonlyMap<string, string>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Optional data provided by the {@link Plugin} providing the Extension
|
|
17
|
+
*/
|
|
18
|
+
readonly pluginData?: ReadonlyMap<string, string>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type ExtensionInfo from "./ExtensionInfo.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Used by a host application to manage discovery of Extensions provided by {@link Plugin} implementations.
|
|
5
|
+
*/
|
|
6
|
+
export default interface PluginManager {
|
|
7
|
+
/**
|
|
8
|
+
* Scan for Plugins and register their Extensions which implement the specified Extension Point.
|
|
9
|
+
*
|
|
10
|
+
* @param extensionPoint the Extension Point for which to register Extensions
|
|
11
|
+
*/
|
|
12
|
+
registerExtensions(extensionPoint: string): Promise<void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return {@link ExtensionInfo} instances for all registered Extensions implementing the specified Extension Point.
|
|
16
|
+
*
|
|
17
|
+
* @param extensionPoint the Extension Point for which to return {@link ExtensionInfo} instances
|
|
18
|
+
*
|
|
19
|
+
* @return array of {@link ExtensionInfo}
|
|
20
|
+
*/
|
|
21
|
+
getRegisteredExtensions(
|
|
22
|
+
extensionPoint: string,
|
|
23
|
+
): Promise<ReadonlyArray<ExtensionInfo>>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Instantiate a specific Extension.
|
|
27
|
+
*
|
|
28
|
+
* @param extensionHandle the opaque handle for the Extension provided by this Extension Manager instance via
|
|
29
|
+
* {@link ExtensionInfo.extensionHandle}.
|
|
30
|
+
* @param hostData optional data to be passed in to the Extension when instantiating it.
|
|
31
|
+
*
|
|
32
|
+
* @return an Extension instance implementing an Extension Point.
|
|
33
|
+
*/
|
|
34
|
+
instantiate(
|
|
35
|
+
extensionHandle: string,
|
|
36
|
+
hostData?: Map<string, string>,
|
|
37
|
+
): Promise<unknown>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type ExtensionPointRegistry from "./registry/ExtensionPointRegistry.ts";
|
|
2
|
+
import type ExtensionRegistry from "./registry/ExtensionRegistry.ts";
|
|
3
|
+
import InMemoryExtensionPointRegistry from "./registry/InMemoryExtensionPointRegistry.ts";
|
|
4
|
+
import InMemoryExtensionRegistry from "./registry/InMemoryExtensionRegistry.ts";
|
|
5
|
+
import type PluginManager from "../api/plugin_manager/PluginManager.ts";
|
|
6
|
+
import type ExtensionInfo from "../api/plugin_manager/ExtensionInfo.ts";
|
|
7
|
+
import type PluginRepository from "./plugin_repository/PluginRepository.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default implementation of a {@link PluginManager}.
|
|
11
|
+
*/
|
|
12
|
+
export default class DefaultPluginManager implements PluginManager {
|
|
13
|
+
private readonly extensionPointRegistry: ExtensionPointRegistry;
|
|
14
|
+
private readonly extensionRegistry: ExtensionRegistry;
|
|
15
|
+
private readonly pluginRepositories: Array<PluginRepository>;
|
|
16
|
+
private readonly pluginRepositoriesByExtensionHandle = new Map<
|
|
17
|
+
string,
|
|
18
|
+
PluginRepository
|
|
19
|
+
>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Constructor configures the instance using the optionally specified
|
|
23
|
+
* {@link ExtensionPointRegistry} and {@link ExtensionRegistry}.
|
|
24
|
+
*
|
|
25
|
+
* @param pluginRepositories One or more {@link PluginRepository} instances to use for plugin discovery.
|
|
26
|
+
* @param extensionPointRegistry optional {@link ExtensionPointRegistry]] implementation. Defaults to using
|
|
27
|
+
* {@link InMemoryExtensionPointRegistry}.
|
|
28
|
+
* @param extensionRegistry optional {@link ExtensionRegistry} implementation. Defaults to using
|
|
29
|
+
* {@link InMemoryExtensionRegistry}
|
|
30
|
+
*/
|
|
31
|
+
public constructor(
|
|
32
|
+
pluginRepositories: Array<PluginRepository>,
|
|
33
|
+
extensionPointRegistry?: ExtensionPointRegistry,
|
|
34
|
+
extensionRegistry?: ExtensionRegistry,
|
|
35
|
+
) {
|
|
36
|
+
this.pluginRepositories = pluginRepositories;
|
|
37
|
+
this.extensionPointRegistry = extensionPointRegistry ||
|
|
38
|
+
new InMemoryExtensionPointRegistry();
|
|
39
|
+
this.extensionRegistry = extensionRegistry ||
|
|
40
|
+
new InMemoryExtensionRegistry();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async registerExtensions(extensionPoint: string): Promise<void> {
|
|
44
|
+
if (await this.extensionPointRegistry.isRegistered(extensionPoint)) {
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await this.extensionPointRegistry.register(extensionPoint);
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < this.pluginRepositories.length; i++) {
|
|
51
|
+
const pluginRepository = this.pluginRepositories[i];
|
|
52
|
+
for await (
|
|
53
|
+
const extensionEntry of pluginRepository.scanForExtensions(
|
|
54
|
+
extensionPoint,
|
|
55
|
+
)
|
|
56
|
+
) {
|
|
57
|
+
const extensionHandle =
|
|
58
|
+
`${i}:${extensionEntry.pluginId}:${extensionEntry.extensionId}`;
|
|
59
|
+
|
|
60
|
+
this.pluginRepositoriesByExtensionHandle.set(
|
|
61
|
+
extensionHandle,
|
|
62
|
+
pluginRepository,
|
|
63
|
+
);
|
|
64
|
+
await this.extensionRegistry.register(extensionHandle, extensionEntry);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async getRegisteredExtensions(
|
|
70
|
+
extensionPoint: string,
|
|
71
|
+
): Promise<ReadonlyArray<ExtensionInfo>> {
|
|
72
|
+
const extensionMap = await this.extensionRegistry.getExtensions(
|
|
73
|
+
extensionPoint,
|
|
74
|
+
);
|
|
75
|
+
const registeredExtensions = new Array<ExtensionInfo>();
|
|
76
|
+
|
|
77
|
+
extensionMap.forEach((entry, handle) => {
|
|
78
|
+
registeredExtensions.push({
|
|
79
|
+
extensionHandle: handle,
|
|
80
|
+
extensionData: entry.extensionData,
|
|
81
|
+
pluginData: entry.pluginData,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return Promise.resolve(registeredExtensions);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @inheritDoc
|
|
90
|
+
*
|
|
91
|
+
* @throws *Error* if the specified Extension Handle is unknown
|
|
92
|
+
*/
|
|
93
|
+
public async instantiate(
|
|
94
|
+
extensionHandle: string,
|
|
95
|
+
hostData?: Map<string, string>,
|
|
96
|
+
): Promise<unknown> {
|
|
97
|
+
// Get the Extension Entry for the Extension Handle
|
|
98
|
+
const extensionEntry = await this.extensionRegistry.get(extensionHandle);
|
|
99
|
+
|
|
100
|
+
// Get the Plugin Repository for the Extension Handle
|
|
101
|
+
const pluginRepository = this.pluginRepositoriesByExtensionHandle.get(
|
|
102
|
+
extensionHandle,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!pluginRepository) {
|
|
106
|
+
return Promise.reject(`Extension handle ${extensionHandle} is unknown`);
|
|
107
|
+
}
|
|
108
|
+
// Get the Extension Descriptor from the Plugin Repository
|
|
109
|
+
const extensionDescriptor = await pluginRepository
|
|
110
|
+
.getExtensionDescriptorFromExtensionEntry(extensionEntry);
|
|
111
|
+
|
|
112
|
+
return extensionDescriptor.factory.create(hostData);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides information for a particular Extension which is provided by a plugin hosted in a {@link PluginRepository}.
|
|
3
|
+
*/
|
|
4
|
+
export default interface ExtensionEntry {
|
|
5
|
+
/**
|
|
6
|
+
* ID provided by a {@link PluginRepository} to reference a a Plugin.
|
|
7
|
+
*
|
|
8
|
+
* Note that this is only unique per {@link PluginRepository}.
|
|
9
|
+
*/
|
|
10
|
+
readonly pluginId: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ID provided by a {@link PluginRepository} to reference an Extension provided by a Plugin.
|
|
14
|
+
*
|
|
15
|
+
* Note that this is only unique per Plugin.
|
|
16
|
+
*/
|
|
17
|
+
readonly extensionId: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The implemented Extension Point
|
|
21
|
+
*/
|
|
22
|
+
readonly extensionPoint: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional data provided by the Plugin providing the Extension to the host application
|
|
26
|
+
*/
|
|
27
|
+
readonly pluginData?: ReadonlyMap<string, string>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional data provided by the Extension to the host application
|
|
31
|
+
*/
|
|
32
|
+
readonly extensionData?: ReadonlyMap<string, string>;
|
|
33
|
+
}
|