fluxplugin 1.0.3 → 1.0.4

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 (50) hide show
  1. package/README.md +15 -12
  2. package/dist/browser/chunks/chunk-AV4KI7LI.js +1792 -0
  3. package/dist/browser/chunks/{chunk-TL63576R.js → chunk-OAYWYU5O.js} +48 -11
  4. package/dist/browser/index.js +3 -1
  5. package/dist/browser/manager-ui.js +386 -185
  6. package/dist/browser/react.js +46 -2
  7. package/dist/cli.js +3622 -257
  8. package/dist/node/chunks/{chunk-EVX2O65A.js → chunk-SDCGUBEW.js} +49 -11
  9. package/dist/node/chunks/{chunk-C7VWZZOX.js → chunk-ZLA5XUXN.js} +175 -51
  10. package/dist/node/cli-api.js +742 -76
  11. package/dist/node/index.js +3 -1
  12. package/dist/node/main.js +8 -2
  13. package/dist/node/preload.js +1 -1
  14. package/dist/types/cli/build.d.ts +5 -0
  15. package/dist/types/cli/cli.d.ts +2 -0
  16. package/dist/types/cli/dist.d.ts +18 -0
  17. package/dist/types/cli/host-templates.d.ts +2 -0
  18. package/dist/types/cli/store.d.ts +13 -0
  19. package/dist/types/cli/templates.d.ts +2 -0
  20. package/dist/types/core/manifest.d.ts +15 -1
  21. package/dist/types/core/protocol.d.ts +18 -5
  22. package/dist/types/core/registry.d.ts +11 -0
  23. package/dist/types/core/routes.d.ts +3 -1
  24. package/dist/types/main/adapters.d.ts +3 -0
  25. package/dist/types/main/assets.d.ts +8 -0
  26. package/dist/types/main/github-registry.d.ts +2 -1
  27. package/dist/types/main/index.d.ts +1 -0
  28. package/dist/types/main/registry.d.ts +55 -18
  29. package/dist/types/main/stores.d.ts +7 -1
  30. package/dist/types/manager-ui/index.d.ts +35 -3
  31. package/dist/types/react/base-css.d.ts +6 -2
  32. package/dist/types/react/http-bridge.d.ts +5 -0
  33. package/dist/types/react/i18n.d.ts +33 -0
  34. package/dist/types/react/index.d.ts +5 -0
  35. package/dist/types/react/markdown.d.ts +2 -1
  36. package/dist/types/react/media.d.ts +33 -0
  37. package/dist/types/react/settings.d.ts +6 -0
  38. package/dist/types/react/sidebar.d.ts +42 -0
  39. package/dist/types/react/theme.d.ts +42 -4
  40. package/dist/types/react/ui.d.ts +42 -0
  41. package/docs/CUSTOMIZING.md +56 -0
  42. package/docs/STORE.md +19 -0
  43. package/docs/screenshots/plugin-page.png +0 -0
  44. package/docs/screenshots/settings-appearance.png +0 -0
  45. package/docs/screenshots/site-guide-dark.png +0 -0
  46. package/docs/screenshots/site-home.png +0 -0
  47. package/docs/screenshots/site-mobile.png +0 -0
  48. package/docs/screenshots/site-search.png +0 -0
  49. package/package.json +1 -1
  50. package/dist/browser/chunks/chunk-VXQJIDXW.js +0 -1232
