@voilabs/plugins 0.0.1-beta.0 → 0.0.1-beta.2

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/README.md CHANGED
@@ -9,8 +9,9 @@ A framework-agnostic plugin system for Node.js applications. The core package ru
9
9
  - HTTP runtime for `/plugins`, `/plugins/:id/install`, `/plugins/:id/config`, and custom plugin routes.
10
10
  - WordPress-like injection support: plugins can inject `script`, `style`, `html`, `meta`, `link`, and `noscript` into placements such as `head`, `body:end`, `admin:head`, and `public:footer`.
11
11
  - Elysia and Next.js adapters that use structural typing and do not require hard dependencies.
12
- - React integration through a fetch client and hook factory without making React a peer dependency.
13
- - GitHub marketplace support through `<plugin-id>.plugin`, `marketplace.json`, `plugins.json`, and `index.json`.
12
+ - React integration through a fetch client, hook factory, and slot renderer without making React a peer dependency.
13
+ - GitHub marketplace support through plugin folders such as `lumina/schema.json`, plus optional `marketplace.json`, `plugins.json`, and `index.json` manifests.
14
+ - Plugin-contained frontend modules and assets: remote schemas can reference component, script, style, image, and font files living beside `schema.json`.
14
15
 
15
16
  ## Installation
16
17
 
@@ -109,7 +110,7 @@ export const luminaAIPlugin = new Plugin({
109
110
  });
110
111
  ```
111
112
 
112
- When you create a plugin locally with `new Plugin()`, `provider` is required and the exact value you provide is used. For GitHub-loaded `.plugin` files, `provider` and `iconUrl` from the file are ignored.
113
+ When you create a plugin locally with `new Plugin()`, `provider` is required and the exact value you provide is used. For GitHub-loaded `schema.json` files, `provider` and `iconUrl` from the file are ignored.
113
114
 
114
115
  Ready-to-use example: `@voilabs/plugins/examples/lumina`.
115
116
 
@@ -155,7 +156,7 @@ const plugins = new PluginManager({
155
156
  branch: "main",
156
157
  token: process.env.GITHUB_TOKEN,
157
158
  files: ["marketplace.json", "plugins.json"],
158
- pluginFileExtensions: [".plugin"],
159
+ schemaFileNames: ["schema.json"],
159
160
  },
160
161
  {
161
162
  github: "voilabs/plugins-marketplace",
@@ -168,18 +169,19 @@ const plugins = new PluginManager({
168
169
 
169
170
  By default, a GitHub repository is read from two sources:
170
171
 
171
- - `*.plugin` files discovered through the repository tree. File names usually follow `<plugin-id>.plugin`.
172
+ - `schema.json` files discovered through the repository tree. Each plugin normally owns a folder such as `lumina/schema.json`.
172
173
  - `marketplace.json`, `plugins.json`, and `index.json` marketplace manifests.
173
174
 
174
175
  If no branch is configured, `main` and `master` are tried. If you provide a GitHub `blob` URL, that exact JSON file is used.
175
176
 
176
- ## GitHub `.plugin` Files
177
+ ## GitHub Plugin Folders
177
178
 
178
- A `.plugin` file contains one plugin manifest as JSON. For GitHub-loaded plugins:
179
+ A plugin folder contains one `schema.json` manifest plus any files the plugin needs. For GitHub-loaded plugins:
179
180
 
180
181
  - `provider` is always the repository owner.
181
182
  - `iconUrl` is always generated from the repository owner avatar: `https://github.com/<owner>.png?size=128`.
182
- - Any `provider` or `iconUrl` inside the `.plugin` file is ignored.
183
+ - Any `provider` or `iconUrl` inside `schema.json` is ignored.
184
+ - Relative component, asset, script, style, and injection URLs are resolved from the folder containing `schema.json`.
183
185
 
