@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 CHANGED
@@ -4,23 +4,23 @@
4
4
  "": {
5
5
  "name": "@flowscripter/dynamic-plugin-framework",
6
6
  "devDependencies": {
7
- "@types/bun": "^1.2.5",
7
+ "@types/bun": "^1.2.9",
8
8
  },
9
9
  "peerDependencies": {
10
- "typescript": "^5.8.2",
10
+ "typescript": "^5.8.3",
11
11
  },
12
12
  },
13
13
  },
14
14
  "packages": {
15
- "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
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.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
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.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
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.16",
19
+ "version": "1.3.17",
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/bun": "^1.2.5"
24
+ "@types/bun": "^1.2.9"
25
25
  },
26
26
  "peerDependencies": {
27
- "typescript": "^5.8.2"
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(private readonly urls: Set<string>) {
20
- if (!urls || (urls.size === 0)) {
21
- throw new Error(`Undefined or empty set of URLs provided`);
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.urls.forEach((url) => {
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: ${(err as Error).message}`,
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
- // As this is just a list of URLs we need to load each and then filter for extensionPoint
38
- for await (const candidateUrl of this.urls) {
39
- const plugin = await this.pluginSource.loadPlugin(new URL(candidateUrl));
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: candidateUrl,
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
- * @param url the URL of the module to import
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(() => new UrlListPluginRepository(new Set("foo"))).toThrow();
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