@@ -63,6 +63,7 @@ __export(core_exports, {
63
63
  normalizePath: () => normalizePath,
64
64
  parseOrThrow: () => parseOrThrow,
65
65
  parseVersion: () => parseVersion,
66
+ parseVideo: () => parseVideo,
66
67
  pluginApi: () => pluginApi,
67
68
  preloadMember: () => preloadMember,
68
69
  routePatternMatches: () => routePatternMatches,
@@ -610,7 +611,7 @@ var manifestSchema = s.object({
610
611
  /** Detailed description as Markdown (default: ./README.md). Rendered safely: no raw HTML. */
611
612
  readme: s.string().optional(),
612
613
  /** Explanation video (https URL; YouTube and Vimeo are embedded, other hosts become a link). */
613
- video: s.string({ pattern: /^https:\/\/\S+$/ }).optional(),
614
+ video: s.string({ pattern: /^(https:\/\/\S+|\.\/\S+)$/ }).optional(),
614
615
  categories: s.array(s.string()).default([]),
615
616
  keywords: s.array(s.string()).default([]),
616
617
  /** Extra links: docs, support, changelog, funding... */
@@ -671,24 +672,33 @@ function validateManifest(raw, opts = {}) {
671
672
  }
672
673
  return { ok: issues.length === 0, manifest: issues.length ? void 0 : manifest, issues, warnings };
673
674
  }
674
- function videoEmbedUrl(url) {
675
+ var MEDIA_EXT = /\.(mp4|m4v|webm|ogv|ogg|mov)(\?.*)?$/i;
676
+ function parseVideo(url) {
675
677
  if (!url) return void 0;
678
+ if (url.startsWith("./")) return MEDIA_EXT.test(url) ? { kind: "html5", url, src: url } : void 0;
676
679
  let u;
677
680
  try {
678
681
  u = new URL(url);
679
682
  } catch {
680
683
  return void 0;
681
684
  }
685
+ if (u.protocol !== "https:") return void 0;
686
+ if (MEDIA_EXT.test(u.pathname)) return { kind: "html5", url, src: url };
682
687
  const host = u.hostname.replace(/^www\./, "");
688
+ const last = u.pathname.split("/").filter(Boolean).pop() ?? "";
683
689
  let id;
684
690
  if (host === "youtu.be") id = u.pathname.slice(1);
685
- else if (host === "youtube.com" || host === "m.youtube.com") id = u.searchParams.get("v") ?? (/^\/(embed|shorts)\//.test(u.pathname) ? u.pathname.split("/")[2] : void 0);
686
- if (id && /^[\w-]{6,20}$/.test(id)) return `https://www.youtube-nocookie.com/embed/${id}`;
687
- if (host === "vimeo.com" || host === "player.vimeo.com") {
688
- const vid = u.pathname.split("/").filter(Boolean).pop();
689
- if (vid && /^\d+$/.test(vid)) return `https://player.vimeo.com/video/${vid}`;
690
- }
691
- return void 0;
691
+ else if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") id = u.searchParams.get("v") ?? (/^\/(embed|shorts|live)\//.test(u.pathname) ? u.pathname.split("/")[2] : void 0);
692
+ if (id && /^[\w-]{6,20}$/.test(id)) return { kind: "youtube", url, embedUrl: `https://www.youtube-nocookie.com/embed/${id}` };
693
+ if ((host === "vimeo.com" || host === "player.vimeo.com") && /^\d+$/.test(last)) return { kind: "vimeo", url, embedUrl: `https://player.vimeo.com/video/${last}` };
694
+ if (host === "dailymotion.com" && /^\/(embed\/)?video\//.test(u.pathname) && /^\w+$/.test(last)) return { kind: "dailymotion", url, embedUrl: `https://www.dailymotion.com/embed/video/${last}` };
695
+ if (host === "dai.ly" && /^\w+$/.test(last)) return { kind: "dailymotion", url, embedUrl: `https://www.dailymotion.com/embed/video/${last}` };
696
+ if (host === "loom.com" && /^\/(share|embed)\//.test(u.pathname) && /^[\w-]+$/.test(last)) return { kind: "loom", url, embedUrl: `https://www.loom.com/embed/${last}` };
697
+ if (host === "streamable.com" && /^\w+$/.test(last)) return { kind: "streamable", url, embedUrl: `https://streamable.com/e/${last}` };
698
+ return { kind: "link", url };
699
+ }
700
+ function videoEmbedUrl(url) {
701
+ return parseVideo(url)?.embedUrl;
692
702
  }
693
703
 
694
704
  // src/core/events.ts
@@ -1121,6 +1131,32 @@ var MenuRegistry = class extends Registry {
1121
1131
  register(item, owner) {
1122
1132
  return this.add(`${item.location}:${item.id}`, item, owner);
1123
1133
  }
1134
+ /**
1135
+ * Hierarchical view of a location: items that name a `parent`, plus the parent's own `children`, are nested under it
1136
+ * (sorted by order). Unknown parents fall back to top level, so a missing plugin never hides its siblings.
1137
+ */
1138
+ tree(location) {
1139
+ const flat = this.items(location);
1140
+ const byId = new Map(flat.map((i) => [i.id, i]));
1141
+ const toNode = (i) => ({ ...i, children: [...i.children ?? []] });
1142
+ const nodes = new Map(flat.map((i) => [i.id, toNode(i)]));
1143
+ const roots = [];
1144
+ for (const i of flat) {
1145
+ const n = nodes.get(i.id);
1146
+ const parent = i.parent && i.parent !== i.id ? nodes.get(i.parent) : void 0;
1147
+ if (parent && byId.has(i.parent)) parent.children.push(n);
1148
+ else roots.push(n);
1149
+ }
1150
+ const sort = (list) => {
1151
+ list.sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.label.localeCompare(b.label));
1152
+ for (const n of list) {
1153
+ if (n.children?.length) sort(n.children);
1154
+ else delete n.children;
1155
+ }
1156
+ };
1157
+ sort(roots);
1158
+ return roots;
1159
+ }
1124
1160
  items(location) {
1125
1161
  return this.list().filter((m) => m.location === location && (!m.when || m.when())).sort((a, b) => (a.group ?? "").localeCompare(b.group ?? "") || (a.order ?? 100) - (b.order ?? 100) || a.label.localeCompare(b.label));
1126
1162
  }
@@ -2387,10 +2423,11 @@ var PluginManager = class {
2387
2423
  id: `route:${route.fullPath}`,
2388
2424
  location: menu.location ?? "sidebar",
2389
2425
  label: menu.label ?? meta.title ?? route.fullPath,
2390
- icon: meta.icon,
2426
+ icon: menu.icon ?? meta.icon,
2391
2427
  path: route.fullPath,
2392
2428
  order: menu.order,
2393
- group: menu.group
2429
+ group: menu.group,
2430
+ parent: menu.parent
2394
2431
  },
2395
2432
  owner
2396
2433
  );
@@ -2678,6 +2715,7 @@ export {
2678
2715
  menuLocations,
2679
2716
  manifestSchema,
2680
2717
  validateManifest,
2718
+ parseVideo,
2681
2719
  videoEmbedUrl,
2682
2720
  matchesPattern,
2683
2721
  EventBus,
@@ -8,10 +8,10 @@ import {
8
8
  compareVersions,
9
9
  core_exports,
10
10
  errorMessage,
11
+ parseVideo,
11
12
  serializeError,
12
- validateManifest,
13
- videoEmbedUrl
14
- } from "./chunk-EVX2O65A.js";
13
+ validateManifest
14
+ } from "./chunk-SDCGUBEW.js";
15
15
 
16
16
  // src/main/framework.ts
17
17
  import { promises as fsp, watch } from "node:fs";
@@ -398,20 +398,80 @@ async function copyDir(src, dest) {
398
398
  }
399
399
  }
400
400
  function entriesFromIndex(index, baseUrl, source, query = "") {
401
+ const list = Array.isArray(index) ? index : index?.plugins ?? [];
401
402
  const q = query.toLowerCase();
402
- return (index.plugins ?? []).filter((p) => p && typeof p.id === "string" && typeof p.version === "string" && typeof p.url === "string").filter((p) => !q || `${p.id} ${p.name ?? ""} ${p.description ?? ""}`.toLowerCase().includes(q)).map((p) => ({
403
- id: p.id,
404
- name: p.name ?? p.id,
405
- version: p.version,
406
- description: p.description,
407
- author: p.author,
408
- permissions: p.permissions ?? [],
409
- location: new URL(p.url, baseUrl).href,
410
- source,
411
- sha256: p.sha256,
412
- homepage: p.homepage,
413
- icon: p.icon
414
- }));
403
+ const abs = (u) => {
404
+ if (!u) return void 0;
405
+ try {
406
+ return new URL(u, baseUrl).href;
407
+ } catch {
408
+ return void 0;
409
+ }
410
+ };
411
+ const out = [];
412
+ for (const p of list) {
413
+ if (!p || typeof p.id !== "string" || typeof p.version !== "string") continue;
414
+ const isFolder = typeof p.folder === "string" && Array.isArray(p.files);
415
+ if (typeof p.url !== "string" && !isFolder) continue;
416
+ if (q && !`${p.id} ${p.name ?? ""} ${p.description ?? ""} ${(p.keywords ?? []).join(" ")}`.toLowerCase().includes(q)) continue;
417
+ const folderUrl = isFolder ? abs(p.folder.endsWith("/") ? p.folder : p.folder + "/") : void 0;
418
+ const location = isFolder ? folderUrl : abs(p.url);
419
+ if (!location) continue;
420
+ out.push({
421
+ id: p.id,
422
+ name: p.name ?? p.id,
423
+ version: p.version,
424
+ description: p.description,
425
+ author: p.author,
426
+ permissions: p.permissions ?? [],
427
+ location,
428
+ source,
429
+ sha256: p.sha256,
430
+ homepage: p.homepage,
431
+ license: p.license,
432
+ iconUrl: abs(p.icon),
433
+ bannerUrl: abs(p.banner),
434
+ screenshots: (p.screenshots ?? []).map((s) => abs(s)).filter((s) => !!s),
435
+ video: p.video ? p.video.startsWith("./") && folderUrl ? abs(new URL(p.video, folderUrl).href) : abs(p.video) : void 0,
436
+ categories: p.categories,
437
+ keywords: p.keywords,
438
+ links: p.links,
439
+ readme: p.readme,
440
+ folder: isFolder || void 0,
441
+ files: isFolder ? p.files : void 0,
442
+ hashes: p.hashes
443
+ });
444
+ }
445
+ return out;
446
+ }
447
+ var SAFE_FILE = /^[\w@+.,() \-]+(\/[\w@+.,() \-]+)*$/;
448
+ async function downloadFolder(entry, dest, get) {
449
+ const base = entry.location.endsWith("/") ? entry.location : entry.location + "/";
450
+ const files = entry.files ?? [];
451
+ if (!files.includes("plugin.json")) throw new Error("catalog entry does not list plugin.json");
452
+ if (files.length > 800) throw new Error("catalog entry lists too many files");
453
+ if (!entry.hashes) throw new Error("catalog entry has no file hashes; refusing to install an unverifiable folder (run `fluxplugin store build`)");
454
+ const root = path5.resolve(dest);
455
+ await fs5.mkdir(root, { recursive: true });
456
+ let total = 0;
457
+ const queue = [...files];
458
+ const worker = async () => {
459
+ for (let f = queue.shift(); f !== void 0; f = queue.shift()) {
460
+ if (!SAFE_FILE.test(f) || f.split("/").some((s) => s === ".." || s === ".")) throw new Error(`unsafe file path in catalog: ${f}`);
461
+ const expected = entry.hashes[f];
462
+ if (!expected || !/^[0-9a-f]{64}$/.test(expected)) throw new Error(`no sha256 for ${f} in the catalog`);
463
+ const res = await get(base + f.split("/").map(encodeURIComponent).join("/"));
464
+ const buf = Buffer.from(await res.arrayBuffer());
465
+ total += buf.length;
466
+ if (total > 100 * 1024 * 1024) throw new Error("plugin is too large");
467
+ if (crypto2.createHash("sha256").update(buf).digest("hex") !== expected) throw new Error(`${f} does not match the catalog checksum`);
468
+ const target = path5.resolve(root, f);
469
+ if (!target.startsWith(root + path5.sep)) throw new Error(`unsafe file path in catalog: ${f}`);
470
+ await fs5.mkdir(path5.dirname(target), { recursive: true });
471
+ await fs5.writeFile(target, buf);
472
+ }
473
+ };
474
+ await Promise.all(Array.from({ length: 6 }, worker));
415
475
  }
416
476
  async function pruneNonRuntime(dir) {
417
477
  const removed = [];
@@ -460,23 +520,39 @@ var LocalRegistry = class {
460
520
  }
461
521
  };
462
522
  var HttpRegistry = class {
463
- constructor(baseUrl, fetchImpl = fetch) {
523
+ constructor(baseUrl, fetchImpl = fetch, headers = {}, direct = false) {
464
524
  this.baseUrl = baseUrl;
465
525
  this.fetchImpl = fetchImpl;
526
+ this.headers = headers;
527
+ this.direct = direct;
466
528
  this.name = `http:${baseUrl}`;
467
529
  }
468
530
  baseUrl;
469
531
  fetchImpl;
532
+ headers;
533
+ direct;
470
534
  name;
535
+ /** Direct link (".json" file, or `direct: true` for an API endpoint) or a folder that contains index.json. */
536
+ get catalogUrl() {
537
+ if (this.direct || new URL(this.baseUrl).pathname.toLowerCase().endsWith(".json")) return this.baseUrl;
538
+ return new URL("index.json", this.baseUrl.endsWith("/") ? this.baseUrl : this.baseUrl + "/").href;
539
+ }
540
+ get(url) {
541
+ return this.fetchImpl(url, { headers: { accept: "application/json, application/octet-stream", ...this.headers } }).then((r) => {
542
+ if (!r.ok) throw new Error(`store responded ${r.status} for ${new URL(url).pathname}`);
543
+ return r;
544
+ });
545
+ }
471
546
  async search(query = "") {
472
- const base = this.baseUrl.endsWith("/") ? this.baseUrl : this.baseUrl + "/";
473
- const res = await this.fetchImpl(new URL("index.json", base));
474
- if (!res.ok) throw new Error(`store responded ${res.status}`);
475
- return entriesFromIndex(await res.json(), base, this.name, query);
547
+ const url = this.catalogUrl;
548
+ return entriesFromIndex(await (await this.get(url)).json(), url, this.name, query);
476
549
  }
477
550
  async fetch(entry, dest) {
478
- const res = await this.fetchImpl(entry.location);
479
- if (!res.ok) throw new Error(`download failed: ${res.status}`);
551
+ if (entry.folder) {
552
+ await downloadFolder(entry, dest, (u) => this.get(u));
553
+ return (await readPluginManifest(dest)).manifest;
554
+ }
555
+ const res = await this.get(entry.location);
480
556
  const buf = Buffer.from(await res.arrayBuffer());
481
557
  if (entry.sha256 && crypto2.createHash("sha256").update(buf).digest("hex") !== entry.sha256.replace(/^sha256-/, "")) {
482
558
  throw new Error("downloaded archive does not match the registry checksum");
@@ -662,6 +738,10 @@ var GitHubRegistry = class {
662
738
  return out.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0) || a.id.localeCompare(b.id));
663
739
  }
664
740
  async fetch(entry, dest) {
741
+ if (entry.folder) {
742
+ await downloadFolder(entry, dest, (u) => this.get(u, "application/octet-stream"));
743
+ return (await readPluginManifest(dest)).manifest;
744
+ }
665
745
  const res = await this.get(entry.location, "application/octet-stream");
666
746
  const len = Number(res.headers.get("content-length") ?? 0);
667
747
  if (len > MAX_PACKAGE_BYTES) throw new Error("package is too large");
@@ -684,7 +764,8 @@ function createRegistries(specs = [DEFAULT_STORE], opts = {}) {
684
764
  case "github":
685
765
  return new GitHubRegistry({ ...s, token, fetchImpl: opts.fetchImpl });
686
766
  case "http":
687
- return new HttpRegistry(s.url, opts.fetchImpl);
767
+ case "json":
768
+ return new HttpRegistry(s.url, opts.fetchImpl, s.headers, s.type === "json");
688
769
  case "local":
689
770
  return new LocalRegistry(path6.resolve(opts.baseDir ?? process.cwd(), s.path));
690
771
  default:
@@ -893,8 +974,11 @@ var MainFramework = class {
893
974
  h(MGMT.logs, ({ id }) => this.logs.query(id ? { source: id } : {}).slice(-200));
894
975
  h(MGMT.diagnostics, ({ id }) => this.manager.diagnostics(id));
895
976
  h(MGMT.details, ({ id }) => this.details(id));
896
- h(MGMT.search, ({ query }) => this.installer.search(query));
897
- h(MGMT.updates, () => this.installer.updates());
977
+ h(MGMT.search, async ({ query }) => {
978
+ const installed = new Map(this.manager.list().map((p) => [p.id, p.version]));
979
+ return (await this.installer.search(query)).map((e) => ({ ...e, installedVersion: installed.get(e.id) }));
980
+ });
981
+ h(MGMT.updates, async () => (await this.installer.updates()).map((e) => ({ ...e, installedVersion: this.manager.has(e.id) ? this.manager.info(e.id).version : void 0 })));
898
982
  h(MGMT.install, async ({ entry }) => this.installAndLoad(entry));
899
983
  h(MGMT.update, async ({ entry }) => this.installAndLoad(entry));
900
984
  h(MGMT.uninstall, async ({ id }) => {
@@ -943,8 +1027,12 @@ var MainFramework = class {
943
1027
  iconUrl: m.icon ? url(id, m.icon) : void 0,
944
1028
  bannerUrl: m.banner ? url(id, m.banner) : void 0,
945
1029
  screenshotUrls: (m.screenshots ?? []).map((s) => url(id, s)),
946
- video: m.video ? { url: m.video, embedUrl: videoEmbedUrl(m.video) } : void 0,
947
- readme
1030
+ video: (() => {
1031
+ const v = parseVideo(m.video);
1032
+ return v?.kind === "html5" && v.src?.startsWith("./") ? { ...v, src: url(id, v.src) } : v;
1033
+ })(),
1034
+ readme,
1035
+ assetBase: url(id, "./")
948
1036
  };
949
1037
  }
950
1038
  requireConfig(id) {
@@ -967,7 +1055,54 @@ var MainFramework = class {
967
1055
  // src/main/adapters.ts
968
1056
  import http from "node:http";
969
1057
  import path8 from "node:path";
1058
+ import { promises as fs7 } from "node:fs";
1059
+
1060
+ // src/main/assets.ts
970
1061
  import { promises as fs6 } from "node:fs";
1062
+ var MIME = {
1063
+ ".js": "text/javascript",
1064
+ ".mjs": "text/javascript",
1065
+ ".css": "text/css",
1066
+ ".json": "application/json",
1067
+ ".map": "application/json",
1068
+ ".svg": "image/svg+xml",
1069
+ ".png": "image/png",
1070
+ ".jpg": "image/jpeg",
1071
+ ".jpeg": "image/jpeg",
1072
+ ".webp": "image/webp",
1073
+ ".gif": "image/gif",
1074
+ ".avif": "image/avif",
1075
+ ".ico": "image/x-icon",
1076
+ ".woff2": "font/woff2",
1077
+ ".mp4": "video/mp4",
1078
+ ".m4v": "video/mp4",
1079
+ ".webm": "video/webm",
1080
+ ".ogv": "video/ogg",
1081
+ ".ogg": "video/ogg",
1082
+ ".mov": "video/quicktime",
1083
+ ".mp3": "audio/mpeg",
1084
+ ".wav": "audio/wav"
1085
+ };
1086
+ async function readAsset(file, mime, range) {
1087
+ const { size } = await fs6.stat(file);
1088
+ const base = { "content-type": mime, "accept-ranges": "bytes", "access-control-allow-origin": "*", "cache-control": "no-store" };
1089
+ const m = range ? /^bytes=(\d*)-(\d*)$/.exec(range.trim()) : null;
1090
+ if (!m || m[1] === "" && m[2] === "") return { status: 200, headers: { ...base, "content-length": String(size) }, body: await fs6.readFile(file) };
1091
+ let start = m[1] === "" ? Math.max(0, size - Number(m[2])) : Number(m[1]);
1092
+ let end = m[1] === "" || m[2] === "" ? size - 1 : Math.min(Number(m[2]), size - 1);
1093
+ if (start > end || start >= size) return { status: 416, headers: { ...base, "content-range": `bytes */${size}` }, body: Buffer.alloc(0) };
1094
+ const fh = await fs6.open(file, "r");
1095
+ try {
1096
+ const len = end - start + 1;
1097
+ const body = Buffer.alloc(len);
1098
+ await fh.read(body, 0, len, start);
1099
+ return { status: 206, headers: { ...base, "content-range": `bytes ${start}-${end}/${size}`, "content-length": String(len) }, body };
1100
+ } finally {
1101
+ await fh.close();
1102
+ }
1103
+ }
1104
+
1105
+ // src/main/adapters.ts
971
1106
  function attachElectron(fw, electron) {
972
1107
  const { ipcMain, webContents } = electron;
973
1108
  ipcMain.handle(CHANNELS.invoke, (e, req) => fw.handleInvoke(req, e.sender.id));
@@ -992,22 +1127,6 @@ var PLUGIN_SCHEME = {
992
1127
  scheme: "fluxplugin",
993
1128
  privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, stream: true }
994
1129
  };
995
- var MIME = {
996
- ".js": "text/javascript",
997
- ".mjs": "text/javascript",
998
- ".css": "text/css",
999
- ".json": "application/json",
1000
- ".svg": "image/svg+xml",
1001
- ".png": "image/png",
1002
- ".jpg": "image/jpeg",
1003
- ".jpeg": "image/jpeg",
1004
- ".webp": "image/webp",
1005
- ".gif": "image/gif",
1006
- ".avif": "image/avif",
1007
- ".ico": "image/x-icon",
1008
- ".woff2": "font/woff2",
1009
- ".map": "application/json"
1010
- };
1011
1130
  async function resolvePluginAsset(fw, url) {
1012
1131
  let u;
1013
1132
  try {
@@ -1028,7 +1147,7 @@ async function resolvePluginAsset(fw, url) {
1028
1147
  const ext = path8.extname(file).toLowerCase();
1029
1148
  if (!(ext in MIME)) return null;
1030
1149
  try {
1031
- await fs6.access(file);
1150
+ await fs7.access(file);
1032
1151
  } catch {
1033
1152
  return null;
1034
1153
  }
@@ -1038,7 +1157,8 @@ function registerPluginProtocol(fw, protocol) {
1038
1157
  protocol.handle(PLUGIN_SCHEME.scheme, async (req) => {
1039
1158
  const r = await resolvePluginAsset(fw, req.url);
1040
1159
  if (!r) return new Response("Not found", { status: 404 });
1041
- return new Response(await fs6.readFile(r.file), { headers: { "content-type": r.mime, "access-control-allow-origin": "*" } });
1160
+ const a = await readAsset(r.file, r.mime, req.headers?.get("range"));
1161
+ return new Response(new Uint8Array(a.body), { status: a.status, headers: a.headers });
1042
1162
  });
1043
1163
  }
1044
1164
  function attachHttp(fw, opts = {}) {
@@ -1091,8 +1211,9 @@ function attachHttp(fw, opts = {}) {
1091
1211
  const id = decodeURIComponent(rest.slice(0, slash));
1092
1212
  const r = await resolvePluginAsset(fw, `fluxplugin://${encodeURIComponent(id)}/${rest.slice(slash + 1)}`);
1093
1213
  if (!r) return json(res, 404, { error: "not found" });
1094
- res.writeHead(200, { "content-type": r.mime, "cache-control": "no-store" });
1095
- return res.end(await fs6.readFile(r.file));
1214
+ const a = await readAsset(r.file, r.mime, req.headers.range);
1215
+ res.writeHead(a.status, a.headers);
1216
+ return res.end(a.body);
1096
1217
  }
1097
1218
  if (opts.fallback && await opts.fallback(req, res)) return;
1098
1219
  json(res, 404, { error: "not found" });
@@ -1142,8 +1263,8 @@ function createLoopbackBridge(fw) {
1142
1263
 
1143
1264
  // src/main/serve-web.ts
1144
1265
  import path9 from "node:path";
1145
- import { promises as fs7 } from "node:fs";
1146
- var TYPES = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".map": "application/json", ".svg": "image/svg+xml", ".png": "image/png" };
1266
+ import { promises as fs8 } from "node:fs";
1267
+ var TYPES = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".map": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".json": "application/json", ".jpg": "image/jpeg", ".webp": "image/webp" };
1147
1268
  function serveWeb(fw, opts) {
1148
1269
  const dist = path9.resolve(opts.distDir);
1149
1270
  return attachHttp(fw, {
@@ -1156,7 +1277,7 @@ function serveWeb(fw, opts) {
1156
1277
  if (!type || !file.startsWith(dist + path9.sep)) return false;
1157
1278
  try {
1158
1279
  res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
1159
- res.end(await fs7.readFile(file));
1280
+ res.end(await fs8.readFile(file));
1160
1281
  return true;
1161
1282
  } catch {
1162
1283
  return false;
@@ -1186,6 +1307,7 @@ export {
1186
1307
  copyPluginRuntime,
1187
1308
  copyDir,
1188
1309
  entriesFromIndex,
1310
+ downloadFolder,
1189
1311
  pruneNonRuntime,
1190
1312
  LocalRegistry,
1191
1313
  HttpRegistry,
@@ -1195,6 +1317,8 @@ export {
1195
1317
  DEFAULT_STORE,
1196
1318
  createRegistries,
1197
1319
  MainFramework,
1320
+ MIME,
1321
+ readAsset,
1198
1322
  attachElectron,
1199
1323
  PLUGIN_SCHEME,
1200
1324
  resolvePluginAsset,