@framework-m/plugin-sdk 0.2.3

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,YAAY,EACV,QAAQ,EACR,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,gBAAgB,EAChB,MAAM,EACN,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,EACxB,gCAAgC,GACjC,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAGnF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,594 @@
1
+ var P = Object.defineProperty;
2
+ var C = (r, e, t) => e in r ? P(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
3
+ var c = (r, e, t) => C(r, typeof e != "symbol" ? e + "" : e, t);
4
+ import { jsx as S } from "react/jsx-runtime";
5
+ import { createContext as O, useState as l, useEffect as d, useContext as f, useMemo as p } from "react";
6
+ class N {
7
+ constructor() {
8
+ c(this, "factories", /* @__PURE__ */ new Map());
9
+ c(this, "instances", /* @__PURE__ */ new Map());
10
+ }
11
+ /**
12
+ * Register a service factory.
13
+ *
14
+ * @param name — unique service name
15
+ * @param factory — factory function (sync or async)
16
+ */
17
+ register(e, t) {
18
+ this.factories.set(e, t), this.instances.delete(e);
19
+ }
20
+ /**
21
+ * Get a service by name. Lazily instantiates on first call (singleton).
22
+ *
23
+ * @throws Error if service is not registered
24
+ */
25
+ async get(e) {
26
+ if (this.instances.has(e))
27
+ return this.instances.get(e);
28
+ const t = this.factories.get(e);
29
+ if (!t)
30
+ throw new Error(`Service "${e}" is not registered`);
31
+ const i = await Promise.resolve(t());
32
+ return this.instances.set(e, i), i;
33
+ }
34
+ /**
35
+ * Check if a service is registered.
36
+ */
37
+ has(e) {
38
+ return this.factories.has(e);
39
+ }
40
+ /**
41
+ * Get all registered service names.
42
+ */
43
+ getAll() {
44
+ return Array.from(this.factories.keys());
45
+ }
46
+ /**
47
+ * Clear all registered factories and cached instances.
48
+ */
49
+ clear() {
50
+ this.factories.clear(), this.instances.clear();
51
+ }
52
+ }
53
+ const v = "0.1.0", w = {
54
+ Sales: "shopping-cart",
55
+ Inventory: "package",
56
+ HR: "users",
57
+ Finance: "dollar-sign",
58
+ Core: "settings",
59
+ Other: "folder"
60
+ };
61
+ function M() {
62
+ const r = globalThis.__FRAMEWORK_M_PLUGIN_DEBUG__;
63
+ if (typeof r == "boolean")
64
+ return r;
65
+ if (typeof r == "string")
66
+ return ["1", "true", "yes", "on"].includes(r.toLowerCase());
67
+ const e = globalThis.process;
68
+ if (e != null && e.env) {
69
+ const t = e.env.FRAMEWORK_M_PLUGIN_DEBUG;
70
+ if (typeof t == "string")
71
+ return ["1", "true", "yes", "on"].includes(t.toLowerCase());
72
+ }
73
+ return !1;
74
+ }
75
+ const I = {
76
+ debug(r, ...e) {
77
+ M() && console.debug(`[PluginRegistry] ${r}`, ...e);
78
+ }
79
+ };
80
+ class R {
81
+ constructor() {
82
+ c(this, "plugins", /* @__PURE__ */ new Map());
83
+ c(this, "menuCache", null);
84
+ c(this, "serviceContainer", new N());
85
+ c(this, "routeOwners", /* @__PURE__ */ new Map());
86
+ c(this, "menuNameOwners", /* @__PURE__ */ new Map());
87
+ c(this, "menuRouteOwners", /* @__PURE__ */ new Map());
88
+ c(this, "serviceOwners", /* @__PURE__ */ new Map());
89
+ c(this, "diagnostics", []);
90
+ c(this, "permissionChecker", null);
91
+ c(this, "eventListeners", {
92
+ "plugin:registered": /* @__PURE__ */ new Set(),
93
+ "plugin:error": /* @__PURE__ */ new Set()
94
+ });
95
+ }
96
+ /**
97
+ * Register a plugin with the registry.
98
+ *
99
+ * validates that the plugin has name and version, checks collisions,
100
+ * stores it, registers any services into the ServiceContainer,
101
+ * and invalidates the menu cache.
102
+ *
103
+ * @throws Error if plugin has no name or version
104
+ */
105
+ async register(e) {
106
+ if (!e.name || !e.version)
107
+ throw new Error("Plugin must have name and version");
108
+ const t = this.validateRegistration(e), i = t.filter((o) => o.severity === "error");
109
+ if (i.length > 0) {
110
+ const o = new Error(
111
+ `Plugin "${e.name}" registration failed: ${i.map((a) => a.message).join("; ")}`
112
+ );
113
+ throw this.emit("plugin:error", { plugin: e, error: o }), o;
114
+ }
115
+ const s = this.plugins.get(e.name);
116
+ this.plugins.set(e.name, e), this.rebuildIndexesAndServices(), this.menuCache = null;
117
+ try {
118
+ e.onInit && await Promise.resolve(e.onInit());
119
+ } catch (o) {
120
+ throw s ? this.plugins.set(e.name, s) : this.plugins.delete(e.name), this.rebuildIndexesAndServices(), this.menuCache = null, this.emit("plugin:error", { plugin: e, error: o }), o;
121
+ }
122
+ s != null && s.onDestroy && s !== e && await Promise.resolve(s.onDestroy()), this.emit("plugin:registered", { plugin: e });
123
+ const n = t.filter((o) => o.severity === "warning");
124
+ n.length > 0 && I.debug(
125
+ `Registered plugin "${e.name}" with warnings`,
126
+ n
127
+ );
128
+ }
129
+ /**
130
+ * Get merged menu tree from all plugins.
131
+ *
132
+ * Collects all MenuItem entries, groups them by `module` (default: "Other"),
133
+ * optionally sub-groups by `category`, and sorts by `order`.
134
+ * Result is cached until a new plugin is registered.
135
+ */
136
+ getMenu() {
137
+ if (this.menuCache) return this.menuCache;
138
+ const e = [];
139
+ for (const t of this.plugins.values())
140
+ t.menu && e.push(...t.menu);
141
+ return e.length === 0 ? (this.menuCache = [], this.menuCache) : (this.menuCache = this.mergeMenus(e), this.menuCache);
142
+ }
143
+ /**
144
+ * Get aggregated routes from all plugins.
145
+ */
146
+ getRoutes() {
147
+ const e = [];
148
+ for (const t of this.plugins.values())
149
+ t.routes && e.push(...t.routes);
150
+ return e;
151
+ }
152
+ /**
153
+ * Get a specific plugin by name.
154
+ */
155
+ getPlugin(e) {
156
+ return this.plugins.get(e);
157
+ }
158
+ /**
159
+ * Get all registered plugins.
160
+ */
161
+ getAllPlugins() {
162
+ return Array.from(this.plugins.values());
163
+ }
164
+ /**
165
+ * Get the service container for DI.
166
+ */
167
+ getServiceContainer() {
168
+ return this.serviceContainer;
169
+ }
170
+ /**
171
+ * Unregister a plugin by name and run cleanup lifecycle if provided.
172
+ */
173
+ async unregister(e) {
174
+ const t = this.plugins.get(e);
175
+ return t ? (this.plugins.delete(e), this.rebuildIndexesAndServices(), this.menuCache = null, t.onDestroy && await Promise.resolve(t.onDestroy()), !0) : !1;
176
+ }
177
+ /**
178
+ * Resolve a service instance from the registry-level DI container.
179
+ */
180
+ async getService(e) {
181
+ return this.serviceContainer.get(e);
182
+ }
183
+ /**
184
+ * Aggregate all widgets contributed by registered plugins.
185
+ */
186
+ getWidgets() {
187
+ const e = [];
188
+ for (const t of this.plugins.values())
189
+ t.widgets && e.push(...t.widgets);
190
+ return e;
191
+ }
192
+ /**
193
+ * Configure a permission checker used by consumer hooks.
194
+ */
195
+ setPermissionChecker(e) {
196
+ this.permissionChecker = e;
197
+ }
198
+ /**
199
+ * Get the configured permission checker.
200
+ */
201
+ getPermissionChecker() {
202
+ return this.permissionChecker;
203
+ }
204
+ /**
205
+ * Subscribe to plugin lifecycle events.
206
+ */
207
+ on(e, t) {
208
+ return this.eventListeners[e].add(
209
+ t
210
+ ), () => {
211
+ this.eventListeners[e].delete(
212
+ t
213
+ );
214
+ };
215
+ }
216
+ /**
217
+ * Get emitted diagnostics.
218
+ */
219
+ getDiagnostics(e) {
220
+ return e ? this.diagnostics.filter((t) => t.severity === e) : [...this.diagnostics];
221
+ }
222
+ /**
223
+ * Clear diagnostics.
224
+ */
225
+ clearDiagnostics() {
226
+ this.diagnostics = [];
227
+ }
228
+ // -------------------------------------------------------------------------
229
+ // Version Compatibility
230
+ // -------------------------------------------------------------------------
231
+ /**
232
+ * Check compatibility of all registered plugins.
233
+ *
234
+ * Returns a report of all plugins with their compatibility status:
235
+ * - SDK version compatibility
236
+ * - Peer plugin dependency satisfaction
237
+ */
238
+ checkCompatibility() {
239
+ const e = [];
240
+ for (const t of this.plugins.values()) {
241
+ const i = {
242
+ name: t.name,
243
+ version: t.version,
244
+ sdkCompatible: !0,
245
+ missingPeerPlugins: []
246
+ };
247
+ if (t.minSdkVersion && (i.sdkCompatible = this.checkSemverCompat(
248
+ v,
249
+ t.minSdkVersion
250
+ )), t.peerPlugins)
251
+ for (const s of t.peerPlugins)
252
+ this.plugins.has(s) || i.missingPeerPlugins.push(s);
253
+ e.push(i);
254
+ }
255
+ return e;
256
+ }
257
+ // -------------------------------------------------------------------------
258
+ // Private helpers
259
+ // -------------------------------------------------------------------------
260
+ /**
261
+ * Simple semver compatibility check.
262
+ * Checks if `current` satisfies `>=required`.
263
+ */
264
+ checkSemverCompat(e, t) {
265
+ const i = t.replace(/^>=?/, ""), [s, n = 0, o = 0] = e.split(".").map(Number), [a, u = 0, m = 0] = i.split(".").map(Number);
266
+ return s !== a ? s > a : n !== u ? n > u : o >= m;
267
+ }
268
+ validateRegistration(e) {
269
+ return [
270
+ ...this.validateSdkCompatibility(e),
271
+ ...this.validateDuplicatePlugin(e),
272
+ ...this.validateRouteCollisions(e),
273
+ ...this.validateMenuCollisions(e),
274
+ ...this.validateServiceCollisions(e)
275
+ ];
276
+ }
277
+ validateSdkCompatibility(e) {
278
+ const t = [];
279
+ return e.minSdkVersion && (this.checkSemverCompat(
280
+ v,
281
+ e.minSdkVersion
282
+ ) || t.push(
283
+ this.addDiagnostic({
284
+ code: "SDK_INCOMPATIBLE",
285
+ severity: "warning",
286
+ message: `Plugin ${e.name} requires SDK >=${e.minSdkVersion}, current SDK is ${v}. Plugin may not work correctly.`,
287
+ pluginName: e.name,
288
+ key: e.minSdkVersion
289
+ })
290
+ )), t;
291
+ }
292
+ validateDuplicatePlugin(e) {
293
+ const t = [];
294
+ return this.plugins.has(e.name) && t.push(
295
+ this.addDiagnostic({
296
+ code: "PLUGIN_DUPLICATE",
297
+ severity: "warning",
298
+ message: `Plugin ${e.name} is already registered and will be replaced`,
299
+ pluginName: e.name,
300
+ conflictingPluginName: e.name,
301
+ key: e.name
302
+ })
303
+ ), t;
304
+ }
305
+ validateRouteCollisions(e) {
306
+ const t = [], i = /* @__PURE__ */ new Set();
307
+ for (const s of e.routes ?? []) {
308
+ if (i.has(s.path)) {
309
+ t.push(
310
+ this.addDiagnostic({
311
+ code: "ROUTE_COLLISION",
312
+ severity: "error",
313
+ message: `Duplicate route path "${s.path}" inside plugin ${e.name}`,
314
+ pluginName: e.name,
315
+ conflictingPluginName: e.name,
316
+ key: s.path
317
+ })
318
+ );
319
+ continue;
320
+ }
321
+ i.add(s.path);
322
+ const n = this.routeOwners.get(s.path);
323
+ n && n !== e.name && t.push(
324
+ this.addDiagnostic({
325
+ code: "ROUTE_COLLISION",
326
+ severity: "error",
327
+ message: `Route path "${s.path}" from plugin ${e.name} conflicts with plugin ${n}`,
328
+ pluginName: e.name,
329
+ conflictingPluginName: n,
330
+ key: s.path
331
+ })
332
+ );
333
+ }
334
+ return t;
335
+ }
336
+ validateMenuCollisions(e) {
337
+ const t = [], i = /* @__PURE__ */ new Set(), s = /* @__PURE__ */ new Set();
338
+ for (const n of e.menu ?? []) {
339
+ if (i.has(n.name))
340
+ t.push(
341
+ this.addDiagnostic({
342
+ code: "MENU_NAME_COLLISION",
343
+ severity: "error",
344
+ message: `Duplicate menu name "${n.name}" inside plugin ${e.name}`,
345
+ pluginName: e.name,
346
+ conflictingPluginName: e.name,
347
+ key: n.name
348
+ })
349
+ );
350
+ else {
351
+ i.add(n.name);
352
+ const o = this.menuNameOwners.get(n.name);
353
+ o && o !== e.name && t.push(
354
+ this.addDiagnostic({
355
+ code: "MENU_NAME_COLLISION",
356
+ severity: "error",
357
+ message: `Menu name "${n.name}" from plugin ${e.name} conflicts with plugin ${o}`,
358
+ pluginName: e.name,
359
+ conflictingPluginName: o,
360
+ key: n.name
361
+ })
362
+ );
363
+ }
364
+ if (s.has(n.route))
365
+ t.push(
366
+ this.addDiagnostic({
367
+ code: "MENU_ROUTE_COLLISION",
368
+ severity: "error",
369
+ message: `Duplicate menu route "${n.route}" inside plugin ${e.name}`,
370
+ pluginName: e.name,
371
+ conflictingPluginName: e.name,
372
+ key: n.route
373
+ })
374
+ );
375
+ else {
376
+ s.add(n.route);
377
+ const o = this.menuRouteOwners.get(n.route);
378
+ o && o !== e.name && t.push(
379
+ this.addDiagnostic({
380
+ code: "MENU_ROUTE_COLLISION",
381
+ severity: "error",
382
+ message: `Menu route "${n.route}" from plugin ${e.name} conflicts with plugin ${o}`,
383
+ pluginName: e.name,
384
+ conflictingPluginName: o,
385
+ key: n.route
386
+ })
387
+ );
388
+ }
389
+ }
390
+ return t;
391
+ }
392
+ validateServiceCollisions(e) {
393
+ const t = [], i = /* @__PURE__ */ new Set();
394
+ for (const s of Object.keys(e.services ?? {})) {
395
+ if (i.has(s)) {
396
+ t.push(
397
+ this.addDiagnostic({
398
+ code: "SERVICE_COLLISION",
399
+ severity: "error",
400
+ message: `Duplicate service "${s}" inside plugin ${e.name}`,
401
+ pluginName: e.name,
402
+ conflictingPluginName: e.name,
403
+ key: s
404
+ })
405
+ );
406
+ continue;
407
+ }
408
+ i.add(s);
409
+ const n = this.serviceOwners.get(s);
410
+ n && n !== e.name && t.push(
411
+ this.addDiagnostic({
412
+ code: "SERVICE_COLLISION",
413
+ severity: "error",
414
+ message: `Service "${s}" from plugin ${e.name} conflicts with plugin ${n}`,
415
+ pluginName: e.name,
416
+ conflictingPluginName: n,
417
+ key: s
418
+ })
419
+ );
420
+ }
421
+ return t;
422
+ }
423
+ addDiagnostic(e) {
424
+ const t = {
425
+ ...e,
426
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
427
+ };
428
+ return this.diagnostics.push(t), t;
429
+ }
430
+ /**
431
+ * Merge flat menu items into a grouped tree:
432
+ * Module → (optional) Category → Items
433
+ *
434
+ * Items without a `module` are placed under "Other".
435
+ */
436
+ mergeMenus(e) {
437
+ var i;
438
+ const t = /* @__PURE__ */ new Map();
439
+ for (const s of e) {
440
+ const n = s.module || "Other";
441
+ t.has(n) || t.set(n, {
442
+ name: n.toLowerCase(),
443
+ label: n,
444
+ route: `/${n.toLowerCase()}`,
445
+ icon: this.getModuleIcon(n),
446
+ order: s.order,
447
+ children: []
448
+ });
449
+ const o = t.get(n);
450
+ if (s.order !== void 0 && (o.order === void 0 || s.order < o.order) && (o.order = s.order), s.category) {
451
+ let a = (i = o.children) == null ? void 0 : i.find(
452
+ (u) => u.label === s.category
453
+ );
454
+ a || (a = {
455
+ name: s.category.toLowerCase(),
456
+ label: s.category,
457
+ route: `/${n.toLowerCase()}/${s.category.toLowerCase()}`,
458
+ children: []
459
+ }, o.children.push(a)), a.children.push(s);
460
+ } else
461
+ o.children.push(s);
462
+ }
463
+ return Array.from(t.values()).sort(
464
+ (s, n) => (s.order ?? 999) - (n.order ?? 999)
465
+ );
466
+ }
467
+ getModuleIcon(e) {
468
+ return w[e] || w.Other;
469
+ }
470
+ rebuildIndexesAndServices() {
471
+ this.routeOwners.clear(), this.menuNameOwners.clear(), this.menuRouteOwners.clear(), this.serviceOwners.clear(), this.serviceContainer.clear();
472
+ for (const e of this.plugins.values())
473
+ this.registerPluginServices(e), this.registerPluginRoutes(e), this.registerPluginMenuItems(e);
474
+ }
475
+ registerPluginServices(e) {
476
+ for (const [t, i] of Object.entries(e.services ?? {}))
477
+ this.serviceContainer.register(t, i), this.serviceOwners.set(t, e.name);
478
+ }
479
+ registerPluginRoutes(e) {
480
+ for (const t of e.routes ?? [])
481
+ this.routeOwners.set(t.path, e.name);
482
+ }
483
+ registerPluginMenuItems(e) {
484
+ for (const t of e.menu ?? [])
485
+ this.menuNameOwners.set(t.name, e.name), this.menuRouteOwners.set(t.route, e.name);
486
+ }
487
+ emit(e, t) {
488
+ for (const i of this.eventListeners[e])
489
+ i(t);
490
+ }
491
+ }
492
+ const h = O(null);
493
+ function b({
494
+ children: r,
495
+ registry: e
496
+ }) {
497
+ const [t] = l(
498
+ () => e ?? new R()
499
+ ), [i, s] = l(!1);
500
+ return d(() => {
501
+ s(!0);
502
+ }, []), i ? /* @__PURE__ */ S(h.Provider, { value: t, children: r }) : null;
503
+ }
504
+ async function y(r, e) {
505
+ const t = [];
506
+ for (const i of r) {
507
+ if (i.hidden || i.permissions && i.permissions.length > 0 && !await Promise.resolve(e(i.permissions)))
508
+ continue;
509
+ const s = i.children ? await y(i.children, e) : void 0;
510
+ i.children && ((s == null ? void 0 : s.length) ?? 0) === 0 || t.push({ ...i, children: s });
511
+ }
512
+ return t;
513
+ }
514
+ function $() {
515
+ const r = f(h), [e, t] = l([]);
516
+ if (!r)
517
+ throw new Error("usePluginMenu must be used within PluginRegistryProvider");
518
+ return d(() => {
519
+ let i = !1;
520
+ return (async () => {
521
+ const n = r.getMenu(), o = r.getPermissionChecker();
522
+ if (!o) {
523
+ i || t(n.filter((u) => !u.hidden));
524
+ return;
525
+ }
526
+ const a = await y(n, o);
527
+ i || t(a);
528
+ })(), () => {
529
+ i = !0;
530
+ };
531
+ }, [r]), e;
532
+ }
533
+ function _(r) {
534
+ const e = f(h);
535
+ if (!e)
536
+ throw new Error("usePlugin must be used within PluginRegistryProvider");
537
+ return p(() => e.getPlugin(r), [e, r]);
538
+ }
539
+ function A(r) {
540
+ const e = f(h);
541
+ if (!e)
542
+ throw new Error("useService must be used within PluginRegistryProvider");
543
+ const [t, i] = l(null), [s, n] = l(!0), [o, a] = l(null), u = p(
544
+ () => e.getService(r),
545
+ [e, r]
546
+ );
547
+ return d(() => {
548
+ let m = !1;
549
+ return n(!0), a(null), u.then((g) => {
550
+ m || (i(g), n(!1));
551
+ }).catch((g) => {
552
+ m || (a(g instanceof Error ? g : new Error(String(g))), n(!1));
553
+ }), () => {
554
+ m = !0;
555
+ };
556
+ }, [u]), { service: t, isLoading: s, error: o };
557
+ }
558
+ async function k(r, e) {
559
+ const t = [];
560
+ for (const i of r)
561
+ i.permissions && i.permissions.length > 0 && !await Promise.resolve(e(i.permissions)) || t.push(i);
562
+ return t;
563
+ }
564
+ function U() {
565
+ const r = f(h), [e, t] = l([]);
566
+ if (!r)
567
+ throw new Error("useWidgets must be used within PluginRegistryProvider");
568
+ return d(() => {
569
+ let i = !1;
570
+ return (async () => {
571
+ const n = r.getWidgets(), o = r.getPermissionChecker();
572
+ if (!o) {
573
+ i || t(n);
574
+ return;
575
+ }
576
+ const a = await k(n, o);
577
+ i || t(a);
578
+ })(), () => {
579
+ i = !0;
580
+ };
581
+ }, [r]), e;
582
+ }
583
+ export {
584
+ R as PluginRegistry,
585
+ h as PluginRegistryContext,
586
+ b as PluginRegistryProvider,
587
+ v as SDK_VERSION,
588
+ N as ServiceContainer,
589
+ _ as usePlugin,
590
+ $ as usePluginMenu,
591
+ A as useService,
592
+ U as useWidgets
593
+ };
594
+ //# sourceMappingURL=index.js.map