184
186
  ```json
185
187
  {
@@ -195,6 +197,23 @@ A `.plugin` file contains one plugin manifest as JSON. For GitHub-loaded plugins
195
197
  "required": true
196
198
  }
197
199
  ],
200
+ "frontend": {
201
+ "components": [
202
+ {
203
+ "key": "lumina-settings",
204
+ "slot": "settings-panel",
205
+ "label": "Lumina settings",
206
+ "component": {
207
+ "type": "remote",
208
+ "path": "components/settings.js",
209
+ "exportName": "default"
210
+ },
211
+ "props": {
212
+ "dense": true
213
+ }
214
+ }
215
+ ]
216
+ },
198
217
  "routes": [
199
218
  {
200
219
  "id": "create-article",
@@ -221,6 +240,13 @@ A `.plugin` file contains one plugin manifest as JSON. For GitHub-loaded plugins
221
240
  "type": "html",
222
241
  "placement": "body:end",
223
242
  "content": "<div data-lumina-widget></div>"
243
+ },
244
+ {
245
+ "key": "lumina-widget-script",
246
+ "type": "script",
247
+ "placement": "public:footer",
248
+ "src": "assets/widget.js",
249
+ "defer": true
224
250
  }
225
251
  ]
226
252
  }
@@ -230,12 +256,27 @@ Example repository:
230
256
 
231
257
  ```text
232
258
  plugins-marketplace/
233
- voidigital-lumina.plugin
234
- analytics-pixel.plugin
235
- commerce/iyzico.plugin
259
+ lumina/
260
+ schema.json
261
+ components/settings.js
262
+ assets/widget.js
263
+ analytics-pixel/
264
+ schema.json
265
+ assets/pixel.js
266
+ commerce/iyzico/
267
+ schema.json
268
+ components/settings.js
236
269
  ```
237
270
 
238
- If you set `path`, only `.plugin` files under that folder are read.
271
+ If you set `path`, only `schema.json` files under that folder are read. A single-plugin repository can also put `schema.json` at the repository root.
272
+
273
+ ## GitHub Examples
274
+
275
+ See `github-examples/google-tag-manager` for a folder-based marketplace plugin. It includes:
276
+
277
+ - `schema.json` with fields, routes, frontend components, assets, and GTM injections.
278
+ - `components/settings.js` and `components/dashboard-card.js` as browser-ready remote components.
279
+ - `assets/admin.css` as a plugin-owned asset referenced from the schema.
239
280
 
240
281
  GitHub-loaded plugins support the same manifest surface as local plugins: routes, frontend components, injections, meta tags, assets, permissions, webhooks, and more. Since JSON cannot carry JavaScript functions, remote plugin routes use declarative actions:
241
282
 
@@ -395,25 +436,340 @@ await plugins.renderInjections({
395
436
  ## Next.js App Router Adapter
396
437
 
397
438
  ```ts
