qdadm 0.30.0 → 0.31.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/package.json +2 -1
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +1 -0
- package/src/composables/useAuth.js +43 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/debug/AuthCollector.js +254 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +376 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +103 -0
- package/src/debug/components/panels/EntitiesPanel.vue +616 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/storage/MockApiStorage.js +17 -1
- package/src/entity/storage/index.js +9 -2
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +436 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SignalBus.js +2 -7
- package/src/kernel/index.js +14 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
package/src/kernel/Kernel.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
import { createApp } from 'vue'
|
|
40
|
+
import { createApp, h } from 'vue'
|
|
41
41
|
import { createPinia } from 'pinia'
|
|
42
42
|
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
|
43
43
|
import ToastService from 'primevue/toastservice'
|
|
@@ -45,7 +45,9 @@ import ConfirmationService from 'primevue/confirmationservice'
|
|
|
45
45
|
import Tooltip from 'primevue/tooltip'
|
|
46
46
|
|
|
47
47
|
import { createQdadm } from '../plugin.js'
|
|
48
|
-
import { initModules, getRoutes, setSectionOrder, alterMenuSections } from '../module/moduleRegistry.js'
|
|
48
|
+
import { initModules, getRoutes, setSectionOrder, alterMenuSections, registry } from '../module/moduleRegistry.js'
|
|
49
|
+
import { createModuleLoader } from './ModuleLoader.js'
|
|
50
|
+
import { createKernelContext } from './KernelContext.js'
|
|
49
51
|
import { Orchestrator } from '../orchestrator/Orchestrator.js'
|
|
50
52
|
import { createSignalBus } from './SignalBus.js'
|
|
51
53
|
import { createZoneRegistry } from '../zones/ZoneRegistry.js'
|
|
@@ -58,12 +60,18 @@ import { createDeferredRegistry } from '../deferred/DeferredRegistry.js'
|
|
|
58
60
|
import { createEventRouter } from './EventRouter.js'
|
|
59
61
|
import { createSSEBridge } from './SSEBridge.js'
|
|
60
62
|
|
|
63
|
+
// Debug imports are dynamic to enable tree-shaking in production
|
|
64
|
+
// When debugBar: false/undefined, no debug code is bundled
|
|
65
|
+
let DebugModule = null
|
|
66
|
+
let QdadmDebugBar = null
|
|
67
|
+
|
|
61
68
|
export class Kernel {
|
|
62
69
|
/**
|
|
63
70
|
* @param {object} options
|
|
64
71
|
* @param {object} options.root - Root Vue component
|
|
65
|
-
* @param {object} options.modules - Result of import.meta.glob for module init files
|
|
72
|
+
* @param {object} options.modules - Result of import.meta.glob for module init files (legacy)
|
|
66
73
|
* @param {object} options.modulesOptions - Options for initModules (e.g., { coreNavItems })
|
|
74
|
+
* @param {Array} options.moduleDefs - New-style module definitions (Module classes/objects/functions)
|
|
67
75
|
* @param {string[]} options.sectionOrder - Navigation section order
|
|
68
76
|
* @param {object} options.managers - Entity managers { name: config } - can be instances, strings, or config objects
|
|
69
77
|
* @param {object} options.managerRegistry - Registry of manager classes from qdadm-gen { name: ManagerClass }
|
|
@@ -71,7 +79,9 @@ export class Kernel {
|
|
|
71
79
|
* @param {function} options.managerResolver - Custom manager resolver (config, entityName, context) => Manager
|
|
72
80
|
* @param {object} options.authAdapter - Auth adapter for login/logout (app-level authentication)
|
|
73
81
|
* @param {object} options.entityAuthAdapter - Auth adapter for entity permissions (scope/silo checks)
|
|
74
|
-
* @param {object} options.pages - Page components {
|
|
82
|
+
* @param {object} options.pages - Page components { layout, shell? }
|
|
83
|
+
* @param {object} options.pages.layout - Main layout component (required)
|
|
84
|
+
* @param {object} options.pages.shell - Optional app shell (enables unified routing with zones everywhere)
|
|
75
85
|
* @param {string} options.homeRoute - Route name for home redirect (or object { name, component })
|
|
76
86
|
* @param {Array} options.coreRoutes - Additional routes as layout children (before module routes)
|
|
77
87
|
* @param {string} options.basePath - Base path for router (e.g., '/dashboard/')
|
|
@@ -84,8 +94,31 @@ export class Kernel {
|
|
|
84
94
|
* @param {boolean} options.warmup - Enable warmup at boot (default: true)
|
|
85
95
|
* @param {object} options.eventRouter - EventRouter config { 'source:signal': ['target:signal', ...] }
|
|
86
96
|
* @param {object} options.sse - SSEBridge config { url, reconnectDelay, signalPrefix, autoConnect, events }
|
|
97
|
+
* @param {object} options.debugBar - Debug bar config { module: DebugModule, component: QdadmDebugBar, ...options }
|
|
87
98
|
*/
|
|
88
99
|
constructor(options) {
|
|
100
|
+
// Auto-inject DebugModule if debugBar.module is provided
|
|
101
|
+
// User must import DebugModule separately for tree-shaking
|
|
102
|
+
if (options.debugBar?.module) {
|
|
103
|
+
const DebugModuleClass = options.debugBar.module
|
|
104
|
+
const { module: _, component: __, ...debugModuleOptions } = options.debugBar
|
|
105
|
+
// Enable by default when using debugBar shorthand
|
|
106
|
+
if (debugModuleOptions.enabled === undefined) {
|
|
107
|
+
debugModuleOptions.enabled = true
|
|
108
|
+
}
|
|
109
|
+
// Mark as kernel-managed to prevent zone block registration
|
|
110
|
+
// (Kernel handles rendering via root wrapper)
|
|
111
|
+
debugModuleOptions._kernelManaged = true
|
|
112
|
+
const debugModule = new DebugModuleClass(debugModuleOptions)
|
|
113
|
+
options.moduleDefs = options.moduleDefs || []
|
|
114
|
+
options.moduleDefs.push(debugModule)
|
|
115
|
+
// Store component for root wrapper
|
|
116
|
+
QdadmDebugBar = options.debugBar.component
|
|
117
|
+
// Enable debug mode
|
|
118
|
+
if (!options.debug) {
|
|
119
|
+
options.debug = true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
89
122
|
this.options = options
|
|
90
123
|
this.vueApp = null
|
|
91
124
|
this.router = null
|
|
@@ -98,10 +131,21 @@ export class Kernel {
|
|
|
98
131
|
this.sseBridge = null
|
|
99
132
|
this.layoutComponents = null
|
|
100
133
|
this.securityChecker = null
|
|
134
|
+
/** @type {import('./ModuleLoader.js').ModuleLoader|null} */
|
|
135
|
+
this.moduleLoader = null
|
|
136
|
+
/** @type {Map<string|symbol, any>} Pending provides from modules (applied after vueApp creation) */
|
|
137
|
+
this._pendingProvides = new Map()
|
|
138
|
+
/** @type {Map<string, import('vue').Component>} Pending components from modules */
|
|
139
|
+
this._pendingComponents = new Map()
|
|
101
140
|
}
|
|
102
141
|
|
|
103
142
|
/**
|
|
104
143
|
* Create and configure the Vue app
|
|
144
|
+
*
|
|
145
|
+
* Note: This method is synchronous for backward compatibility.
|
|
146
|
+
* New-style modules (moduleDefs) are loaded synchronously via _loadModules().
|
|
147
|
+
* For async module loading, use createAppAsync() instead.
|
|
148
|
+
*
|
|
105
149
|
* @returns {App} Vue app instance ready to mount
|
|
106
150
|
*/
|
|
107
151
|
createApp() {
|
|
@@ -112,23 +156,72 @@ export class Kernel {
|
|
|
112
156
|
this._createDeferredRegistry()
|
|
113
157
|
// 2. Register auth:ready deferred (if auth configured)
|
|
114
158
|
this._registerAuthDeferred()
|
|
115
|
-
// 3. Initialize modules (can use all services, registers routes)
|
|
159
|
+
// 3. Initialize legacy modules (can use all services, registers routes)
|
|
116
160
|
this._initModules()
|
|
161
|
+
// 3.5. Load new-style modules (moduleDefs) - synchronous for backward compat
|
|
162
|
+
this._loadModulesSync()
|
|
117
163
|
// 4. Create router (needs routes from modules)
|
|
118
164
|
this._createRouter()
|
|
165
|
+
// 4.5. Setup auth guard (if authAdapter provided)
|
|
166
|
+
this._setupAuthGuard()
|
|
119
167
|
// 5. Setup auth:expired handler (needs router + authAdapter)
|
|
120
168
|
this._setupAuthExpiredHandler()
|
|
121
169
|
// 6. Create orchestrator and remaining components
|
|
122
170
|
this._createOrchestrator()
|
|
123
|
-
// 7.
|
|
171
|
+
// 7. Wire modules that need orchestrator (phase 2)
|
|
172
|
+
this._wireModules()
|
|
173
|
+
// 8. Create EventRouter (needs signals + orchestrator)
|
|
124
174
|
this._createEventRouter()
|
|
125
|
-
//
|
|
175
|
+
// 9. Create SSEBridge (needs signals + authAdapter for token)
|
|
126
176
|
this._createSSEBridge()
|
|
127
177
|
this._setupSecurity()
|
|
128
178
|
this._createLayoutComponents()
|
|
129
179
|
this._createVueApp()
|
|
130
180
|
this._installPlugins()
|
|
131
|
-
//
|
|
181
|
+
// 10. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
|
|
182
|
+
this._fireWarmups()
|
|
183
|
+
return this.vueApp
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create and configure the Vue app asynchronously
|
|
188
|
+
*
|
|
189
|
+
* Use this method when modules have async connect() methods.
|
|
190
|
+
* Supports the full async module loading flow.
|
|
191
|
+
*
|
|
192
|
+
* @returns {Promise<App>} Vue app instance ready to mount
|
|
193
|
+
*/
|
|
194
|
+
async createAppAsync() {
|
|
195
|
+
// 1. Create services first (modules need them)
|
|
196
|
+
this._createSignalBus()
|
|
197
|
+
this._createHookRegistry()
|
|
198
|
+
this._createZoneRegistry()
|
|
199
|
+
this._createDeferredRegistry()
|
|
200
|
+
// 2. Register auth:ready deferred (if auth configured)
|
|
201
|
+
this._registerAuthDeferred()
|
|
202
|
+
// 3. Initialize legacy modules (can use all services, registers routes)
|
|
203
|
+
this._initModules()
|
|
204
|
+
// 3.5. Load new-style modules (moduleDefs) - async version
|
|
205
|
+
await this._loadModules()
|
|
206
|
+
// 4. Create router (needs routes from modules)
|
|
207
|
+
this._createRouter()
|
|
208
|
+
// 4.5. Setup auth guard (if authAdapter provided)
|
|
209
|
+
this._setupAuthGuard()
|
|
210
|
+
// 5. Setup auth:expired handler (needs router + authAdapter)
|
|
211
|
+
this._setupAuthExpiredHandler()
|
|
212
|
+
// 6. Create orchestrator and remaining components
|
|
213
|
+
this._createOrchestrator()
|
|
214
|
+
// 7. Wire modules that need orchestrator (phase 2)
|
|
215
|
+
await this._wireModulesAsync()
|
|
216
|
+
// 8. Create EventRouter (needs signals + orchestrator)
|
|
217
|
+
this._createEventRouter()
|
|
218
|
+
// 9. Create SSEBridge (needs signals + authAdapter for token)
|
|
219
|
+
this._createSSEBridge()
|
|
220
|
+
this._setupSecurity()
|
|
221
|
+
this._createLayoutComponents()
|
|
222
|
+
this._createVueApp()
|
|
223
|
+
this._installPlugins()
|
|
224
|
+
// 10. Fire warmups (fire-and-forget, pages await via DeferredRegistry)
|
|
132
225
|
this._fireWarmups()
|
|
133
226
|
return this.vueApp
|
|
134
227
|
}
|
|
@@ -217,7 +310,7 @@ export class Kernel {
|
|
|
217
310
|
}
|
|
218
311
|
|
|
219
312
|
/**
|
|
220
|
-
* Initialize modules from glob import
|
|
313
|
+
* Initialize legacy modules from glob import
|
|
221
314
|
* Passes services to modules for zone/signal/hook registration
|
|
222
315
|
*/
|
|
223
316
|
_initModules() {
|
|
@@ -236,15 +329,168 @@ export class Kernel {
|
|
|
236
329
|
}
|
|
237
330
|
|
|
238
331
|
/**
|
|
239
|
-
* Create
|
|
332
|
+
* Create KernelContext for module connection
|
|
333
|
+
*
|
|
334
|
+
* Creates a context object that provides modules access to kernel services
|
|
335
|
+
* and registration APIs.
|
|
336
|
+
*
|
|
337
|
+
* @param {import('./Module.js').Module} module - Module instance
|
|
338
|
+
* @returns {import('./KernelContext.js').KernelContext}
|
|
339
|
+
* @private
|
|
240
340
|
*/
|
|
241
|
-
|
|
242
|
-
|
|
341
|
+
_createModuleContext(module) {
|
|
342
|
+
return createKernelContext(this, module)
|
|
343
|
+
}
|
|
243
344
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Load new-style modules synchronously
|
|
347
|
+
*
|
|
348
|
+
* Used by createApp() for backward compatibility. Modules with async
|
|
349
|
+
* connect() methods will have their promises ignored (fire-and-forget).
|
|
350
|
+
* For proper async support, use createAppAsync().
|
|
351
|
+
*
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
_loadModulesSync() {
|
|
355
|
+
const { moduleDefs } = this.options
|
|
356
|
+
if (!moduleDefs?.length) return
|
|
357
|
+
|
|
358
|
+
this.moduleLoader = createModuleLoader()
|
|
359
|
+
|
|
360
|
+
// Add all modules
|
|
361
|
+
for (const mod of moduleDefs) {
|
|
362
|
+
this.moduleLoader.add(mod)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Get sorted modules and load synchronously
|
|
366
|
+
const sorted = this.moduleLoader._topologicalSort()
|
|
367
|
+
|
|
368
|
+
for (const name of sorted) {
|
|
369
|
+
const module = this.moduleLoader._registered.get(name)
|
|
370
|
+
const ctx = this._createModuleContext(module)
|
|
371
|
+
|
|
372
|
+
// Check if enabled
|
|
373
|
+
if (!module.enabled(ctx)) {
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Connect module (sync - async modules will be fire-and-forget)
|
|
378
|
+
const result = module.connect(ctx)
|
|
379
|
+
|
|
380
|
+
// If it's a promise, we can't await it in sync context
|
|
381
|
+
// The module should handle its own async initialization
|
|
382
|
+
if (result instanceof Promise) {
|
|
383
|
+
result.catch(err => {
|
|
384
|
+
console.error(`[Kernel] Async module '${name}' failed:`, err)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.moduleLoader._loaded.set(name, module)
|
|
389
|
+
this.moduleLoader._loadOrder.push(name)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Load new-style modules asynchronously
|
|
395
|
+
*
|
|
396
|
+
* Fully supports async connect() methods in modules.
|
|
397
|
+
* Used by createAppAsync().
|
|
398
|
+
*
|
|
399
|
+
* @returns {Promise<void>}
|
|
400
|
+
* @private
|
|
401
|
+
*/
|
|
402
|
+
async _loadModules() {
|
|
403
|
+
const { moduleDefs } = this.options
|
|
404
|
+
if (!moduleDefs?.length) return
|
|
405
|
+
|
|
406
|
+
this.moduleLoader = createModuleLoader()
|
|
407
|
+
|
|
408
|
+
// Add all modules
|
|
409
|
+
for (const mod of moduleDefs) {
|
|
410
|
+
this.moduleLoader.add(mod)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Get sorted modules and load
|
|
414
|
+
const sorted = this.moduleLoader._topologicalSort()
|
|
415
|
+
|
|
416
|
+
for (const name of sorted) {
|
|
417
|
+
const module = this.moduleLoader._registered.get(name)
|
|
418
|
+
const ctx = this._createModuleContext(module)
|
|
419
|
+
|
|
420
|
+
// Check if enabled
|
|
421
|
+
if (!module.enabled(ctx)) {
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Connect module (await async)
|
|
426
|
+
await module.connect(ctx)
|
|
427
|
+
|
|
428
|
+
this.moduleLoader._loaded.set(name, module)
|
|
429
|
+
this.moduleLoader._loadOrder.push(name)
|
|
247
430
|
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Wire modules that need orchestrator (phase 2)
|
|
435
|
+
*
|
|
436
|
+
* Some modules may need access to the orchestrator after it's created.
|
|
437
|
+
* This method emits 'kernel:ready' signal that modules can listen to.
|
|
438
|
+
*
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
_wireModules() {
|
|
442
|
+
if (!this.moduleLoader) return
|
|
443
|
+
|
|
444
|
+
// Emit kernel:ready signal for modules that need orchestrator
|
|
445
|
+
const result = this.signals.emit('kernel:ready', {
|
|
446
|
+
orchestrator: this.orchestrator,
|
|
447
|
+
kernel: this
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
// If emit returns a promise (async handlers), we can't await it
|
|
451
|
+
if (result instanceof Promise) {
|
|
452
|
+
result.catch(err => {
|
|
453
|
+
console.error('[Kernel] kernel:ready handler failed:', err)
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Wire modules that need orchestrator (phase 2) - async version
|
|
460
|
+
*
|
|
461
|
+
* @returns {Promise<void>}
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
async _wireModulesAsync() {
|
|
465
|
+
if (!this.moduleLoader) return
|
|
466
|
+
|
|
467
|
+
// Emit kernel:ready signal for modules that need orchestrator
|
|
468
|
+
await this.signals.emit('kernel:ready', {
|
|
469
|
+
orchestrator: this.orchestrator,
|
|
470
|
+
kernel: this
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Create Vue Router
|
|
476
|
+
*
|
|
477
|
+
* Routes are built from:
|
|
478
|
+
* 1. Module routes (via ctx.routes()) - can be public or protected via meta
|
|
479
|
+
* 2. coreRoutes option - additional routes
|
|
480
|
+
* 3. homeRoute - redirect or component for /
|
|
481
|
+
*
|
|
482
|
+
* Auth is NOT hardcoded here - it's handled via:
|
|
483
|
+
* - Route meta: { public: true } or { requiresAuth: false }
|
|
484
|
+
* - Auth guard registered via _setupAuthGuard() if authAdapter provided
|
|
485
|
+
*
|
|
486
|
+
* Layout modes:
|
|
487
|
+
* - **Shell mode** (pages.shell): All routes inside shell, layout wraps protected area
|
|
488
|
+
* - **Layout-only mode**: Layout at root with all routes as children
|
|
489
|
+
*/
|
|
490
|
+
_createRouter() {
|
|
491
|
+
const { pages, homeRoute, coreRoutes, basePath, hashMode } = this.options
|
|
492
|
+
|
|
493
|
+
// Layout is required (shell is optional)
|
|
248
494
|
if (!pages?.layout) {
|
|
249
495
|
throw new Error('[Kernel] pages.layout is required')
|
|
250
496
|
}
|
|
@@ -252,50 +498,115 @@ export class Kernel {
|
|
|
252
498
|
// Build home route
|
|
253
499
|
let homeRouteConfig
|
|
254
500
|
if (typeof homeRoute === 'object' && homeRoute.component) {
|
|
255
|
-
// homeRoute is a route config with component
|
|
256
501
|
homeRouteConfig = { path: '', ...homeRoute }
|
|
257
502
|
} else {
|
|
258
|
-
// homeRoute is a route name for redirect
|
|
259
503
|
homeRouteConfig = { path: '', redirect: { name: homeRoute || 'home' } }
|
|
260
504
|
}
|
|
261
505
|
|
|
262
|
-
//
|
|
506
|
+
// Collect all module routes
|
|
507
|
+
const moduleRoutes = getRoutes()
|
|
508
|
+
|
|
509
|
+
// Separate public routes (meta.public or meta.requiresAuth === false)
|
|
510
|
+
const publicRoutes = moduleRoutes.filter(r => r.meta?.public || r.meta?.requiresAuth === false)
|
|
511
|
+
const protectedRoutes = moduleRoutes.filter(r => !r.meta?.public && r.meta?.requiresAuth !== false)
|
|
512
|
+
|
|
513
|
+
// Auto-create login route if pages.login is provided (backward compatibility)
|
|
514
|
+
// This creates a public route that modules don't need to define explicitly.
|
|
515
|
+
if (pages.login) {
|
|
516
|
+
publicRoutes.unshift({
|
|
517
|
+
path: '/login',
|
|
518
|
+
name: 'login',
|
|
519
|
+
component: pages.login,
|
|
520
|
+
meta: { public: true }
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Build layout children: home + coreRoutes + protected module routes
|
|
263
525
|
const layoutChildren = [
|
|
264
526
|
homeRouteConfig,
|
|
265
527
|
...(coreRoutes || []),
|
|
266
|
-
...
|
|
528
|
+
...protectedRoutes
|
|
267
529
|
]
|
|
268
530
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
531
|
+
let routes
|
|
532
|
+
|
|
533
|
+
if (pages.shell) {
|
|
534
|
+
// Shell mode: shell wraps everything, layout wraps protected area
|
|
535
|
+
routes = [
|
|
536
|
+
{
|
|
537
|
+
path: '/',
|
|
538
|
+
component: pages.shell,
|
|
539
|
+
children: [
|
|
540
|
+
// Public routes at shell level (login, register, etc.)
|
|
541
|
+
...publicRoutes,
|
|
542
|
+
// Protected area with layout
|
|
543
|
+
{
|
|
544
|
+
path: '',
|
|
545
|
+
component: pages.layout,
|
|
546
|
+
meta: { requiresAuth: true },
|
|
547
|
+
children: layoutChildren
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
]
|
|
552
|
+
} else {
|
|
553
|
+
// Layout-only mode: layout at root
|
|
554
|
+
// Public routes need to be handled differently (no shell)
|
|
555
|
+
routes = [
|
|
556
|
+
// Public routes standalone
|
|
557
|
+
...publicRoutes,
|
|
558
|
+
// Protected area with layout
|
|
559
|
+
{
|
|
560
|
+
path: '/',
|
|
561
|
+
component: pages.layout,
|
|
562
|
+
meta: { requiresAuth: true },
|
|
563
|
+
children: layoutChildren
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
}
|
|
283
567
|
|
|
284
568
|
this.router = createRouter({
|
|
285
569
|
history: hashMode ? createWebHashHistory(basePath) : createWebHistory(basePath),
|
|
286
570
|
routes
|
|
287
571
|
})
|
|
572
|
+
}
|
|
288
573
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Setup auth guard on router
|
|
576
|
+
*
|
|
577
|
+
* Only installed if authAdapter is provided.
|
|
578
|
+
* Uses route meta to determine access:
|
|
579
|
+
* - meta.public: true → always accessible
|
|
580
|
+
* - meta.requiresAuth: false → always accessible
|
|
581
|
+
* - meta.requiresAuth: true → requires auth (default for layout children)
|
|
582
|
+
* - no meta → inherits from parent route
|
|
583
|
+
*/
|
|
584
|
+
_setupAuthGuard() {
|
|
585
|
+
const { authAdapter } = this.options
|
|
586
|
+
if (!authAdapter) return
|
|
587
|
+
|
|
588
|
+
this.router.beforeEach((to, from, next) => {
|
|
589
|
+
// Check if route or any parent is explicitly public
|
|
590
|
+
const isPublic = to.matched.some(record =>
|
|
591
|
+
record.meta.public === true || record.meta.requiresAuth === false
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
if (isPublic) {
|
|
595
|
+
next()
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Check if route or any parent requires auth
|
|
600
|
+
const requiresAuth = to.matched.some(record => record.meta.requiresAuth === true)
|
|
601
|
+
|
|
602
|
+
if (requiresAuth && !authAdapter.isAuthenticated()) {
|
|
603
|
+
// Redirect to login (if exists) or emit signal
|
|
604
|
+
const loginRoute = this.router.hasRoute('login') ? { name: 'login' } : '/'
|
|
605
|
+
next(loginRoute)
|
|
606
|
+
} else {
|
|
607
|
+
next()
|
|
608
|
+
}
|
|
609
|
+
})
|
|
299
610
|
}
|
|
300
611
|
|
|
301
612
|
/**
|
|
@@ -390,12 +701,11 @@ export class Kernel {
|
|
|
390
701
|
|
|
391
702
|
/**
|
|
392
703
|
* Create zone registry for extensible UI composition
|
|
393
|
-
*
|
|
704
|
+
* Zones are created dynamically when used (no pre-registration).
|
|
394
705
|
*/
|
|
395
706
|
_createZoneRegistry() {
|
|
396
707
|
const debug = this.options.debug ?? false
|
|
397
708
|
this.zoneRegistry = createZoneRegistry({ debug })
|
|
398
|
-
registerStandardZones(this.zoneRegistry)
|
|
399
709
|
}
|
|
400
710
|
|
|
401
711
|
/**
|
|
@@ -489,12 +799,29 @@ export class Kernel {
|
|
|
489
799
|
|
|
490
800
|
/**
|
|
491
801
|
* Create Vue app instance
|
|
802
|
+
*
|
|
803
|
+
* When debugBar is enabled, wraps the root component to include QdadmDebugBar
|
|
804
|
+
* at the app level, ensuring it's visible on all pages (including login).
|
|
492
805
|
*/
|
|
493
806
|
_createVueApp() {
|
|
494
807
|
if (!this.options.root) {
|
|
495
808
|
throw new Error('[Kernel] root component is required')
|
|
496
809
|
}
|
|
497
|
-
|
|
810
|
+
|
|
811
|
+
// If debugBar is enabled and component provided, wrap root with DebugBar
|
|
812
|
+
if (this.options.debugBar?.component && QdadmDebugBar) {
|
|
813
|
+
const OriginalRoot = this.options.root
|
|
814
|
+
const DebugBarComponent = QdadmDebugBar
|
|
815
|
+
const WrappedRoot = {
|
|
816
|
+
name: 'QdadmRootWrapper',
|
|
817
|
+
render() {
|
|
818
|
+
return [h(OriginalRoot), h(DebugBarComponent)]
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
this.vueApp = createApp(WrappedRoot)
|
|
822
|
+
} else {
|
|
823
|
+
this.vueApp = createApp(this.options.root)
|
|
824
|
+
}
|
|
498
825
|
}
|
|
499
826
|
|
|
500
827
|
/**
|
|
@@ -527,6 +854,18 @@ export class Kernel {
|
|
|
527
854
|
// Router
|
|
528
855
|
app.use(this.router)
|
|
529
856
|
|
|
857
|
+
// Apply pending provides from modules (registered before vueApp existed)
|
|
858
|
+
for (const [key, value] of this._pendingProvides) {
|
|
859
|
+
app.provide(key, value)
|
|
860
|
+
}
|
|
861
|
+
this._pendingProvides.clear()
|
|
862
|
+
|
|
863
|
+
// Apply pending components from modules
|
|
864
|
+
for (const [name, component] of this._pendingComponents) {
|
|
865
|
+
app.component(name, component)
|
|
866
|
+
}
|
|
867
|
+
this._pendingComponents.clear()
|
|
868
|
+
|
|
530
869
|
// Extract home route name for breadcrumb
|
|
531
870
|
const { homeRoute } = this.options
|
|
532
871
|
const homeRouteName = typeof homeRoute === 'object' ? homeRoute.name : homeRoute
|
|
@@ -534,10 +873,24 @@ export class Kernel {
|
|
|
534
873
|
// Zone registry injection
|
|
535
874
|
app.provide('qdadmZoneRegistry', this.zoneRegistry)
|
|
536
875
|
|
|
537
|
-
// Dev mode: expose
|
|
876
|
+
// Dev mode: expose qdadm services on window for DevTools inspection
|
|
538
877
|
if (this.options.debug && typeof window !== 'undefined') {
|
|
878
|
+
window.__qdadm = {
|
|
879
|
+
kernel: this,
|
|
880
|
+
orchestrator: this.orchestrator,
|
|
881
|
+
signals: this.signals,
|
|
882
|
+
hooks: this.hookRegistry,
|
|
883
|
+
zones: this.zoneRegistry,
|
|
884
|
+
deferred: this.deferred,
|
|
885
|
+
router: this.router,
|
|
886
|
+
// Helper to get a manager quickly
|
|
887
|
+
get: (name) => this.orchestrator.get(name),
|
|
888
|
+
// List all managers
|
|
889
|
+
managers: () => this.orchestrator.getRegisteredNames()
|
|
890
|
+
}
|
|
891
|
+
// Legacy alias
|
|
539
892
|
window.__qdadmZones = this.zoneRegistry
|
|
540
|
-
console.debug('[qdadm]
|
|
893
|
+
console.debug('[qdadm] Debug mode: window.__qdadm exposed (orchestrator, signals, hooks, zones, deferred, router)')
|
|
541
894
|
}
|
|
542
895
|
|
|
543
896
|
// Signal bus injection
|
|
@@ -697,6 +1050,41 @@ export class Kernel {
|
|
|
697
1050
|
return this.securityChecker
|
|
698
1051
|
}
|
|
699
1052
|
|
|
1053
|
+
/**
|
|
1054
|
+
* Get the ModuleLoader instance
|
|
1055
|
+
* @returns {import('./ModuleLoader.js').ModuleLoader|null}
|
|
1056
|
+
*/
|
|
1057
|
+
getModuleLoader() {
|
|
1058
|
+
return this.moduleLoader
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Shorthand accessor for module loader
|
|
1063
|
+
* Allows `kernel.modules.getModules()` syntax
|
|
1064
|
+
* @returns {import('./ModuleLoader.js').ModuleLoader|null}
|
|
1065
|
+
*/
|
|
1066
|
+
get modules() {
|
|
1067
|
+
return this.moduleLoader
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Get the DebugModule instance if loaded
|
|
1072
|
+
* @returns {import('../debug/DebugModule.js').DebugModule|null}
|
|
1073
|
+
*/
|
|
1074
|
+
getDebugModule() {
|
|
1075
|
+
return this.moduleLoader?._loaded?.get('debug') ?? null
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Shorthand accessor for debug bridge
|
|
1080
|
+
* Allows `kernel.debugBar.toggle()` syntax
|
|
1081
|
+
* @returns {import('../debug/DebugBridge.js').DebugBridge|null}
|
|
1082
|
+
*/
|
|
1083
|
+
get debugBar() {
|
|
1084
|
+
const debugModule = this.getDebugModule()
|
|
1085
|
+
return debugModule?.getBridge?.() ?? null
|
|
1086
|
+
}
|
|
1087
|
+
|
|
700
1088
|
/**
|
|
701
1089
|
* Setup an axios client with automatic auth and error handling
|
|
702
1090
|
*
|