@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,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,5 @@
1
+ export default {
2
+ extensionDescriptors: [{
3
+ extensionData: "foo",
4
+ }],
5
+ };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ extensionDescriptors: [{
3
+ extensionPoint: "foo",
4
+ }],
5
+ };
@@ -0,0 +1,6 @@
1
+ export default {
2
+ extensionDescriptors: [{
3
+ extensionPoint: "foo",
4
+ factory: "bar",
5
+ }],
6
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ extensionDescriptors: [{
3
+ extensionPoint: "foo",
4
+ factory: {
5
+ bar: () => {},
6
+ },
7
+ }],
8
+ };
@@ -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
+ });