@flowscripter/dynamic-plugin-framework 1.3.16 → 1.3.17
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/bun.lock +5 -5
- package/package.json +3 -3
- package/src/plugin_manager/plugin_repository/UrlListPluginRepository.ts +31 -10
- package/src/plugin_manager/util/PluginLoader.ts +45 -1
- package/tests/plugin_manager/DefaultPluginManager_test.ts +3 -3
- package/tests/plugin_manager/plugin_repository/UrlListPluginRepository_test.ts +9 -5
package/bun.lock
CHANGED
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
"": {
|
|
5
5
|
"name": "@flowscripter/dynamic-plugin-framework",
|
|
6
6
|
"devDependencies": {
|
|
7
|
-
"@types/bun": "^1.2.
|
|
7
|
+
"@types/bun": "^1.2.9",
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"typescript": "^5.8.
|
|
10
|
+
"typescript": "^5.8.3",
|
|
11
11
|
},
|
|
12
12
|
},
|
|
13
13
|
},
|
|
14
14
|
"packages": {
|
|
15
|
-
"@types/bun": ["@types/bun@1.2.
|
|
15
|
+
"@types/bun": ["@types/bun@1.2.9", "", { "dependencies": { "bun-types": "1.2.9" } }, "sha512-epShhLGQYc4Bv/aceHbmBhOz1XgUnuTZgcxjxk+WXwNyDXavv5QHD1QEFV0FwbTSQtNq6g4ZcV6y0vZakTjswg=="],
|
|
16
16
|
|
|
17
17
|
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
|
|
18
18
|
|
|
19
19
|
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
|
20
20
|
|
|
21
|
-
"bun-types": ["bun-types@1.2.
|
|
21
|
+
"bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
|
|
22
22
|
|
|
23
|
-
"typescript": ["typescript@5.8.
|
|
23
|
+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
|
24
24
|
|
|
25
25
|
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
|
26
26
|
}
|
package/package.json
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
],
|
|
17
17
|
"module": "index.ts",
|
|
18
18
|
"type": "module",
|
|
19
|
-
"version": "1.3.
|
|
19
|
+
"version": "1.3.17",
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/bun": "^1.2.
|
|
24
|
+
"@types/bun": "^1.2.9"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"typescript": "^5.8.
|
|
27
|
+
"typescript": "^5.8.3"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -5,6 +5,7 @@ import UrlPluginSource from "./UrlPluginSource.ts";
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Implementation of {@link PluginRepository} using a provided set of URLs to access Plugins.
|
|
8
|
+
* Each plugin URL is linked to a list of extension points that are available in the Plugin.
|
|
8
9
|
*
|
|
9
10
|
* When scanning for Plugins each provided URL will be used to attempt to load a Plugin and examine it.
|
|
10
11
|
*/
|
|
@@ -16,29 +17,49 @@ export default class UrlListPluginRepository implements PluginRepository {
|
|
|
16
17
|
*
|
|
17
18
|
* @throws *Error* if the URL set contains a non-valid URL.
|
|
18
19
|
*/
|
|
19
|
-
public constructor(
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
public constructor(
|
|
21
|
+
private readonly urlsAndExtensionPoints: Set<
|
|
22
|
+
{ url: string; extensionPoints: string[] }
|
|
23
|
+
>,
|
|
24
|
+
) {
|
|
25
|
+
if (!urlsAndExtensionPoints || (urlsAndExtensionPoints.size === 0)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Undefined or empty set of URL and extension points provided`,
|
|
28
|
+
);
|
|
22
29
|
}
|
|
23
|
-
this.
|
|
30
|
+
this.urlsAndExtensionPoints.forEach((urlAndExtensionPoint) => {
|
|
24
31
|
try {
|
|
25
|
-
new URL(url);
|
|
32
|
+
new URL(urlAndExtensionPoint.url);
|
|
26
33
|
} catch (err) {
|
|
27
34
|
throw new Error(
|
|
28
|
-
`Cannot parse ${url} as a URL: ${
|
|
35
|
+
`Cannot parse ${urlAndExtensionPoint.url} as a URL: ${
|
|
36
|
+
(err as Error).message
|
|
37
|
+
}`,
|
|
29
38
|
);
|
|
30
39
|
}
|
|
40
|
+
if (
|
|
41
|
+
!urlAndExtensionPoint.extensionPoints ||
|
|
42
|
+
(urlAndExtensionPoint.extensionPoints.length === 0)
|
|
43
|
+
) {
|
|
44
|
+
throw new Error(`Undefined or empty set of extension points provided`);
|
|
45
|
+
}
|
|
31
46
|
});
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
private async *getExtensionEntryAsyncIterable(
|
|
35
50
|
extensionPoint: string,
|
|
36
51
|
): AsyncIterable<ExtensionEntry> {
|
|
37
|
-
//
|
|
38
|
-
for await (const
|
|
39
|
-
|
|
52
|
+
// We need to iterate each entry and filter for extensionPoint
|
|
53
|
+
for await (const urlAndExtensionPoints of this.urlsAndExtensionPoints) {
|
|
54
|
+
if (!urlAndExtensionPoints.extensionPoints.includes(extensionPoint)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const plugin = await this.pluginSource.loadPlugin(
|
|
58
|
+
new URL(urlAndExtensionPoints.url),
|
|
59
|
+
);
|
|
40
60
|
|
|
41
61
|
if (plugin) {
|
|
62
|
+
// Once we have loaded the plugin, double check on the extension points in the plugin
|
|
42
63
|
// filter Extensions in each Plugin by specified Extension Point
|
|
43
64
|
// and map any matches to an Extension Entry
|
|
44
65
|
for (let i = 0; i < plugin.extensionDescriptors.length; i++) {
|
|
@@ -47,7 +68,7 @@ export default class UrlListPluginRepository implements PluginRepository {
|
|
|
47
68
|
continue;
|
|
48
69
|
}
|
|
49
70
|
yield {
|
|
50
|
-
pluginId:
|
|
71
|
+
pluginId: urlAndExtensionPoints.url,
|
|
51
72
|
extensionId: `${i}`,
|
|
52
73
|
extensionPoint,
|
|
53
74
|
pluginData: plugin.pluginData,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import type Plugin from "../../api/plugin/Plugin.ts";
|
|
2
5
|
|
|
3
6
|
/**
|
|
@@ -20,10 +23,38 @@ export interface PluginLoadResult {
|
|
|
20
23
|
error?: Error;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
async function getLocalUrl(remoteUrl: string) {
|
|
27
|
+
const url = new URL(remoteUrl);
|
|
28
|
+
const urlPath = url.pathname;
|
|
29
|
+
|
|
30
|
+
// look in local cache location
|
|
31
|
+
const localPluginFolder = path.join(os.homedir(), ".flowscripter", "plugin");
|
|
32
|
+
const localPluginPath = path.join(localPluginFolder, urlPath);
|
|
33
|
+
const installePluginFile = Bun.file(localPluginPath);
|
|
34
|
+
const exists = await installePluginFile.exists();
|
|
35
|
+
|
|
36
|
+
if (exists) {
|
|
37
|
+
return localPluginPath;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await mkdir(path.dirname(localPluginPath), { recursive: true });
|
|
41
|
+
|
|
42
|
+
const result = await fetch(remoteUrl);
|
|
43
|
+
|
|
44
|
+
await Bun.write(installePluginFile, result);
|
|
45
|
+
|
|
46
|
+
return localPluginPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
/**
|
|
24
50
|
* Utility function to import a specified module and validate it is a {@link Plugin} implementation.
|
|
25
51
|
*
|
|
26
|
-
*
|
|
52
|
+
* If the URL specified is not local filesystem (e.g. http(s)://) and the dynamic import fails with ENOENT
|
|
53
|
+
* then the remote item will be fetched and the contents of the response will be used as the module source
|
|
54
|
+
* i.e. a local dynamic import will be attempted. This is because the Bun runtime does not support importing
|
|
55
|
+
* remote modules directly as per https://github.com/oven-sh/bun/issues/38
|
|
56
|
+
*
|
|
57
|
+
* @param url the URL of the module to import.
|
|
27
58
|
*/
|
|
28
59
|
export default async function loadPlugin(
|
|
29
60
|
url: string,
|
|
@@ -39,6 +70,19 @@ export default async function loadPlugin(
|
|
|
39
70
|
try {
|
|
40
71
|
module = await import(url);
|
|
41
72
|
} catch (err) {
|
|
73
|
+
if ((err instanceof Error) && (err.message === "ENOENT")) {
|
|
74
|
+
const urlLower = url.toLowerCase();
|
|
75
|
+
if (urlLower.startsWith("http://") || urlLower.startsWith("https://")) {
|
|
76
|
+
const localUrl = await getLocalUrl(url);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
module = await import(localUrl);
|
|
80
|
+
} catch (err2) {
|
|
81
|
+
result.error = err2 as Error;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
42
86
|
result.error = err as Error;
|
|
43
87
|
return result;
|
|
44
88
|
}
|
|
@@ -22,7 +22,7 @@ describe("DefaultPluginManager Tests", () => {
|
|
|
22
22
|
|
|
23
23
|
test("Successfully register extension point", async () => {
|
|
24
24
|
const urlListPluginRepository = new UrlListPluginRepository(
|
|
25
|
-
new Set([PLUGIN_1_URL]),
|
|
25
|
+
new Set([{ url: PLUGIN_1_URL, extensionPoints: [EXTENSION_POINT_1] }]),
|
|
26
26
|
);
|
|
27
27
|
const defaultPluginManager = new DefaultPluginManager([
|
|
28
28
|
urlListPluginRepository,
|
|
@@ -45,7 +45,7 @@ describe("DefaultPluginManager Tests", () => {
|
|
|
45
45
|
|
|
46
46
|
test("Registering an extension point twice has no effect", async () => {
|
|
47
47
|
const urlListPluginRepository = new UrlListPluginRepository(
|
|
48
|
-
new Set([PLUGIN_1_URL]),
|
|
48
|
+
new Set([{ url: PLUGIN_1_URL, extensionPoints: [EXTENSION_POINT_1] }]),
|
|
49
49
|
);
|
|
50
50
|
const defaultPluginManager = new DefaultPluginManager([
|
|
51
51
|
urlListPluginRepository,
|
|
@@ -70,7 +70,7 @@ describe("DefaultPluginManager Tests", () => {
|
|
|
70
70
|
|
|
71
71
|
test("Instantiating an extension works", async () => {
|
|
72
72
|
const urlListPluginRepository = new UrlListPluginRepository(
|
|
73
|
-
new Set([PLUGIN_1_URL]),
|
|
73
|
+
new Set([{ url: PLUGIN_1_URL, extensionPoints: [EXTENSION_POINT_1] }]),
|
|
74
74
|
);
|
|
75
75
|
const defaultPluginManager = new DefaultPluginManager([
|
|
76
76
|
urlListPluginRepository,
|
|
@@ -18,12 +18,16 @@ describe("UrlPluginSource Tests", () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
test("Throws on invalid URL", () => {
|
|
21
|
-
expect(() =>
|
|
21
|
+
expect(() =>
|
|
22
|
+
new UrlListPluginRepository(
|
|
23
|
+
new Set([{ url: "foo", extensionPoints: ["bar"] }]),
|
|
24
|
+
)
|
|
25
|
+
).toThrow();
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
test("Successfully scans for matching plugin", async () => {
|
|
25
|
-
const urlSet = new Set<string>();
|
|
26
|
-
urlSet.add(PLUGIN_1_URL);
|
|
29
|
+
const urlSet = new Set<{ url: string; extensionPoints: string[] }>();
|
|
30
|
+
urlSet.add({ url: PLUGIN_1_URL, extensionPoints: [EXTENSION_POINT_1] });
|
|
27
31
|
|
|
28
32
|
const pluginRepository = new UrlListPluginRepository(urlSet);
|
|
29
33
|
|
|
@@ -41,8 +45,8 @@ describe("UrlPluginSource Tests", () => {
|
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
test("Successfully scans for non-matching plugin", async () => {
|
|
44
|
-
const urlSet = new Set<string>();
|
|
45
|
-
urlSet.add(PLUGIN_1_URL);
|
|
48
|
+
const urlSet = new Set<{ url: string; extensionPoints: string[] }>();
|
|
49
|
+
urlSet.add({ url: PLUGIN_1_URL, extensionPoints: [EXTENSION_POINT_1] });
|
|
46
50
|
|
|
47
51
|
const pluginRepository = new UrlListPluginRepository(urlSet);
|
|
48
52
|
|