398
- // app/api/plugins/[...voilabs]/route.ts
439
+ // app/api/plugins/[[...voilabs]]/route.ts
440
+ import { createNextPluginRouteHandlers } from "@voilabs/plugins/adapters/next";
441
+ import { plugins } from "@/server/plugins";
442
+
443
+ export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD } =
444
+ createNextPluginRouteHandlers(plugins, {
445
+ prefix: "/api/plugins",
446
+ });
447
+ ```
448
+
449
+ Use an optional catch-all route (`[[...voilabs]]`) so both `/api/plugins` and `/api/plugins/:pluginId/*` reach the same handler.
450
+
451
+ ## Minimal Next.js App Router Example
452
+
453
+ This is a copy-paste friendly setup for a native Next.js app. It uses the App Router, one API route, one client-side plugin provider, a tiny admin page, and a client injection bridge.
454
+
455
+ ```text
456
+ instrumentation.ts
457
+ src/
458
+ server/plugins.ts
459
+ app/
460
+ api/plugins/[[...voilabs]]/route.ts
461
+ admin/plugins/page.tsx
462
+ admin/plugins/voilabs-plugin-react.tsx
463
+ plugin-injections.tsx
464
+ layout.tsx
465
+ ```
466
+
467
+ ### `src/server/plugins.ts`
468
+
469
+ ```ts
470
+ import "server-only";
471
+ import { MemoryPluginDatabase, PluginManager } from "@voilabs/plugins";
472
+
473
+ const globalForPlugins = globalThis as typeof globalThis & {
474
+ __voilabsPlugins?: PluginManager;
475
+ };
476
+
477
+ const marketplace = process.env.VOILABS_PLUGIN_MARKETPLACE;
478
+ const githubToken = process.env.GITHUB_TOKEN;
479
+
480
+ export const plugins =
481
+ globalForPlugins.__voilabsPlugins ??
482
+ (globalForPlugins.__voilabsPlugins = new PluginManager({
483
+ marketplaces: marketplace ? [marketplace] : [],
484
+ autoSyncMarketplaces: Boolean(marketplace),
485
+ database: new MemoryPluginDatabase(),
486
+ encryption: {
487
+ encrypt: async (value) => value,
488
+ decrypt: async (value) => value,
489
+ },
490
+ github: githubToken ? { token: githubToken } : undefined,
491
+ marketplaceRequestTimeoutMs: 10_000,
492
+ marketplaceRefreshIntervalMs: 5 * 60 * 1000,
493
+ }));
494
+ ```
495
+
496
+ If `VOILABS_PLUGIN_MARKETPLACE` is not set, the API returns an empty plugin list instead of trying to fetch a placeholder repository.
497
+
498
+ For Redis persistence, install a Redis client and assign it to `redis`. Upstash works well in serverless Next.js projects:
499
+
500
+ ```ts
501
+ import { Redis } from "@upstash/redis";
502
+ import { MemoryPluginDatabase, RedisPluginDatabase } from "@voilabs/plugins";
503
+
504
+ const redis =
505
+ process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN
506
+ ? Redis.fromEnv()
507
+ : undefined;
508
+
509
+ const database = redis
510
+ ? new RedisPluginDatabase(redis, { prefix: "my-app:plugins" })
511
+ : new MemoryPluginDatabase();
512
+ ```
513
+
514
+ Then pass `database` into `new PluginManager({ database, ... })`. You can also let the manager create the adapter:
515
+
516
+ ```ts
517
+ new PluginManager({
518
+ redis,
519
+ redisPrefix: "my-app:plugins",
520
+ });
521
+ ```
522
+
523
+ The built-in `RedisPluginDatabase` stores installations and config in Redis. For production secret fields, replace the placeholder encryption provider with your KMS or app encryption layer.
524
+
525
+ ### `instrumentation.ts`
526
+
527
+ ```ts
528
+ export async function register() {
529
+ if (process.env.NEXT_RUNTIME === "nodejs") {
530
+ const { plugins } = await import("./src/server/plugins");
531
+ await plugins.ready();
532
+ }
533
+ }
534
+ ```
535
+
536
+ `instrumentation.ts` warms the marketplace registry when the Next.js server starts, so `/admin/plugins` does not have to perform the first GitHub sync during its initial loading state.
537
+
538
+ ### `app/api/plugins/[[...voilabs]]/route.ts`
539
+
540
+ ```ts
399
541
  import { createNextPluginRouteHandlers } from "@voilabs/plugins/adapters/next";
400
542
  import { plugins } from "@/server/plugins";
401
543
 
544
+ export const runtime = "nodejs";
545
+ export const dynamic = "force-dynamic";
546
+
402
547
  export const { GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD } =
403
548
  createNextPluginRouteHandlers(plugins, {
404
549
  prefix: "/api/plugins",
550
+ exposeErrors: process.env.NODE_ENV !== "production",
405
551
  });
406
552
  ```
407
553
 
554
+ ### `app/admin/plugins/voilabs-plugin-react.tsx`
555
+
556
+ ```tsx
557
+ "use client";
558
+
559
+ import * as React from "react";
560
+ import { createPluginReactIntegration } from "@voilabs/plugins/react";
561
+
562
+ export const {
563
+ PluginProvider,
564
+ PluginSlot,
565
+ usePluginClient,
566
+ usePlugins,
567
+ } = createPluginReactIntegration(React, {
568
+ baseUrl: "/api/plugins",
569
+ requestTimeoutMs: 15_000,
570
+ });
571
+ ```
572
+
573
+ ### `app/admin/plugins/page.tsx`
574
+
575
+ ```tsx
576
+ "use client";
577
+
578
+ import {
579
+ PluginProvider,
580
+ PluginSlot,
581
+ usePluginClient,
582
+ usePlugins,
583
+ } from "./voilabs-plugin-react";
584
+
585
+ function defaultConfig(plugin: { id: string; fields?: Array<{ key: string; defaultValue?: unknown }> }) {
586
+ if (plugin.id === "google-tag-manager") {
587
+ return {
588
+ containerId: "GTM-ABC123",
589
+ dataLayerName: "dataLayer",
590
+ };
591
+ }
592
+
593
+ return Object.fromEntries(
594
+ (plugin.fields ?? []).map((field) => [field.key, field.defaultValue ?? ""]),
595
+ );
596
+ }
597
+
598
+ function PluginAdmin() {
599
+ const client = usePluginClient();
600
+ const { data, loading, error } = usePlugins({ page: 1, limit: 50 });
601
+
602
+ if (loading) return <main>Loading plugins...</main>;
603
+ if (error) {
604
+ return (
605
+ <main style={{ padding: 24 }}>
606
+ <h1>Plugin API error</h1>
607
+ <pre>{error instanceof Error ? error.message : String(error)}</pre>
608
+ </main>
609
+ );
610
+ }
611
+
612
+ return (
613
+ <main style={{ display: "grid", gap: 24, padding: 24 }}>
614
+ <section style={{ display: "grid", gap: 12 }}>
615
+ {data?.data.map((plugin) => (
616
+ <article
617
+ key={plugin.id}
618
+ style={{ border: "1px solid #ddd", borderRadius: 8, padding: 16 }}
619
+ >
620
+ <img src={plugin.iconUrl} alt="" width={32} height={32} />
621
+ <h2>{plugin.name}</h2>
622
+ <p>{plugin.summary}</p>
623
+ <p>
624
+ {plugin.provider} / {plugin.category}
625
+ </p>
626
+ <button
627
+ onClick={() => client.install(plugin.id, defaultConfig(plugin))}
628
+ >
629
+ Install
630
+ </button>
631
+ <button onClick={() => client.enable(plugin.id)}>Enable</button>
632
+ <button onClick={() => client.disable(plugin.id)}>Disable</button>
633
+ </article>
634
+ ))}
635
+ </section>
636
+
637
+ <aside>
638
+ <PluginSlot
639
+ slot="settings-panel"
640
+ installedOnly={false}
641
+ enabledOnly={false}
642
+ />
643
+ </aside>
644
+ </main>
645
+ );
646
+ }
647
+
648
+ export default function PluginsPage() {
649
+ return (
650
+ <PluginProvider>
651
+ <PluginAdmin />
652
+ </PluginProvider>
653
+ );
654
+ }
655
+ ```
656
+
657
+ ### `app/plugin-injections.tsx`
658
+
659
+ ```tsx
660
+ "use client";
661
+
662
+ import { useEffect } from "react";
663
+
664
+ type Placement = "head" | "body:start" | "body:end";
665
+
666
+ export function PluginInjections() {
667
+ useEffect(() => {
668
+ const controller = new AbortController();
669
+
670
+ void inject("head", document.head, "beforeend", controller.signal);
671
+ void inject("body:start", document.body, "afterbegin", controller.signal);
672
+ void inject("body:end", document.body, "beforeend", controller.signal);
673
+
674
+ return () => controller.abort();
675
+ }, []);
676
+
677
+ return null;
678
+ }
679
+
680
+ async function inject(
681
+ placement: Placement,
682
+ target: HTMLElement,
683
+ position: InsertPosition,
684
+ signal: AbortSignal,
685
+ ) {
686
+ const marker = `voilabs:${placement}`;
687
+ if (document.querySelector(`[data-voilabs-injection="${marker}"]`)) {
688
+ return;
689
+ }
690
+
691
+ const response = await fetch(
692
+ `/api/plugins/injections?area=public&placement=${encodeURIComponent(placement)}`,
693
+ { signal },
694
+ );
695
+ const payload = (await response.json()) as { html?: string };
696
+ if (!payload.html) return;
697
+
698
+ const wrapper = document.createElement("div");
699
+ wrapper.dataset.voilabsInjection = marker;
700
+ appendExecutableHtml(wrapper, payload.html);
701
+ target.insertAdjacentElement(position, wrapper);
702
+ }
703
+
704
+ function appendExecutableHtml(target: HTMLElement, html: string) {
705
+ const template = document.createElement("template");
706
+ template.innerHTML = html;
707
+
708
+ for (const node of Array.from(template.content.childNodes)) {
709
+ if (node instanceof HTMLScriptElement) {
710
+ const script = document.createElement("script");
711
+ for (const attribute of Array.from(node.attributes)) {
712
+ script.setAttribute(attribute.name, attribute.value);
713
+ }
714
+ script.text = node.text;
715
+ target.appendChild(script);
716
+ continue;
717
+ }
718
+
719
+ target.appendChild(node);
720
+ }
721
+ }
722
+ ```
723
+
724
+ ### `app/layout.tsx`
725
+
726
+ ```tsx
727
+ import { PluginInjections } from "./plugin-injections";
728
+
729
+ export default function RootLayout({
730
+ children,
731
+ }: {
732
+ children: React.ReactNode;
733
+ }) {
734
+ return (
735
+ <html lang="en">
736
+ <body>
737
+ <PluginInjections />
738
+ {children}
739
+ </body>
740
+ </html>
741
+ );
742
+ }
743
+ ```
744
+
745
+ ### Try It With The GTM Example
746
+
747
+ Push `github-examples/google-tag-manager` to a GitHub repo, then set:
748
+
749
+ ```bash
750
+ VOILABS_PLUGIN_MARKETPLACE=<github-owner>/<plugins-repo>
751
+ ```
752
+
753
+ Open `/admin/plugins`, install `google-tag-manager`, then visit any public page. The app will request `/api/plugins/injections?area=public&placement=head` and inject the GTM script for installed and enabled plugins.
754
+
755
+ ### If The Page Stays Loading
756
+
757
+ Open `/api/plugins` directly in the browser.
758
+
759
+ - If it is pending, the server is waiting on GitHub. Check `VOILABS_PLUGIN_MARKETPLACE`, add `GITHUB_TOKEN` for private or rate-limited repos, or lower `marketplaceRequestTimeoutMs`.
760
+ - If it returns 404, make sure the route folder is `app/api/plugins/[[...voilabs]]/route.ts`, not `[...voilabs]`.
761
+ - If it returns JSON with `data: []`, the Next.js wiring is working and the marketplace repo did not provide discoverable `schema.json` files.
762
+ - If `/admin/plugins` still says loading after `/api/plugins` returns JSON, confirm `PluginProvider` uses `baseUrl: "/api/plugins"` and the page is a client component with `"use client"`.
763
+
408
764
  ## React Integration
409
765
 
410
- React is not imported by the package. Pass your app's React object into the integration factory.
766
+ React is not imported by the package. Pass your app's React object into the integration factory. The host app does not need to register every marketplace component manually; it can render plugin slots and let the plugin schema load browser-ready component modules from the plugin folder.
411
767
 
412
768
  ```tsx
413
769
  import * as React from "react";
414
770
  import { createPluginReactIntegration } from "@voilabs/plugins/react";
415
771
 
416
- const { PluginProvider, usePlugins } = createPluginReactIntegration(React, {
772
+ const { PluginProvider, PluginSlot, usePlugins } = createPluginReactIntegration(React, {
417
773
  baseUrl: "/api/plugins",
418
774
  });
419
775
 
@@ -436,11 +792,45 @@ export function App() {
436
792
  return (
437
793
  <PluginProvider>
438
794
  <PluginList />
795
+ <PluginSlot slot="settings-panel" />
439
796
  </PluginProvider>
440
797
  );
441
798
  }
442
799
  ```
443
800
 
801
+ Remote component modules are loaded from the plugin asset endpoint when a schema uses `component.path`:
802
+
803
+ ```json
804
+ {
805
+ "frontend": {
806
+ "components": [
807
+ {
808
+ "key": "analytics-card",
809
+ "slot": "dashboard-card",
810
+ "component": {
811
+ "type": "remote",
812
+ "path": "components/dashboard-card.js"
813
+ }
814
+ }
815
+ ]
816
+ }
817
+ }
818
+ ```
819
+
820
+ The component file must be browser-ready ESM:
821
+
822
+ ```js
823
+ export default function DashboardCard(props) {
824
+ return props.React.createElement(
825
+ "section",
826
+ null,
827
+ `Plugin: ${props.plugin.name}`,
828
+ );
829
+ }
830
+ ```
831
+
832
+ If a project uses a custom module federation/runtime loader, pass `loadRemoteModule` to `createPluginReactIntegration` or `PluginProvider`.
833
+
444
834
  ## Marketplace JSON Format
445
835
 
446
836
  Marketplace JSON can be either an array or an object:
@@ -460,7 +850,7 @@ Marketplace JSON can be either an array or an object:
460
850
  }
461
851
  ```
462
852
 
463
- When a GitHub repository URL is provided, `<plugin-id>.plugin` files are discovered first. `marketplace.json`, `plugins.json`, and `index.json` are also tried.
853
+ When a GitHub repository URL is provided, folder-based `schema.json` files are discovered first. `marketplace.json`, `plugins.json`, and `index.json` are also tried for curated indexes or non-GitHub sources.
464
854
 
465
855
  ## Integration Prompts
466
856
 
@@ -473,11 +863,11 @@ Integrate @voilabs/plugins into this project using Next.js for the frontend/admi
473
863
 
474
864
  Goals:
475
865
  - Use GitHub marketplace repositories as the plugin source.
476
- - Automatically discover <plugin-id>.plugin files from the configured GitHub repo.
866
+ - Automatically discover plugin folders with schema.json files from the configured GitHub repo.
477
867
  - Treat the GitHub repo owner as the provider for remote plugins.
478
868
  - Use the GitHub owner avatar as the plugin icon.
479
- - Ignore provider and iconUrl values inside remote .plugin files.
480
- - Support plugin fields, install/update/enable/disable/uninstall, declarative routes, proxy routes, redirects, frontend component manifests, and HTML/script/style injections.
869
+ - Ignore provider and iconUrl values inside remote schema.json files.
870
+ - Support plugin fields, install/update/enable/disable/uninstall, declarative routes, proxy routes, redirects, plugin-contained frontend modules/assets, and HTML/script/style injections.
481
871
 
482
872
  Backend/Elysia work:
483
873
  1. Create a singleton plugin manager, for example in src/server/plugins.ts:
@@ -508,9 +898,10 @@ Next.js/React work:
508
898
  - generate config forms from plugin.fields
509
899
  - support secret fields without echoing plain secret values back into the UI
510
900
  - install, update config, enable, disable, and uninstall plugins
511
- 4. Render frontend component manifests through a local component registry:
512
- - support component.type === "registry"
513
- - ignore unknown registry components safely
901
+ 4. Render frontend component manifests through plugin slots:
902
+ - use PluginSlot from @voilabs/plugins/react for slots such as settings-panel, dashboard-card, sidebar-item
903
+ - support component.type === "remote" with component.path resolved from the plugin folder
904
+ - avoid host-app component registration unless the project intentionally uses local registry components
514
905
  5. Add injection support:
515
906
  - head injections: render via the API or server helper and place into the document head
516
907
  - body/footer injections: provide slots in the app layout
@@ -518,7 +909,7 @@ Next.js/React work:
518
909
 
519
910
  Verification:
520
911
  - Run TypeScript checks and the project build.
521
- - Verify GitHub .plugin discovery with at least one plugin that does not define provider or iconUrl.
912
+ - Verify GitHub schema.json discovery with at least one plugin that does not define provider or iconUrl.
522
913
  - Confirm the UI shows provider as the GitHub owner and icon as the GitHub avatar.
523
914
  - Confirm install + enable works.
524
915
  - Confirm a declarative route using response/proxy/redirect works.
@@ -533,10 +924,10 @@ Integrate @voilabs/plugins into this native Next.js project.
533
924
  Goals:
534
925
  - Use Next.js App Router API routes for the plugin runtime.
535
926
  - Use React integration for the admin/plugin UI.
536
- - Load remote plugins from GitHub marketplace repositories and <plugin-id>.plugin files.
927
+ - Load remote plugins from GitHub marketplace repositories and plugin folders with schema.json files.
537
928
  - Remote plugin provider must always be the GitHub repo owner.
538
929
  - Remote plugin iconUrl must always be the GitHub owner avatar.
539
- - Ignore provider and iconUrl fields inside GitHub-loaded plugin files.
930
+ - Ignore provider and iconUrl fields inside GitHub-loaded schema.json files.
540
931
 
541
932
  Server work:
542
933
  1. Create src/server/plugins.ts:
@@ -544,7 +935,7 @@ Server work:
544
935
  - configure marketplaces: ["<github-owner>/<plugins-repo>"]
545
936
  - use MemoryPluginDatabase as a temporary adapter if no DB adapter exists
546
937
  - add encryption provider hooks for secret fields
547
- 2. Create app/api/plugins/[...voilabs]/route.ts:
938
+ 2. Create app/api/plugins/[[...voilabs]]/route.ts:
548
939
  - import createNextPluginRouteHandlers from @voilabs/plugins/adapters/next
549
940
  - export GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD from createNextPluginRouteHandlers(plugins, { prefix: "/api/plugins" })
550
941
  3. Ensure the following routes work:
@@ -569,8 +960,9 @@ React/admin work:
569
960
  - handle secret fields carefully and do not display stored secrets as plain text
570
961
  - wire install, updateConfig, enable, disable, uninstall
571
962
  4. Add support for plugin frontend manifests:
572
- - resolve frontend.components with a local registry map
573
- - render known registry components in their slots
963
+ - use PluginSlot from @voilabs/plugins/react
964
+ - render remote components declared by frontend.components without adding per-plugin host code
965
+ - only use a local registry map for explicitly local components
574
966
  5. Add injection support:
575
967
  - for server layouts, call plugins.renderInjections({ area: "public", placement: "head", nonce })
576
968
  - create safe body/footer slots for public and admin injections
@@ -578,7 +970,7 @@ React/admin work:
578
970
 
579
971
  Verification:
580
972
  - Run lint/typecheck/build.
581
- - Test a GitHub .plugin file without provider/iconUrl.
973
+ - Test a GitHub schema.json file without provider/iconUrl.
582
974
  - Confirm provider is GitHub owner and iconUrl is https://github.com/<owner>.png?size=128.
583
975
  - Confirm declarative response, proxy, and redirect routes work.
584
976
  - Confirm plugin injections render in the expected placements.
@@ -591,10 +983,10 @@ Integrate @voilabs/plugins into this native Elysia.js project.
591
983
 
592
984
  Goals:
593
985
  - Use Elysia as the plugin API runtime.
594
- - Load plugins from GitHub marketplace repositories and <plugin-id>.plugin files.
986
+ - Load plugins from GitHub marketplace repositories and plugin folders with schema.json files.
595
987
  - GitHub-loaded plugin provider must be the GitHub repo owner.
596
988
  - GitHub-loaded plugin iconUrl must be the GitHub owner avatar.
597
- - Ignore provider and iconUrl fields inside remote .plugin files.
989
+ - Ignore provider and iconUrl fields inside remote schema.json files.
598
990
  - Support install/config/enable/disable/uninstall, declarative routes, proxy routes, redirects, and injection rendering endpoints.
599
991
 
600
992
  Implementation:
@@ -626,7 +1018,7 @@ Implementation:
626
1018
 
627
1019
  Verification:
628
1020
  - Run typecheck/build/tests.
629
- - Mock or use a real GitHub marketplace with at least one <plugin-id>.plugin file.
1021
+ - Mock or use a real GitHub marketplace with at least one plugin-folder/schema.json file.
630
1022
  - Confirm provider and iconUrl are derived from the GitHub repo owner.
631
1023
  - Install and enable the plugin.
632
1024
  - Confirm declarative response/proxy/redirect routes work.
package/dist/client.d.ts CHANGED
@@ -1,16 +1,27 @@
1
- import type { PaginatedResult, PluginConfig, PluginInstallOptions, PluginListItem, PluginListOptions } from "./types.js";
1
+ import type { PaginatedResult, PluginConfig, PluginInstallOptions, PluginListItem, PluginListOptions, ResolvedPluginFrontendComponent } from "./types.js";
2
2
  export interface PluginApiClientOptions {
3
3
  baseUrl?: string;
4
4
  fetcher?: typeof fetch;
5
5
  headers?: HeadersInit | (() => HeadersInit);
6
+ requestTimeoutMs?: number;
7
+ }
8
+ export interface PluginComponentListOptions {
9
+ pluginId?: string;
10
+ slot?: string;
11
+ tenantId?: string;
12
+ installedOnly?: boolean;
13
+ enabledOnly?: boolean;
6
14
  }
7
15
  export declare class PluginApiClient {
8
16
  private readonly baseUrl;
9
17
  private readonly fetcher;
10
18
  private readonly headers?;
19
+ private readonly requestTimeoutMs;
11
20
  constructor(options?: PluginApiClientOptions);
12
21
  list(options?: PluginListOptions): Promise<PaginatedResult<PluginListItem>>;
13
22
  get(pluginId: string): Promise<PluginListItem>;
23
+ components(options?: PluginComponentListOptions): Promise<ResolvedPluginFrontendComponent[]>;
24
+ assetUrl(pluginId: string, path: string): string;
14
25
  install<TConfig extends PluginConfig = PluginConfig>(pluginId: string, config: Partial<TConfig>, options?: PluginInstallOptions): Promise<unknown>;
15
26
  updateConfig<TConfig extends PluginConfig = PluginConfig>(pluginId: string, config: Partial<TConfig>, options?: {
16
27
  tenantId?: string;
@@ -25,6 +36,10 @@ export declare class PluginApiClient {
25
36
  }): Promise<TResponse>;
26
37
  private request;
27
38
  }
39
+ export declare class PluginApiClientTimeoutError extends Error {
40
+ readonly timeoutMs: number;
41
+ constructor(timeoutMs: number);
42
+ }
28
43
  export declare class PluginApiClientError extends Error {
29
44
  readonly status: number;
30
45
  readonly payload: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;CAC7C;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAoC;gBAEjD,OAAO,GAAE,sBAA2B;IAUhD,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;IAe/E,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAI9C,OAAO,CAAC,OAAO,SAAS,YAAY,GAAG,YAAY,EACjD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,EACxB,OAAO,GAAE,oBAAyB;IAapC,YAAY,CAAC,OAAO,SAAS,YAAY,GAAG,YAAY,EACtD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,EACxB,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAO;IAatF,MAAM,CAAC,QAAQ,EAAE,MAAM;IAMvB,OAAO,CAAC,QAAQ,EAAE,MAAM;IAMxB,SAAS,CAAC,QAAQ,EAAE,MAAM;IAM1B,IAAI,CAAC,SAAS,GAAG,OAAO,EACtB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAO,GACxD,OAAO,CAAC,SAAS,CAAC;YAOP,OAAO;CAkCtB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;gBAEd,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;CAM7C"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,OAAO,GAAE,sBAA2B;IAWhD,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;IAe/E,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAI9C,UAAU,CACR,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,+BAA+B,EAAE,CAAC;IAe7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAOhD,OAAO,CAAC,OAAO,SAAS,YAAY,GAAG,YAAY,EACjD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,EACxB,OAAO,GAAE,oBAAyB;IAapC,YAAY,CAAC,OAAO,SAAS,YAAY,GAAG,YAAY,EACtD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,EACxB,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAO;IAatF,MAAM,CAAC,QAAQ,EAAE,MAAM;IAMvB,OAAO,CAAC,QAAQ,EAAE,MAAM;IAMxB,SAAS,CAAC,QAAQ,EAAE,MAAM;IAM1B,IAAI,CAAC,SAAS,GAAG,OAAO,EACtB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAO,GACxD,OAAO,CAAC,SAAS,CAAC;YAOP,OAAO;CA8CtB;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,SAAS,EAAE,MAAM;CAK9B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;gBAEd,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;CAM7C"}