@kuckit/sdk-react 1.0.0
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/dist/index.d.ts +668 -0
- package/dist/index.js +700 -0
- package/package.json +41 -0
- package/src/index.ts +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { Fragment, createContext, createElement, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/define-module.ts
|
|
4
|
+
/**
|
|
5
|
+
* Helper function to define a client module with type safety
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* export const kuckitClientModule = defineKuckitClientModule({
|
|
10
|
+
* id: 'acme.billing',
|
|
11
|
+
* displayName: 'Billing',
|
|
12
|
+
* version: '1.0.0',
|
|
13
|
+
*
|
|
14
|
+
* register(ctx) {
|
|
15
|
+
* ctx.registerComponent('BillingDashboard', BillingDashboard)
|
|
16
|
+
* ctx.registerComponent('InvoiceList', InvoiceList)
|
|
17
|
+
* },
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function defineKuckitClientModule(mod) {
|
|
22
|
+
return mod;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/registry.ts
|
|
27
|
+
/**
|
|
28
|
+
* Registry for module-provided routes
|
|
29
|
+
*
|
|
30
|
+
* Collects route definitions during module loading and provides
|
|
31
|
+
* methods to build a TanStack Router route tree.
|
|
32
|
+
*/
|
|
33
|
+
var RouteRegistry = class {
|
|
34
|
+
routes = /* @__PURE__ */ new Map();
|
|
35
|
+
frozen = false;
|
|
36
|
+
/**
|
|
37
|
+
* Register a route definition
|
|
38
|
+
* @throws Error if registry is frozen or route ID already exists
|
|
39
|
+
*/
|
|
40
|
+
add(route) {
|
|
41
|
+
if (this.frozen) throw new Error(`RouteRegistry is frozen. Cannot add route "${route.id}" after finalization.`);
|
|
42
|
+
if (this.routes.has(route.id)) throw new Error(`Route with ID "${route.id}" is already registered.`);
|
|
43
|
+
this.routes.set(route.id, route);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get all registered routes
|
|
47
|
+
*/
|
|
48
|
+
getAll() {
|
|
49
|
+
return Array.from(this.routes.values());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get a route by ID
|
|
53
|
+
*/
|
|
54
|
+
get(id) {
|
|
55
|
+
return this.routes.get(id);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if a route is registered
|
|
59
|
+
*/
|
|
60
|
+
has(id) {
|
|
61
|
+
return this.routes.has(id);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get routes grouped by parent ID
|
|
65
|
+
*/
|
|
66
|
+
getByParent(parentId = "__root__") {
|
|
67
|
+
return this.getAll().filter((r) => (r.parentRouteId ?? "__root__") === parentId);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Freeze the registry to prevent further modifications
|
|
71
|
+
*/
|
|
72
|
+
freeze() {
|
|
73
|
+
this.frozen = true;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if registry is frozen
|
|
77
|
+
*/
|
|
78
|
+
isFrozen() {
|
|
79
|
+
return this.frozen;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the number of registered routes
|
|
83
|
+
*/
|
|
84
|
+
get size() {
|
|
85
|
+
return this.routes.size;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clear all routes (only works if not frozen)
|
|
89
|
+
*/
|
|
90
|
+
clear() {
|
|
91
|
+
if (this.frozen) throw new Error("RouteRegistry is frozen. Cannot clear routes.");
|
|
92
|
+
this.routes.clear();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Registry for module-provided navigation items
|
|
97
|
+
*
|
|
98
|
+
* Collects nav items during module loading and provides
|
|
99
|
+
* methods to build navigation menus.
|
|
100
|
+
*/
|
|
101
|
+
var NavRegistry = class {
|
|
102
|
+
items = /* @__PURE__ */ new Map();
|
|
103
|
+
frozen = false;
|
|
104
|
+
/**
|
|
105
|
+
* Register a navigation item
|
|
106
|
+
* @throws Error if registry is frozen or item ID already exists
|
|
107
|
+
*/
|
|
108
|
+
add(item) {
|
|
109
|
+
if (this.frozen) throw new Error(`NavRegistry is frozen. Cannot add item "${item.id}" after finalization.`);
|
|
110
|
+
if (this.items.has(item.id)) throw new Error(`Navigation item with ID "${item.id}" is already registered.`);
|
|
111
|
+
this.items.set(item.id, item);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get all registered navigation items, sorted by order
|
|
115
|
+
*/
|
|
116
|
+
getAll() {
|
|
117
|
+
return Array.from(this.items.values()).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get a navigation item by ID
|
|
121
|
+
*/
|
|
122
|
+
get(id) {
|
|
123
|
+
return this.items.get(id);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check if a nav item is registered
|
|
127
|
+
*/
|
|
128
|
+
has(id) {
|
|
129
|
+
return this.items.has(id);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get top-level navigation items (no parent)
|
|
133
|
+
*/
|
|
134
|
+
getTopLevel() {
|
|
135
|
+
return this.getAll().filter((item) => !item.parentId);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get nav items that should appear in main navigation
|
|
139
|
+
* (items with showInMainNav === true)
|
|
140
|
+
*/
|
|
141
|
+
getMainNavItems() {
|
|
142
|
+
return this.getAll().filter((item) => !item.parentId && item.showInMainNav === true);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get module nav items (not in main nav)
|
|
146
|
+
* (items without showInMainNav or showInMainNav === false)
|
|
147
|
+
*/
|
|
148
|
+
getModuleNavItems() {
|
|
149
|
+
return this.getAll().filter((item) => !item.parentId && item.showInMainNav !== true);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get child navigation items for a parent
|
|
153
|
+
*/
|
|
154
|
+
getChildren(parentId) {
|
|
155
|
+
return this.getAll().filter((item) => item.parentId === parentId);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build a hierarchical navigation tree
|
|
159
|
+
*/
|
|
160
|
+
buildTree() {
|
|
161
|
+
return this.getTopLevel().map((item) => this.buildTreeItem(item));
|
|
162
|
+
}
|
|
163
|
+
buildTreeItem(item) {
|
|
164
|
+
const children = this.getChildren(item.id);
|
|
165
|
+
return {
|
|
166
|
+
...item,
|
|
167
|
+
children: children.length > 0 ? children.map((c) => this.buildTreeItem(c)) : void 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Freeze the registry to prevent further modifications
|
|
172
|
+
*/
|
|
173
|
+
freeze() {
|
|
174
|
+
this.frozen = true;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check if registry is frozen
|
|
178
|
+
*/
|
|
179
|
+
isFrozen() {
|
|
180
|
+
return this.frozen;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get the number of registered items
|
|
184
|
+
*/
|
|
185
|
+
get size() {
|
|
186
|
+
return this.items.size;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Clear all items (only works if not frozen)
|
|
190
|
+
*/
|
|
191
|
+
clear() {
|
|
192
|
+
if (this.frozen) throw new Error("NavRegistry is frozen. Cannot clear items.");
|
|
193
|
+
this.items.clear();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Registry for module-provided slot components
|
|
198
|
+
*
|
|
199
|
+
* Collects components registered to named slots during module loading
|
|
200
|
+
* and provides methods to retrieve them for rendering.
|
|
201
|
+
*/
|
|
202
|
+
var SlotRegistry = class {
|
|
203
|
+
slots = /* @__PURE__ */ new Map();
|
|
204
|
+
frozen = false;
|
|
205
|
+
/**
|
|
206
|
+
* Register a component into a named slot
|
|
207
|
+
* @throws Error if registry is frozen
|
|
208
|
+
*/
|
|
209
|
+
add(slotName, component, moduleId, options) {
|
|
210
|
+
if (this.frozen) throw new Error(`SlotRegistry is frozen. Cannot add to slot "${slotName}" after finalization.`);
|
|
211
|
+
const registration = {
|
|
212
|
+
slotName,
|
|
213
|
+
component,
|
|
214
|
+
moduleId,
|
|
215
|
+
order: options?.order ?? 100,
|
|
216
|
+
props: options?.props
|
|
217
|
+
};
|
|
218
|
+
const existing = this.slots.get(slotName);
|
|
219
|
+
if (existing) existing.push(registration);
|
|
220
|
+
else this.slots.set(slotName, [registration]);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get all components registered to a slot, sorted by order
|
|
224
|
+
*/
|
|
225
|
+
getSlot(name) {
|
|
226
|
+
return [...this.slots.get(name) ?? []].sort((a, b) => a.order - b.order);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if a slot has any components registered
|
|
230
|
+
*/
|
|
231
|
+
hasSlot(name) {
|
|
232
|
+
const registrations = this.slots.get(name);
|
|
233
|
+
return registrations !== void 0 && registrations.length > 0;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get all slot names that have components registered
|
|
237
|
+
*/
|
|
238
|
+
getSlotNames() {
|
|
239
|
+
return Array.from(this.slots.keys());
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get total number of registered components across all slots
|
|
243
|
+
*/
|
|
244
|
+
get size() {
|
|
245
|
+
let count = 0;
|
|
246
|
+
for (const registrations of this.slots.values()) count += registrations.length;
|
|
247
|
+
return count;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Freeze the registry to prevent further modifications
|
|
251
|
+
*/
|
|
252
|
+
freeze() {
|
|
253
|
+
this.frozen = true;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Check if registry is frozen
|
|
257
|
+
*/
|
|
258
|
+
isFrozen() {
|
|
259
|
+
return this.frozen;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Clear all slots (only works if not frozen)
|
|
263
|
+
*/
|
|
264
|
+
clear() {
|
|
265
|
+
if (this.frozen) throw new Error("SlotRegistry is frozen. Cannot clear slots.");
|
|
266
|
+
this.slots.clear();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Context for navigation registry
|
|
271
|
+
*/
|
|
272
|
+
const NavContext = createContext(null);
|
|
273
|
+
function KuckitNavProvider({ registry, children }) {
|
|
274
|
+
return createElement(NavContext.Provider, { value: registry }, children);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Hook to access navigation items
|
|
278
|
+
*/
|
|
279
|
+
function useKuckitNav() {
|
|
280
|
+
const ctx = useContext(NavContext);
|
|
281
|
+
if (!ctx) throw new Error("useKuckitNav must be used within a KuckitNavProvider");
|
|
282
|
+
return ctx;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Hook to get sorted navigation items
|
|
286
|
+
*/
|
|
287
|
+
function useNavItems() {
|
|
288
|
+
return useKuckitNav().getAll();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Hook to get hierarchical navigation tree
|
|
292
|
+
*/
|
|
293
|
+
function useNavTree() {
|
|
294
|
+
return useKuckitNav().buildTree();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Create a component registry for storing named components
|
|
298
|
+
*/
|
|
299
|
+
function createComponentRegistry() {
|
|
300
|
+
const components = /* @__PURE__ */ new Map();
|
|
301
|
+
return {
|
|
302
|
+
register: (name, component) => {
|
|
303
|
+
if (components.has(name)) console.warn(`Component "${name}" is already registered. Overwriting.`);
|
|
304
|
+
components.set(name, component);
|
|
305
|
+
},
|
|
306
|
+
get: (name) => components.get(name),
|
|
307
|
+
has: (name) => components.has(name),
|
|
308
|
+
getAll: () => new Map(components)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Validates that a capability string matches expected patterns
|
|
313
|
+
*/
|
|
314
|
+
const isValidCapability = (cap) => {
|
|
315
|
+
if ([
|
|
316
|
+
"nav.item",
|
|
317
|
+
"settings.page",
|
|
318
|
+
"dashboard.widget",
|
|
319
|
+
"api.webhook",
|
|
320
|
+
"api.public",
|
|
321
|
+
"slot.provider"
|
|
322
|
+
].includes(cap)) return true;
|
|
323
|
+
if (cap.startsWith("custom.") && cap.length > 7) return true;
|
|
324
|
+
return false;
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Registry for loaded Kuckit client modules
|
|
328
|
+
*
|
|
329
|
+
* Tracks all loaded modules and their capabilities for querying.
|
|
330
|
+
*/
|
|
331
|
+
var ClientModuleRegistry = class {
|
|
332
|
+
modules = /* @__PURE__ */ new Map();
|
|
333
|
+
frozen = false;
|
|
334
|
+
/**
|
|
335
|
+
* Register a loaded module
|
|
336
|
+
* @throws Error if registry is frozen or module ID already exists
|
|
337
|
+
*/
|
|
338
|
+
register(module) {
|
|
339
|
+
if (this.frozen) throw new Error(`ClientModuleRegistry is frozen. Cannot register module "${module.id}" after finalization.`);
|
|
340
|
+
if (this.modules.has(module.id)) throw new Error(`Module with ID "${module.id}" is already registered.`);
|
|
341
|
+
const capabilities = module.capabilities ?? [];
|
|
342
|
+
for (const cap of capabilities) if (!isValidCapability(cap)) throw new Error(`Invalid capability "${cap}" in module "${module.id}". Capabilities must be built-in (nav.item, settings.page, etc.) or custom.* prefixed.`);
|
|
343
|
+
this.modules.set(module.id, {
|
|
344
|
+
id: module.id,
|
|
345
|
+
displayName: module.displayName,
|
|
346
|
+
description: module.description,
|
|
347
|
+
version: module.version,
|
|
348
|
+
capabilities
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get all registered modules
|
|
353
|
+
*/
|
|
354
|
+
getAll() {
|
|
355
|
+
return Array.from(this.modules.values());
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get a module by ID
|
|
359
|
+
*/
|
|
360
|
+
getById(id) {
|
|
361
|
+
return this.modules.get(id);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Check if a module is registered
|
|
365
|
+
*/
|
|
366
|
+
has(id) {
|
|
367
|
+
return this.modules.has(id);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Get all modules that have a specific capability
|
|
371
|
+
*/
|
|
372
|
+
getWithCapability(capability) {
|
|
373
|
+
return this.getAll().filter((mod) => mod.capabilities.includes(capability));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Check if a specific module has a capability
|
|
377
|
+
*/
|
|
378
|
+
hasCapability(moduleId, capability) {
|
|
379
|
+
const module = this.modules.get(moduleId);
|
|
380
|
+
return module ? module.capabilities.includes(capability) : false;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Get all unique capabilities across all modules
|
|
384
|
+
*/
|
|
385
|
+
getAllCapabilities() {
|
|
386
|
+
const caps = /* @__PURE__ */ new Set();
|
|
387
|
+
for (const mod of this.modules.values()) for (const cap of mod.capabilities) caps.add(cap);
|
|
388
|
+
return Array.from(caps);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Freeze the registry to prevent further modifications
|
|
392
|
+
*/
|
|
393
|
+
freeze() {
|
|
394
|
+
this.frozen = true;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Check if registry is frozen
|
|
398
|
+
*/
|
|
399
|
+
isFrozen() {
|
|
400
|
+
return this.frozen;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Get the number of registered modules
|
|
404
|
+
*/
|
|
405
|
+
get size() {
|
|
406
|
+
return this.modules.size;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Clear all modules (only works if not frozen)
|
|
410
|
+
*/
|
|
411
|
+
clear() {
|
|
412
|
+
if (this.frozen) throw new Error("ClientModuleRegistry is frozen. Cannot clear modules.");
|
|
413
|
+
this.modules.clear();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
let globalClientRegistry = null;
|
|
417
|
+
/**
|
|
418
|
+
* Get the global client module registry
|
|
419
|
+
* Creates one if it doesn't exist
|
|
420
|
+
*/
|
|
421
|
+
function getClientModuleRegistry() {
|
|
422
|
+
if (!globalClientRegistry) globalClientRegistry = new ClientModuleRegistry();
|
|
423
|
+
return globalClientRegistry;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get all client modules that have a specific capability
|
|
427
|
+
* Convenience function that uses the global registry
|
|
428
|
+
*/
|
|
429
|
+
function getClientModulesWithCapability(capability) {
|
|
430
|
+
return getClientModuleRegistry().getWithCapability(capability);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Reset the global client registry (mainly for testing)
|
|
434
|
+
*/
|
|
435
|
+
function resetClientModuleRegistry() {
|
|
436
|
+
globalClientRegistry = null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Factory function to create fresh registries
|
|
440
|
+
*/
|
|
441
|
+
function createRegistries() {
|
|
442
|
+
return {
|
|
443
|
+
routeRegistry: new RouteRegistry(),
|
|
444
|
+
navRegistry: new NavRegistry(),
|
|
445
|
+
componentRegistry: createComponentRegistry(),
|
|
446
|
+
slotRegistry: new SlotRegistry(),
|
|
447
|
+
moduleRegistry: new ClientModuleRegistry()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/loader.ts
|
|
453
|
+
/**
|
|
454
|
+
* Validates that a module has the correct shape
|
|
455
|
+
*/
|
|
456
|
+
const validateModule = (candidate, source) => {
|
|
457
|
+
if (!candidate || typeof candidate !== "object") throw new Error(`Invalid Kuckit client module from "${source}": expected an object with module definition`);
|
|
458
|
+
const mod = candidate;
|
|
459
|
+
if (!mod.id || typeof mod.id !== "string") throw new Error(`Invalid Kuckit client module from "${source}": missing or invalid 'id' property`);
|
|
460
|
+
if (mod.register !== void 0 && typeof mod.register !== "function") throw new Error(`Invalid Kuckit client module from "${source}": 'register' must be a function`);
|
|
461
|
+
if (mod.onUnload !== void 0 && typeof mod.onUnload !== "function") throw new Error(`Invalid Kuckit client module from "${source}": 'onUnload' must be a function`);
|
|
462
|
+
return mod;
|
|
463
|
+
};
|
|
464
|
+
/**
|
|
465
|
+
* Load and initialize Kuckit client modules
|
|
466
|
+
*
|
|
467
|
+
* This function orchestrates the client module loading lifecycle:
|
|
468
|
+
*
|
|
469
|
+
* 1. **Import Phase**: Dynamic import each module package (or use direct reference)
|
|
470
|
+
* 2. **Register Phase**: Run register() hooks with context (routes, nav, components, slots)
|
|
471
|
+
* 3. **Finalize Phase**: Freeze registries and call onComplete
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```tsx
|
|
475
|
+
* const { modules, routeRegistry, navRegistry, slotRegistry, moduleRegistry } = await loadKuckitClientModules({
|
|
476
|
+
* orpc,
|
|
477
|
+
* queryClient,
|
|
478
|
+
* env: 'production',
|
|
479
|
+
* modules: [
|
|
480
|
+
* { package: '@acme/billing-module/client' },
|
|
481
|
+
* { module: myLocalModule },
|
|
482
|
+
* ],
|
|
483
|
+
* })
|
|
484
|
+
*
|
|
485
|
+
* // Use routeRegistry to build TanStack Router routes
|
|
486
|
+
* const moduleRoutes = routeRegistry.getAll()
|
|
487
|
+
*
|
|
488
|
+
* // Use navRegistry for sidebar navigation
|
|
489
|
+
* const navItems = navRegistry.getAll()
|
|
490
|
+
*
|
|
491
|
+
* // Use slotRegistry with KuckitSlot component
|
|
492
|
+
* <KuckitSlot name="dashboard.widgets" />
|
|
493
|
+
*
|
|
494
|
+
* // Query modules by capability
|
|
495
|
+
* const navModules = moduleRegistry.getWithCapability('nav.item')
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
const loadKuckitClientModules = async (opts) => {
|
|
499
|
+
const { orpc, queryClient, env, modules, onRegisterComponent, onRegisterRoute, onRegisterNavItem, onRegisterSlot, onComplete } = opts;
|
|
500
|
+
const loadedModules = [];
|
|
501
|
+
const routeRegistry = new RouteRegistry();
|
|
502
|
+
const navRegistry = new NavRegistry();
|
|
503
|
+
const componentRegistry = createComponentRegistry();
|
|
504
|
+
const slotRegistry = new SlotRegistry();
|
|
505
|
+
const moduleRegistry = getClientModuleRegistry();
|
|
506
|
+
for (const spec of modules) {
|
|
507
|
+
if (spec.disabled) continue;
|
|
508
|
+
let validated;
|
|
509
|
+
let source;
|
|
510
|
+
if (spec.module) {
|
|
511
|
+
validated = validateModule(spec.module, spec.module.id ?? "direct-module");
|
|
512
|
+
source = "direct";
|
|
513
|
+
} else if (spec.package) {
|
|
514
|
+
let imported;
|
|
515
|
+
try {
|
|
516
|
+
imported = await import(
|
|
517
|
+
/* @vite-ignore */
|
|
518
|
+
spec.package
|
|
519
|
+
);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
522
|
+
throw new Error(`Failed to import client module "${spec.package}": ${message}`);
|
|
523
|
+
}
|
|
524
|
+
validated = validateModule(imported.kuckitClientModule ?? imported.default, spec.package);
|
|
525
|
+
source = "package";
|
|
526
|
+
} else throw new Error("ClientModuleSpec must have either \"module\" or \"package\" property");
|
|
527
|
+
moduleRegistry.register(validated);
|
|
528
|
+
loadedModules.push({
|
|
529
|
+
...validated,
|
|
530
|
+
_source: source
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
for (const mod of loadedModules) if (mod.register) {
|
|
534
|
+
const ctx = {
|
|
535
|
+
orpc,
|
|
536
|
+
queryClient,
|
|
537
|
+
env,
|
|
538
|
+
registerComponent: (name, component) => {
|
|
539
|
+
componentRegistry.register(name, component);
|
|
540
|
+
onRegisterComponent?.(name, component);
|
|
541
|
+
},
|
|
542
|
+
addRoute: (route) => {
|
|
543
|
+
routeRegistry.add(route);
|
|
544
|
+
onRegisterRoute?.(route);
|
|
545
|
+
},
|
|
546
|
+
addNavItem: (item) => {
|
|
547
|
+
navRegistry.add(item);
|
|
548
|
+
onRegisterNavItem?.(item);
|
|
549
|
+
},
|
|
550
|
+
registerSlot: (slotName, component, options) => {
|
|
551
|
+
slotRegistry.add(slotName, component, mod.id, options);
|
|
552
|
+
onRegisterSlot?.(slotName, component, mod.id, options);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
try {
|
|
556
|
+
await mod.register(ctx);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
559
|
+
throw new Error(`Client module "${mod.id}" register() failed: ${message}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
routeRegistry.freeze();
|
|
563
|
+
navRegistry.freeze();
|
|
564
|
+
slotRegistry.freeze();
|
|
565
|
+
const result = {
|
|
566
|
+
modules: loadedModules,
|
|
567
|
+
routeRegistry,
|
|
568
|
+
navRegistry,
|
|
569
|
+
componentRegistry,
|
|
570
|
+
slotRegistry,
|
|
571
|
+
moduleRegistry
|
|
572
|
+
};
|
|
573
|
+
if (onComplete) await onComplete(result);
|
|
574
|
+
return result;
|
|
575
|
+
};
|
|
576
|
+
/**
|
|
577
|
+
* Create an unload handler for loaded client modules
|
|
578
|
+
*
|
|
579
|
+
* Returns a function that calls onUnload() on all modules in reverse order.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* ```ts
|
|
583
|
+
* const unload = createClientModuleUnloadHandler(loadedModules)
|
|
584
|
+
*
|
|
585
|
+
* // When cleaning up (e.g., HMR, route unmount)
|
|
586
|
+
* await unload()
|
|
587
|
+
* ```
|
|
588
|
+
*/
|
|
589
|
+
const createClientModuleUnloadHandler = (modules) => {
|
|
590
|
+
return async () => {
|
|
591
|
+
for (const mod of [...modules].reverse()) if (mod.onUnload) try {
|
|
592
|
+
await mod.onUnload();
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error(`Client module "${mod.id}" onUnload() failed:`, error);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/slots.ts
|
|
601
|
+
/**
|
|
602
|
+
* Context for slot registry
|
|
603
|
+
*/
|
|
604
|
+
const SlotContext = createContext(null);
|
|
605
|
+
function KuckitSlotProvider({ registry, children }) {
|
|
606
|
+
return createElement(SlotContext.Provider, { value: registry }, children);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Hook to access the slot registry
|
|
610
|
+
*/
|
|
611
|
+
function useSlotRegistry() {
|
|
612
|
+
const ctx = useContext(SlotContext);
|
|
613
|
+
if (!ctx) throw new Error("useSlotRegistry must be used within a KuckitSlotProvider");
|
|
614
|
+
return ctx;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Hook to get components registered to a specific slot
|
|
618
|
+
*/
|
|
619
|
+
function useSlot(name) {
|
|
620
|
+
return useSlotRegistry().getSlot(name);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Hook to check if a slot has any components
|
|
624
|
+
*/
|
|
625
|
+
function useHasSlot(name) {
|
|
626
|
+
return useSlotRegistry().hasSlot(name);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Component that renders all components registered to a named slot
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```tsx
|
|
633
|
+
* // In host app layout
|
|
634
|
+
* <KuckitSlot name="dashboard.widgets" />
|
|
635
|
+
*
|
|
636
|
+
* // With fallback
|
|
637
|
+
* <KuckitSlot name="settings.tabs" fallback={<EmptyState />} />
|
|
638
|
+
*
|
|
639
|
+
* // With wrapper
|
|
640
|
+
* <KuckitSlot name="nav.items" wrapperTag="nav" wrapperClassName="flex gap-2" />
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
function KuckitSlot({ name, fallback, wrapperClassName, wrapperTag, slotProps }) {
|
|
644
|
+
const ctx = useContext(SlotContext);
|
|
645
|
+
if (!ctx) return fallback ?? null;
|
|
646
|
+
const registrations = ctx.getSlot(name);
|
|
647
|
+
if (registrations.length === 0) return fallback ?? null;
|
|
648
|
+
const rendered = registrations.map((reg, index) => {
|
|
649
|
+
const Component = reg.component;
|
|
650
|
+
const combinedProps = {
|
|
651
|
+
...reg.props,
|
|
652
|
+
...slotProps
|
|
653
|
+
};
|
|
654
|
+
return createElement(Component, {
|
|
655
|
+
key: `${reg.moduleId}-${index}`,
|
|
656
|
+
...combinedProps
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
if (wrapperTag) return createElement(wrapperTag, { className: wrapperClassName }, rendered);
|
|
660
|
+
if (wrapperClassName) return createElement("div", { className: wrapperClassName }, rendered);
|
|
661
|
+
return createElement(Fragment, null, rendered);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/rpc-context.ts
|
|
666
|
+
/**
|
|
667
|
+
* Context for RPC client access
|
|
668
|
+
*/
|
|
669
|
+
const RpcContext = createContext(null);
|
|
670
|
+
function KuckitRpcProvider({ client, children }) {
|
|
671
|
+
return createElement(RpcContext.Provider, { value: client }, children);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Hook to access the RPC client
|
|
675
|
+
*
|
|
676
|
+
* @example
|
|
677
|
+
* ```tsx
|
|
678
|
+
* import { useRpc } from '@kuckit/sdk-react'
|
|
679
|
+
* import type { AppRouterClient } from '@kuckit/api/routers/index'
|
|
680
|
+
*
|
|
681
|
+
* function MyComponent() {
|
|
682
|
+
* const rpc = useRpc<AppRouterClient>()
|
|
683
|
+
* const result = await rpc.myRouter.myProcedure({ input: 'value' })
|
|
684
|
+
* }
|
|
685
|
+
* ```
|
|
686
|
+
*/
|
|
687
|
+
function useRpc() {
|
|
688
|
+
const ctx = useContext(RpcContext);
|
|
689
|
+
if (!ctx) throw new Error("useRpc must be used within a KuckitRpcProvider");
|
|
690
|
+
return ctx;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Hook to check if RPC client is available
|
|
694
|
+
*/
|
|
695
|
+
function useHasRpc() {
|
|
696
|
+
return useContext(RpcContext) !== null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
//#endregion
|
|
700
|
+
export { ClientModuleRegistry, KuckitNavProvider, KuckitRpcProvider, KuckitSlot, KuckitSlotProvider, NavRegistry, RouteRegistry, SlotRegistry, createClientModuleUnloadHandler, createComponentRegistry, createRegistries, defineKuckitClientModule, getClientModuleRegistry, getClientModulesWithCapability, loadKuckitClientModules, resetClientModuleRegistry, useHasRpc, useHasSlot, useKuckitNav, useNavItems, useNavTree, useRpc, useSlot, useSlotRegistry };
|