@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.
Files changed (38) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +9 -0
  2. package/.github/workflows/lint-pr-message.yml +11 -0
  3. package/.github/workflows/release-bun-library.yml +8 -0
  4. package/.github/workflows/validate-bun-library-pr.yml +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +303 -0
  7. package/bun.lock +27 -0
  8. package/index.ts +11 -0
  9. package/package.json +15 -0
  10. package/src/api/plugin/ExtensionDescriptor.ts +22 -0
  11. package/src/api/plugin/ExtensionFactory.ts +13 -0
  12. package/src/api/plugin/Plugin.ts +16 -0
  13. package/src/api/plugin_manager/ExtensionInfo.ts +19 -0
  14. package/src/api/plugin_manager/PluginManager.ts +38 -0
  15. package/src/plugin_manager/DefaultPluginManager.ts +114 -0
  16. package/src/plugin_manager/plugin_repository/ExtensionEntry.ts +33 -0
  17. package/src/plugin_manager/plugin_repository/PluginRepository.ts +30 -0
  18. package/src/plugin_manager/plugin_repository/UrlListPluginRepository.ts +105 -0
  19. package/src/plugin_manager/plugin_repository/UrlPluginSource.ts +34 -0
  20. package/src/plugin_manager/registry/ExtensionPointRegistry.ts +25 -0
  21. package/src/plugin_manager/registry/ExtensionRegistry.ts +38 -0
  22. package/src/plugin_manager/registry/InMemoryExtensionPointRegistry.ts +29 -0
  23. package/src/plugin_manager/registry/InMemoryExtensionRegistry.ts +69 -0
  24. package/src/plugin_manager/util/PluginLoader.ts +93 -0
  25. package/tests/fixtures/Constants.ts +15 -0
  26. package/tests/fixtures/InvalidPlugin1.ts +1 -0
  27. package/tests/fixtures/InvalidPlugin2.ts +5 -0
  28. package/tests/fixtures/InvalidPlugin3.ts +5 -0
  29. package/tests/fixtures/InvalidPlugin4.ts +6 -0
  30. package/tests/fixtures/InvalidPlugin5.ts +8 -0
  31. package/tests/fixtures/ValidPlugin1.ts +34 -0
  32. package/tests/plugin_manager/DefaultPluginManager_test.ts +96 -0
  33. package/tests/plugin_manager/plugin_repository/UrlListPluginRepository_test.ts +61 -0
  34. package/tests/plugin_manager/plugin_repository/UrlPluginSource_test.ts +48 -0
  35. package/tests/plugin_manager/registry/InMemoryExtensionPointRegistry_test.ts +41 -0
  36. package/tests/plugin_manager/registry/InMemoryExtensionRegistry_test.ts +72 -0
  37. package/tests/plugin_manager/util/PluginLoader_test.ts +107 -0
  38. package/tsconfig.json +27 -0
@@ -0,0 +1,9 @@
1
+ name: check-bun-dependencies
2
+ on:
3
+ workflow_dispatch:
4
+ schedule:
5
+ - cron: "0 0 * * *"
6
+ jobs:
7
+ call-check-bun-dependencies:
8
+ uses: flowscripter/.github/.github/workflows/check-bun-dependencies.yml@v1
9
+ secrets: inherit
@@ -0,0 +1,11 @@
1
+ name: lint-pr-message
2
+ on:
3
+ pull_request_target:
4
+ types:
5
+ - opened
6
+ - edited
7
+ - synchronize
8
+ jobs:
9
+ call-lint-pr-message:
10
+ uses: flowscripter/.github/.github/workflows/lint-pr-message.yml@v1
11
+ secrets: inherit
@@ -0,0 +1,8 @@
1
+ name: release-bun-library
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ jobs:
6
+ call-release-bun-library:
7
+ uses: flowscripter/.github/.github/workflows/release-bun-library.yml@v1
8
+ secrets: inherit
@@ -0,0 +1,8 @@
1
+ name: validate-bun-library-pr
2
+ on:
3
+ pull_request:
4
+ branches: [main]
5
+ jobs:
6
+ call-validate-bun-library-pr:
7
+ uses: flowscripter/.github/.github/workflows/validate-bun-library-pr.yml@v1
8
+ secrets: inherit
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
+ [![version](https://img.shields.io/github/v/release/flowscripter/dynamic-plugin-framework?sort=semver)](https://github.com/flowscripter/dynamic-plugin-framework/releases)
4
+ [![build](https://img.shields.io/github/actions/workflow/status/flowscripter/dynamic-plugin-framework/release-bun-library.yml)](https://github.com/flowscripter/dynamic-plugin-framework/actions/workflows/release-bun-library.yml)
5
+ [![coverage](https://codecov.io/gh/flowscripter/dynamic-plugin-framework/branch/main/graph/badge.svg?token=EMFT2938ZF)](https://codecov.io/gh/flowscripter/dynamic-plugin-framework)
6
+ [![docs](https://img.shields.io/badge/docs-API-blue)](https://flowscripter.github.io/dynamic-plugin-framework/index.html)
7
+ [![license: MIT](https://img.shields.io/github/license/flowscripter/dynamic-plugin-framework)](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
+ }