@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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type ExtensionEntry from "./ExtensionEntry.ts";
|
|
2
|
+
import type ExtensionDescriptor from "../../api/plugin/ExtensionDescriptor.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A source of {@link Plugin} implementations.
|
|
6
|
+
*/
|
|
7
|
+
export default interface PluginRepository {
|
|
8
|
+
/**
|
|
9
|
+
* Return an {@link ExtensionEntry} for each Extension hosted in the Plugin Repository which provides an
|
|
10
|
+
* Extension for the specified Extension Point.
|
|
11
|
+
*
|
|
12
|
+
* @param extensionPoint the Extension Point for which to return {@link ExtensionEntry} instances.
|
|
13
|
+
*
|
|
14
|
+
* @return an async iterable of {@link ExtensionEntry} instances for all matching Extensions.
|
|
15
|
+
*/
|
|
16
|
+
scanForExtensions(
|
|
17
|
+
extensionPoint: string,
|
|
18
|
+
): AsyncIterable<Readonly<ExtensionEntry>>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return the {@link ExtensionDescriptor} for the Extension identified by the specified {@link ExtensionEntry}.
|
|
22
|
+
*
|
|
23
|
+
* @param extensionEntry the {@link extensionEntry} for the desired Extension.
|
|
24
|
+
*
|
|
25
|
+
* @return an {@link ExtensionDescriptor} instance.
|
|
26
|
+
*/
|
|
27
|
+
getExtensionDescriptorFromExtensionEntry(
|
|
28
|
+
extensionEntry: ExtensionEntry,
|
|
29
|
+
): Promise<Readonly<ExtensionDescriptor>>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type PluginRepository from "./PluginRepository.ts";
|
|
2
|
+
import type ExtensionDescriptor from "../../api/plugin/ExtensionDescriptor.ts";
|
|
3
|
+
import type ExtensionEntry from "./ExtensionEntry.ts";
|
|
4
|
+
import UrlPluginSource from "./UrlPluginSource.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Implementation of {@link PluginRepository} using a provided set of URLs to access Plugins.
|
|
8
|
+
*
|
|
9
|
+
* When scanning for Plugins each provided URL will be used to attempt to load a Plugin and examine it.
|
|
10
|
+
*/
|
|
11
|
+
export default class UrlListPluginRepository implements PluginRepository {
|
|
12
|
+
private readonly urls: Set<string>;
|
|
13
|
+
private readonly pluginSource = new UrlPluginSource();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Constructor configures the instance using the specified set of URLs.
|
|
17
|
+
*
|
|
18
|
+
* @throws *Error* if the URL set contains a non-valid URL.
|
|
19
|
+
*/
|
|
20
|
+
public constructor(urls: Set<string>) {
|
|
21
|
+
if (!urls || (urls.size === 0)) {
|
|
22
|
+
throw new Error(`Undefined or empty set of URLs provided`);
|
|
23
|
+
}
|
|
24
|
+
this.urls = urls;
|
|
25
|
+
this.urls.forEach((url) => {
|
|
26
|
+
try {
|
|
27
|
+
new URL(url);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Cannot parse ${url} as a URL: ${(err as Error).message}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async *getExtensionEntryAsyncIterable(
|
|
37
|
+
extensionPoint: string,
|
|
38
|
+
): AsyncIterable<ExtensionEntry> {
|
|
39
|
+
// As this is just a list of URLs we need to load each and then filter for extensionPoint
|
|
40
|
+
for await (const candidateUrl of this.urls) {
|
|
41
|
+
const plugin = await this.pluginSource.loadPlugin(new URL(candidateUrl));
|
|
42
|
+
|
|
43
|
+
if (plugin) {
|
|
44
|
+
// filter Extensions in each Plugin by specified Extension Point
|
|
45
|
+
// and map any matches to an Extension Entry
|
|
46
|
+
for (let i = 0; i < plugin.extensionDescriptors.length; i++) {
|
|
47
|
+
const extensionDescripter = plugin.extensionDescriptors[i];
|
|
48
|
+
if (extensionDescripter.extensionPoint !== extensionPoint) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
yield {
|
|
52
|
+
pluginId: candidateUrl,
|
|
53
|
+
extensionId: `${i}`,
|
|
54
|
+
extensionPoint,
|
|
55
|
+
pluginData: plugin.pluginData,
|
|
56
|
+
extensionData: extensionDescripter.extensionData,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public scanForExtensions(
|
|
64
|
+
extensionPoint: string,
|
|
65
|
+
): AsyncIterable<ExtensionEntry> {
|
|
66
|
+
return this.getExtensionEntryAsyncIterable(extensionPoint);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @inheritDoc
|
|
71
|
+
*
|
|
72
|
+
* @throws *Error* if the specified ExtensionEntry is unknown.
|
|
73
|
+
*/
|
|
74
|
+
public async getExtensionDescriptorFromExtensionEntry(
|
|
75
|
+
extensionEntry: ExtensionEntry,
|
|
76
|
+
): Promise<Readonly<ExtensionDescriptor>> {
|
|
77
|
+
const plugin = await this.pluginSource.loadPlugin(
|
|
78
|
+
new URL(extensionEntry.pluginId),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!plugin) {
|
|
82
|
+
return Promise.reject(`Plugin ID ${extensionEntry.pluginId} is unknown`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let extensionId = -1;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
extensionId = parseInt(extensionEntry.extensionId);
|
|
89
|
+
} catch (_e) {
|
|
90
|
+
return Promise.reject(
|
|
91
|
+
`Extension ID ${extensionEntry.extensionId} is unknown`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
(extensionId < 0) || (extensionId >= plugin.extensionDescriptors.length)
|
|
97
|
+
) {
|
|
98
|
+
return Promise.reject(
|
|
99
|
+
`Extension ID ${extensionEntry.extensionId} is unknown`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Promise.resolve(plugin.extensionDescriptors[extensionId]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type Plugin from "../../api/plugin/Plugin.ts";
|
|
2
|
+
import loadPlugin from "../util/PluginLoader.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A source of {@link Plugin} instances sourced from URLs.
|
|
6
|
+
*
|
|
7
|
+
* Any loaded URLs will be cached so that subsequent calls to {@link loadPlugin} are (possibly) lower cost.
|
|
8
|
+
*/
|
|
9
|
+
export default class UrlPluginSource {
|
|
10
|
+
private readonly pluginsByUrl: Map<URL, Plugin> = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Attempt to load a {@link Plugin} object from the specified URL.
|
|
14
|
+
*
|
|
15
|
+
* @param url the URL to load from
|
|
16
|
+
*
|
|
17
|
+
* @returns a {@link Plugin} object if successful otherwise undefined
|
|
18
|
+
*/
|
|
19
|
+
public async loadPlugin(url: URL): Promise<Plugin | undefined> {
|
|
20
|
+
let plugin = this.pluginsByUrl.get(url);
|
|
21
|
+
|
|
22
|
+
if (!plugin) {
|
|
23
|
+
const pluginLoadResult = await loadPlugin(url.toString());
|
|
24
|
+
if (
|
|
25
|
+
pluginLoadResult.isValidPlugin &&
|
|
26
|
+
(pluginLoadResult.plugin !== undefined)
|
|
27
|
+
) {
|
|
28
|
+
plugin = pluginLoadResult.plugin;
|
|
29
|
+
this.pluginsByUrl.set(url, plugin);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return Promise.resolve(this.pluginsByUrl.get(url));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A registry of Extension Points.
|
|
3
|
+
*/
|
|
4
|
+
export default interface ExtensionPointRegistry {
|
|
5
|
+
/**
|
|
6
|
+
* Register a specified Extension Point.
|
|
7
|
+
*
|
|
8
|
+
* @param extensionPoint the Extension Point to register
|
|
9
|
+
*/
|
|
10
|
+
register(extensionPoint: string): Promise<void>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return all registered Extension Points.
|
|
14
|
+
*
|
|
15
|
+
* @return Set of Extension Points
|
|
16
|
+
*/
|
|
17
|
+
getAll(): Promise<ReadonlySet<string>>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns *true* if the specified Extension Point has been registered.
|
|
21
|
+
*
|
|
22
|
+
* @param extensionPoint the Extension Point to check
|
|
23
|
+
*/
|
|
24
|
+
isRegistered(extensionPoint: string): Promise<boolean>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type ExtensionEntry from "../plugin_repository/ExtensionEntry.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A registry of Extensions.
|
|
5
|
+
*/
|
|
6
|
+
export default interface ExtensionRegistry {
|
|
7
|
+
/**
|
|
8
|
+
* Register a specified {@link ExtensionEntry} with a specified Extension handle.
|
|
9
|
+
*
|
|
10
|
+
* @param extensionHandle a unique identifier under which to register the {@link ExtensionEntry}
|
|
11
|
+
* @param extensionEntry the {@link ExtensionEntry} for the Extension to register
|
|
12
|
+
*/
|
|
13
|
+
register(
|
|
14
|
+
extensionHandle: string,
|
|
15
|
+
extensionEntry: ExtensionEntry,
|
|
16
|
+
): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the specified registered {@link ExtensionEntry} instance.
|
|
20
|
+
*
|
|
21
|
+
* @param extensionHandle the handle for the desired {@link ExtensionEntry} instance
|
|
22
|
+
*
|
|
23
|
+
* @return an {@link ExtensionEntry} instance
|
|
24
|
+
*/
|
|
25
|
+
get(extensionHandle: string): Promise<Readonly<ExtensionEntry>>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Return {@link ExtensionEntry} instances for registered Extensions implementing the specified Extension Point.
|
|
29
|
+
*
|
|
30
|
+
* @param extensionPoint the Extension Point to match
|
|
31
|
+
*
|
|
32
|
+
* @return a map of extension handle to {@link ExtensionEntry} for all matching
|
|
33
|
+
* registered {@link ExtensionEntry} instances
|
|
34
|
+
*/
|
|
35
|
+
getExtensions(
|
|
36
|
+
extensionPoint: string,
|
|
37
|
+
): Promise<ReadonlyMap<string, ExtensionEntry>>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type ExtensionPointRegistry from "./ExtensionPointRegistry.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple implementation of an {@link ExtensionPointRegistry} using an in-memory Set.
|
|
5
|
+
*/
|
|
6
|
+
export default class InMemoryExtensionPointRegistry
|
|
7
|
+
implements ExtensionPointRegistry {
|
|
8
|
+
private readonly extensionPoints: Set<string> = new Set();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @inheritDoc
|
|
12
|
+
*
|
|
13
|
+
* @throws *Error* if the specified Extension Point has already been registered
|
|
14
|
+
*/
|
|
15
|
+
public async register(extensionPoint: string): Promise<void> {
|
|
16
|
+
if (await this.isRegistered(extensionPoint)) {
|
|
17
|
+
throw new Error(`Extension Point ${extensionPoint} already registered`);
|
|
18
|
+
}
|
|
19
|
+
this.extensionPoints.add(extensionPoint);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getAll(): Promise<ReadonlySet<string>> {
|
|
23
|
+
return Promise.resolve(this.extensionPoints);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public isRegistered(extensionPoint: string): Promise<boolean> {
|
|
27
|
+
return Promise.resolve(this.extensionPoints.has(extensionPoint));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type ExtensionRegistry from "./ExtensionRegistry.ts";
|
|
2
|
+
import type ExtensionEntry from "../plugin_repository/ExtensionEntry.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Simple implementation of an {@link ExtensionRegistry} using an in-memory map.
|
|
6
|
+
*/
|
|
7
|
+
export default class InMemoryExtensionRegistry implements ExtensionRegistry {
|
|
8
|
+
private readonly extensionEntriesByHandle: Map<string, ExtensionEntry> =
|
|
9
|
+
new Map();
|
|
10
|
+
|
|
11
|
+
private readonly extensionEntriesByExtensionPoint: Map<
|
|
12
|
+
string,
|
|
13
|
+
Map<string, ExtensionEntry>
|
|
14
|
+
> = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @inheritDoc
|
|
18
|
+
*
|
|
19
|
+
* @throws *Error* if the specified Extension handle has already been registered
|
|
20
|
+
*/
|
|
21
|
+
public register(
|
|
22
|
+
extensionHandle: string,
|
|
23
|
+
extensionEntry: ExtensionEntry,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
if (this.extensionEntriesByHandle.has(extensionHandle)) {
|
|
26
|
+
return Promise.reject(
|
|
27
|
+
`Extension handle ${extensionHandle} has already been registered`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
this.extensionEntriesByHandle.set(extensionHandle, extensionEntry);
|
|
31
|
+
|
|
32
|
+
let extensionEntries = this.extensionEntriesByExtensionPoint.get(
|
|
33
|
+
extensionEntry.extensionPoint,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (extensionEntries === undefined) {
|
|
37
|
+
extensionEntries = new Map();
|
|
38
|
+
this.extensionEntriesByExtensionPoint.set(
|
|
39
|
+
extensionEntry.extensionPoint,
|
|
40
|
+
extensionEntries,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
extensionEntries.set(extensionHandle, extensionEntry);
|
|
44
|
+
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @inheritDoc
|
|
50
|
+
*
|
|
51
|
+
* @throws *Error* if the specified Extension handle has not been registered
|
|
52
|
+
*/
|
|
53
|
+
public get(extensionHandle: string): Promise<Readonly<ExtensionEntry>> {
|
|
54
|
+
const extensionEntry = this.extensionEntriesByHandle.get(extensionHandle);
|
|
55
|
+
|
|
56
|
+
if (!extensionEntry) {
|
|
57
|
+
return Promise.reject(`Extension handle ${extensionHandle} is unknown`);
|
|
58
|
+
}
|
|
59
|
+
return Promise.resolve(Object.freeze(extensionEntry));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public getExtensions(
|
|
63
|
+
extensionPoint: string,
|
|
64
|
+
): Promise<ReadonlyMap<string, ExtensionEntry>> {
|
|
65
|
+
return Promise.resolve(
|
|
66
|
+
this.extensionEntriesByExtensionPoint.get(extensionPoint) || new Map(),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type Plugin from "../../api/plugin/Plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of a {@link loadPlugin} invocation.
|
|
5
|
+
*/
|
|
6
|
+
export interface PluginLoadResult {
|
|
7
|
+
/**
|
|
8
|
+
* `true` if the module at the specified URL is a valid {@link Plugin} implementation.
|
|
9
|
+
*/
|
|
10
|
+
isValidPlugin: boolean;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Populated with the {@link Plugin} object if {@link PluginLoadResult.isValidPlugin} is `true`
|
|
14
|
+
*/
|
|
15
|
+
plugin: Plugin | undefined;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Populated if {@link PluginLoadResult.isValidPlugin} is `false`
|
|
19
|
+
*/
|
|
20
|
+
error?: Error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Utility function to import a specified module and validate it is a {@link Plugin} implementation.
|
|
25
|
+
*
|
|
26
|
+
* @param url the URL of the module to import
|
|
27
|
+
*/
|
|
28
|
+
export default async function loadPlugin(
|
|
29
|
+
url: string,
|
|
30
|
+
): Promise<Readonly<PluginLoadResult>> {
|
|
31
|
+
const result: PluginLoadResult = {
|
|
32
|
+
isValidPlugin: false,
|
|
33
|
+
plugin: undefined,
|
|
34
|
+
error: undefined,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let module;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
module = await import(url);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
result.error = err as Error;
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const potentialPlugin = module.default;
|
|
47
|
+
|
|
48
|
+
// check the assumed Plugin has an array of extension descriptors
|
|
49
|
+
if (!Array.isArray(potentialPlugin.extensionDescriptors)) {
|
|
50
|
+
result.error = new Error(
|
|
51
|
+
`Plugin from ${url} does not provide an extensionDescriptors array`,
|
|
52
|
+
);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// At this point assume it is a valid plugin and then disprove this
|
|
57
|
+
result.isValidPlugin = true;
|
|
58
|
+
|
|
59
|
+
for (
|
|
60
|
+
const potentialExtensionDescriptor of potentialPlugin
|
|
61
|
+
.extensionDescriptors
|
|
62
|
+
) {
|
|
63
|
+
// check for valid {@link ExtensionDescriptor.extensionPoint}
|
|
64
|
+
if (
|
|
65
|
+
(potentialExtensionDescriptor.extensionPoint === undefined) ||
|
|
66
|
+
(!(potentialExtensionDescriptor.extensionPoint as unknown instanceof
|
|
67
|
+
String) &&
|
|
68
|
+
(typeof potentialExtensionDescriptor.extensionPoint !== "string"))
|
|
69
|
+
) {
|
|
70
|
+
result.isValidPlugin = false;
|
|
71
|
+
result.error = new Error(
|
|
72
|
+
`Plugin from ${url} does not provide an extensionPoint string in one of the extensionDescriptors`,
|
|
73
|
+
);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
// check for valid {@link ExtensionDescriptor.factory.create function}
|
|
77
|
+
if (
|
|
78
|
+
(potentialExtensionDescriptor.factory === undefined) ||
|
|
79
|
+
(potentialExtensionDescriptor.factory.create === undefined) ||
|
|
80
|
+
!(potentialExtensionDescriptor.factory.create as unknown instanceof
|
|
81
|
+
Function)
|
|
82
|
+
) {
|
|
83
|
+
result.isValidPlugin = false;
|
|
84
|
+
result.error = new Error(
|
|
85
|
+
`Plugin from ${url} does not provide a factory with a create function in one of the extensionDescriptors`,
|
|
86
|
+
);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
result.plugin = potentialPlugin;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const EXTENSION_POINT_1 = "foo";
|
|
2
|
+
export const EXTENSION_POINT_2 = "bar";
|
|
3
|
+
|
|
4
|
+
export const PLUGIN_1_ID = "Plugin1";
|
|
5
|
+
export const PLUGIN_2_ID = "Plugin2";
|
|
6
|
+
|
|
7
|
+
export const EXTENSION_1_ID = "1";
|
|
8
|
+
export const EXTENSION_2_ID = "2";
|
|
9
|
+
|
|
10
|
+
export const EXTENSION_1_HANDLE = "foobar1";
|
|
11
|
+
export const EXTENSION_2_HANDLE = "foobar2";
|
|
12
|
+
|
|
13
|
+
export interface ExtensionPoint1 {
|
|
14
|
+
sayHello(): string;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type Plugin from "../../src/api/plugin/Plugin.ts";
|
|
2
|
+
import type ExtensionDescriptor from "../../src/api/plugin/ExtensionDescriptor.ts";
|
|
3
|
+
import type ExtensionFactory from "../../src/api/plugin/ExtensionFactory.ts";
|
|
4
|
+
import { EXTENSION_POINT_1, type ExtensionPoint1 } from "./Constants.ts";
|
|
5
|
+
|
|
6
|
+
const extension1: ExtensionPoint1 = {
|
|
7
|
+
sayHello: () => {
|
|
8
|
+
return "hello";
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const extensionFactory1: ExtensionFactory = {
|
|
13
|
+
create: () => {
|
|
14
|
+
return Promise.resolve(extension1);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const extensionDescriptor1: ExtensionDescriptor = {
|
|
19
|
+
extensionPoint: EXTENSION_POINT_1,
|
|
20
|
+
|
|
21
|
+
factory: extensionFactory1,
|
|
22
|
+
|
|
23
|
+
extensionData: new Map([["foo", "bar"]]),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const plugin1: Plugin = {
|
|
27
|
+
pluginData: new Map([["foo", "bar"]]),
|
|
28
|
+
|
|
29
|
+
extensionDescriptors: [
|
|
30
|
+
extensionDescriptor1,
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default plugin1;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import DefaultPluginManager from "../../src/plugin_manager/DefaultPluginManager.ts";
|
|
4
|
+
import UrlListPluginRepository from "../../src/plugin_manager/plugin_repository/UrlListPluginRepository.ts";
|
|
5
|
+
import {
|
|
6
|
+
EXTENSION_POINT_1,
|
|
7
|
+
type ExtensionPoint1,
|
|
8
|
+
} from "../fixtures/Constants.ts";
|
|
9
|
+
|
|
10
|
+
const PLUGIN_1_URL = "file://" +
|
|
11
|
+
path.join(
|
|
12
|
+
path.dirname(Bun.fileURLToPath(import.meta.url)),
|
|
13
|
+
"../fixtures/ValidPlugin1.ts",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe("DefaultPluginManager Tests", () => {
|
|
17
|
+
test("Register without a plugin repository does not fail", async () => {
|
|
18
|
+
const defaultPluginManager = new DefaultPluginManager([]);
|
|
19
|
+
|
|
20
|
+
await defaultPluginManager.registerExtensions(EXTENSION_POINT_1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("Successfully register extension point", async () => {
|
|
24
|
+
const urlListPluginRepository = new UrlListPluginRepository(
|
|
25
|
+
new Set([PLUGIN_1_URL]),
|
|
26
|
+
);
|
|
27
|
+
const defaultPluginManager = new DefaultPluginManager([
|
|
28
|
+
urlListPluginRepository,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
let extensions = await defaultPluginManager.getRegisteredExtensions(
|
|
32
|
+
EXTENSION_POINT_1,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(extensions.length).toEqual(0);
|
|
36
|
+
|
|
37
|
+
await defaultPluginManager.registerExtensions(EXTENSION_POINT_1);
|
|
38
|
+
|
|
39
|
+
extensions = await defaultPluginManager.getRegisteredExtensions(
|
|
40
|
+
EXTENSION_POINT_1,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(extensions.length).toEqual(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Registering an extension point twice has no effect", async () => {
|
|
47
|
+
const urlListPluginRepository = new UrlListPluginRepository(
|
|
48
|
+
new Set([PLUGIN_1_URL]),
|
|
49
|
+
);
|
|
50
|
+
const defaultPluginManager = new DefaultPluginManager([
|
|
51
|
+
urlListPluginRepository,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
await defaultPluginManager.registerExtensions(EXTENSION_POINT_1);
|
|
55
|
+
|
|
56
|
+
let extensions = await defaultPluginManager.getRegisteredExtensions(
|
|
57
|
+
EXTENSION_POINT_1,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(extensions.length).toEqual(1);
|
|
61
|
+
|
|
62
|
+
await defaultPluginManager.registerExtensions(EXTENSION_POINT_1);
|
|
63
|
+
|
|
64
|
+
extensions = await defaultPluginManager.getRegisteredExtensions(
|
|
65
|
+
EXTENSION_POINT_1,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(extensions.length).toEqual(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("Instantiating an extension works", async () => {
|
|
72
|
+
const urlListPluginRepository = new UrlListPluginRepository(
|
|
73
|
+
new Set([PLUGIN_1_URL]),
|
|
74
|
+
);
|
|
75
|
+
const defaultPluginManager = new DefaultPluginManager([
|
|
76
|
+
urlListPluginRepository,
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
await defaultPluginManager.registerExtensions(EXTENSION_POINT_1);
|
|
80
|
+
|
|
81
|
+
const extensionInfos = await defaultPluginManager.getRegisteredExtensions(
|
|
82
|
+
EXTENSION_POINT_1,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const extensionInfo = extensionInfos[0];
|
|
86
|
+
|
|
87
|
+
expect(extensionInfo.pluginData?.get("foo"), "bar");
|
|
88
|
+
expect(extensionInfo.extensionData?.get("foo"), "bar");
|
|
89
|
+
|
|
90
|
+
const instance = await defaultPluginManager.instantiate(
|
|
91
|
+
extensionInfo.extensionHandle,
|
|
92
|
+
) as ExtensionPoint1;
|
|
93
|
+
|
|
94
|
+
expect(instance.sayHello()).toEqual("hello");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import UrlListPluginRepository from "../../../src/plugin_manager/plugin_repository/UrlListPluginRepository.ts";
|
|
4
|
+
import {
|
|
5
|
+
EXTENSION_POINT_1,
|
|
6
|
+
EXTENSION_POINT_2,
|
|
7
|
+
} from "../../fixtures/Constants.ts";
|
|
8
|
+
|
|
9
|
+
const PLUGIN_1_URL = "file://" +
|
|
10
|
+
path.join(
|
|
11
|
+
path.dirname(Bun.fileURLToPath(import.meta.url)),
|
|
12
|
+
"../../fixtures/ValidPlugin1.ts",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
describe("UrlPluginSource Tests", () => {
|
|
16
|
+
test("Throws on empty set of URLs", () => {
|
|
17
|
+
expect(() => new UrlListPluginRepository(new Set())).toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("Throws on invalid URL", () => {
|
|
21
|
+
expect(() => new UrlListPluginRepository(new Set("foo"))).toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Successfully scans for matching plugin", async () => {
|
|
25
|
+
const urlSet = new Set<string>();
|
|
26
|
+
urlSet.add(PLUGIN_1_URL);
|
|
27
|
+
|
|
28
|
+
const pluginRepository = new UrlListPluginRepository(urlSet);
|
|
29
|
+
|
|
30
|
+
let count = 0;
|
|
31
|
+
for await (
|
|
32
|
+
const extensionEntry of pluginRepository.scanForExtensions(
|
|
33
|
+
EXTENSION_POINT_1,
|
|
34
|
+
)
|
|
35
|
+
) {
|
|
36
|
+
expect(extensionEntry.pluginId).toEqual(PLUGIN_1_URL);
|
|
37
|
+
expect(extensionEntry.extensionPoint).toEqual(EXTENSION_POINT_1);
|
|
38
|
+
count += 1;
|
|
39
|
+
}
|
|
40
|
+
expect(count).toEqual(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("Successfully scans for non-matching plugin", async () => {
|
|
44
|
+
const urlSet = new Set<string>();
|
|
45
|
+
urlSet.add(PLUGIN_1_URL);
|
|
46
|
+
|
|
47
|
+
const pluginRepository = new UrlListPluginRepository(urlSet);
|
|
48
|
+
|
|
49
|
+
let count = 0;
|
|
50
|
+
for await (
|
|
51
|
+
const extensionEntry of pluginRepository.scanForExtensions(
|
|
52
|
+
EXTENSION_POINT_2,
|
|
53
|
+
)
|
|
54
|
+
) {
|
|
55
|
+
// this should not be called
|
|
56
|
+
expect(extensionEntry.pluginId, "not good");
|
|
57
|
+
count += 1;
|
|
58
|
+
}
|
|
59
|
+
expect(count).toEqual(0);
|
|
60
|
+
});
|
|
61
|
+
});
|