qdadm 1.13.0 → 1.19.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.
Files changed (86) hide show
  1. package/package.json +8 -4
  2. package/src/chain/ActiveStack.ts +79 -98
  3. package/src/chain/StackHydrator.ts +3 -2
  4. package/src/chain/index.ts +7 -1
  5. package/src/components/QdadmRoot.vue +52 -0
  6. package/src/components/edit/FormActions.vue +9 -6
  7. package/src/components/edit/LookupPickerDialog.vue +6 -3
  8. package/src/components/index.ts +6 -0
  9. package/src/composables/useEntityItemFormPage.ts +1 -0
  10. package/src/composables/useEntityItemShowPage.ts +1 -0
  11. package/src/composables/useFieldManager.ts +100 -3
  12. package/src/composables/useListPage.ts +50 -59
  13. package/src/composables/useListPage.utils.ts +101 -0
  14. package/src/composables/useNavigation.ts +26 -3
  15. package/src/composables/useOptionsLookup.ts +5 -1
  16. package/src/gen/generateManagers.test.js +27 -0
  17. package/src/gen/generateManagers.ts +12 -0
  18. package/src/hooks/HookRegistry.ts +14 -435
  19. package/src/i18n/I18n.ts +344 -0
  20. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  21. package/src/i18n/InlineTranslationProvider.ts +4 -0
  22. package/src/i18n/LazyTranslationProvider.ts +102 -0
  23. package/src/i18n/MessagesRegistry.ts +4 -0
  24. package/src/i18n/Resolver.ts +4 -0
  25. package/src/i18n/__tests__/I18n.test.ts +169 -0
  26. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  27. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  28. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  29. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  30. package/src/i18n/defaults/core.en.yml +55 -0
  31. package/src/i18n/defaults/core.fr.yml +55 -0
  32. package/src/i18n/index.ts +55 -0
  33. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  34. package/src/i18n/loaders/yaml.ts +35 -0
  35. package/src/i18n/strategies.ts +4 -0
  36. package/src/i18n/types.ts +34 -0
  37. package/src/i18n/useI18n.ts +34 -0
  38. package/src/index.ts +37 -0
  39. package/src/kernel/EventRouter.ts +17 -300
  40. package/src/kernel/Kernel.i18n.ts +29 -0
  41. package/src/kernel/Kernel.modules.ts +6 -0
  42. package/src/kernel/Kernel.registries.ts +10 -2
  43. package/src/kernel/Kernel.routing.ts +43 -1
  44. package/src/kernel/Kernel.ts +43 -0
  45. package/src/kernel/Kernel.types.ts +52 -1
  46. package/src/kernel/Kernel.vue.ts +121 -15
  47. package/src/kernel/KernelContext.entities.ts +80 -0
  48. package/src/kernel/KernelContext.events.ts +57 -0
  49. package/src/kernel/KernelContext.i18n.ts +37 -0
  50. package/src/kernel/KernelContext.permissions.ts +38 -0
  51. package/src/kernel/KernelContext.routing.ts +280 -0
  52. package/src/kernel/KernelContext.ts +125 -834
  53. package/src/kernel/KernelContext.types.ts +173 -0
  54. package/src/kernel/KernelContext.zones.ts +54 -0
  55. package/src/kernel/SSEBridge.ts +7 -362
  56. package/src/kernel/SignalBus.ts +24 -148
  57. package/src/modules/debug/AuthCollector.ts +48 -1
  58. package/src/modules/debug/Collector.ts +16 -302
  59. package/src/modules/debug/DebugBridge.ts +10 -171
  60. package/src/modules/debug/DebugModule.ts +35 -5
  61. package/src/modules/debug/EntitiesCollector.ts +97 -1
  62. package/src/modules/debug/ErrorCollector.ts +2 -77
  63. package/src/modules/debug/I18nCollector.ts +9 -0
  64. package/src/modules/debug/LocalStorageAdapter.ts +3 -147
  65. package/src/modules/debug/RouterCollector.ts +101 -1
  66. package/src/modules/debug/SignalCollector.ts +2 -150
  67. package/src/modules/debug/ToastCollector.ts +2 -91
  68. package/src/modules/debug/ZonesCollector.ts +93 -1
  69. package/src/modules/debug/components/DebugBar.vue +19 -775
  70. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  71. package/src/modules/debug/components/index.ts +4 -3
  72. package/src/modules/debug/components/panels/AuthPanel.vue +1 -1
  73. package/src/modules/debug/components/panels/EntitiesPanel.vue +5 -5
  74. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  75. package/src/modules/debug/components/panels/RouterPanel.vue +1 -1
  76. package/src/modules/debug/components/panels/index.ts +10 -4
  77. package/src/modules/debug/index.ts +15 -0
  78. package/src/modules/debug/styles.scss +22 -18
  79. package/src/utils/index.ts +0 -3
  80. package/src/vite/qdadmDebugPlugin.ts +401 -0
  81. package/src/vite-env.d.ts +16 -0
  82. package/src/modules/debug/components/ObjectTree.vue +0 -123
  83. package/src/modules/debug/components/panels/EntriesPanel.vue +0 -100
  84. package/src/modules/debug/components/panels/SignalsPanel.vue +0 -188
  85. package/src/modules/debug/components/panels/ToastsPanel.vue +0 -45
  86. package/src/utils/debugInjector.ts +0 -306
@@ -9,7 +9,7 @@
9
9
  * - All registered routes
10
10
  */
11
11
  import { ref, computed, watch } from 'vue'
12
- import ObjectTree from '../ObjectTree.vue'
12
+ import { ObjectTree } from '@quazardous/qddebug'
13
13
 
14
14
  interface RouteLocation {
15
15
  name?: string
@@ -1,10 +1,16 @@
1
1
  /**
2
- * Debug Panel Components
2
+ * Debug Panel Components — qdadm.
3
+ *
4
+ * Admin-specific panels live here; generic panels (Entries, Signals, Toasts)
5
+ * are re-exported from `@quazardous/qddebug` to preserve qdadm's public API.
3
6
  */
7
+
8
+ // Admin-specific panels
4
9
  export { default as ZonesPanel } from './ZonesPanel.vue'
5
10
  export { default as AuthPanel } from './AuthPanel.vue'
6
11
  export { default as EntitiesPanel } from './EntitiesPanel.vue'
7
- export { default as ToastsPanel } from './ToastsPanel.vue'
8
- export { default as EntriesPanel } from './EntriesPanel.vue'
9
- export { default as SignalsPanel } from './SignalsPanel.vue'
10
12
  export { default as RouterPanel } from './RouterPanel.vue'
13
+ export { default as I18nPanel } from './I18nPanel.vue'
14
+
15
+ // Generic panels — re-exported from qddebug (single source of truth)
16
+ export { EntriesPanel, SignalsPanel, ToastsPanel } from '@quazardous/qddebug'
@@ -27,6 +27,12 @@ export {
27
27
  type StatsInfo
28
28
  } from './EntitiesCollector'
29
29
  export { RouterCollector, type NavigationEntry, type RouterCollectorOptions } from './RouterCollector'
30
+ export {
31
+ I18nCollector,
32
+ type MissingKeyEntry,
33
+ type LocaleChange,
34
+ type I18nCollectorOptions
35
+ } from './I18nCollector'
30
36
  export { LocalStorageAdapter, createLocalStorageAdapter } from './LocalStorageAdapter'
31
37
 
32
38
  // Module System v2 integration
@@ -41,3 +47,12 @@ export const debugBar = {
41
47
  module: _DebugModule,
42
48
  component: _QdadmDebugBar
43
49
  }
50
+
51
+ // Admin-specific panels + tab metadata, exposed for hosts that own a
52
+ // shared bridge and render a single `<DebugBar />` covering both
53
+ // qdadm and host-side (e.g. qdcms) collectors. Merge with your own
54
+ // panels and pass to `@quazardous/qddebug`'s `<DebugBar />`.
55
+ export {
56
+ adminPanels,
57
+ adminPanelsMeta,
58
+ } from './components/adminPanelsConfig'
@@ -246,7 +246,7 @@ $debug-info: #3b82f6;
246
246
  .debug-right .debug-tab-label {
247
247
  display: none;
248
248
  }
249
- .debug-right .debug-tab .p-badge {
249
+ .debug-right .debug-tab .qd-badge {
250
250
  position: absolute;
251
251
  top: -2px;
252
252
  right: -2px;
@@ -335,7 +335,7 @@ $debug-info: #3b82f6;
335
335
  .debug-fullscreen .debug-tab-label {
336
336
  display: inline;
337
337
  }
338
- .debug-fullscreen .debug-tab .p-badge {
338
+ .debug-fullscreen .debug-tab .qd-badge {
339
339
  position: static;
340
340
  }
341
341
  .debug-fullscreen .debug-actions {
@@ -406,7 +406,7 @@ $debug-info: #3b82f6;
406
406
  .debug-mobile .debug-tab-label {
407
407
  display: none;
408
408
  }
409
- .debug-mobile .debug-tab .p-badge {
409
+ .debug-mobile .debug-tab .qd-badge {
410
410
  position: absolute;
411
411
  top: -2px;
412
412
  right: -2px;
@@ -1881,14 +1881,18 @@ $debug-info: #3b82f6;
1881
1881
  word-break: break-all;
1882
1882
  }
1883
1883
 
1884
- /* Breadcrumb list */
1885
- .breadcrumb-list {
1884
+ /* Router-panel breadcrumb list — namespaced under .router-panel because
1885
+ `.breadcrumb-list` already exists globally for the AppLayout breadcrumb
1886
+ in styles/_main.scss (align-items: center, gap, etc.) and would otherwise
1887
+ clobber these rules. */
1888
+ .router-panel .breadcrumb-list {
1886
1889
  display: flex;
1887
1890
  flex-direction: column;
1891
+ align-items: stretch;
1888
1892
  gap: 4px;
1889
1893
  }
1890
1894
 
1891
- .breadcrumb-entry {
1895
+ .router-panel .breadcrumb-entry {
1892
1896
  padding: 6px 8px;
1893
1897
  background: #27272a;
1894
1898
  border-radius: 4px;
@@ -1896,13 +1900,13 @@ $debug-info: #3b82f6;
1896
1900
  border-left: 2px solid #3f3f46;
1897
1901
  }
1898
1902
 
1899
- .breadcrumb-entry-header {
1903
+ .router-panel .breadcrumb-entry-header {
1900
1904
  display: flex;
1901
1905
  align-items: center;
1902
1906
  gap: 8px;
1903
1907
  }
1904
1908
 
1905
- .breadcrumb-index {
1909
+ .router-panel .breadcrumb-index {
1906
1910
  width: 16px;
1907
1911
  height: 16px;
1908
1912
  display: flex;
@@ -1914,12 +1918,12 @@ $debug-info: #3b82f6;
1914
1918
  color: #a1a1aa;
1915
1919
  }
1916
1920
 
1917
- .breadcrumb-kind {
1921
+ .router-panel .breadcrumb-kind {
1918
1922
  font-weight: 500;
1919
1923
  color: #f4f4f5;
1920
1924
  }
1921
1925
 
1922
- .breadcrumb-entry-props {
1926
+ .router-panel .breadcrumb-entry-props {
1923
1927
  display: flex;
1924
1928
  flex-wrap: wrap;
1925
1929
  gap: 12px;
@@ -1927,12 +1931,12 @@ $debug-info: #3b82f6;
1927
1931
  margin-top: 4px;
1928
1932
  }
1929
1933
 
1930
- .breadcrumb-prop {
1934
+ .router-panel .breadcrumb-prop {
1931
1935
  color: #71717a;
1932
1936
  font-size: 10px;
1933
1937
  }
1934
1938
 
1935
- .breadcrumb-prop code {
1939
+ .router-panel .breadcrumb-prop code {
1936
1940
  color: #60a5fa;
1937
1941
  background: #18181b;
1938
1942
  padding: 1px 4px;
@@ -1940,27 +1944,27 @@ $debug-info: #3b82f6;
1940
1944
  }
1941
1945
 
1942
1946
  /* Kind-based colors */
1943
- .breadcrumb-route {
1947
+ .router-panel .breadcrumb-route {
1944
1948
  border-left-color: #71717a;
1945
1949
  }
1946
1950
 
1947
- .breadcrumb-entity-list {
1951
+ .router-panel .breadcrumb-entity-list {
1948
1952
  border-left-color: #3b82f6;
1949
1953
  }
1950
1954
 
1951
- .breadcrumb-entity-show {
1955
+ .router-panel .breadcrumb-entity-show {
1952
1956
  border-left-color: #10b981;
1953
1957
  }
1954
1958
 
1955
- .breadcrumb-entity-edit {
1959
+ .router-panel .breadcrumb-entity-edit {
1956
1960
  border-left-color: #f59e0b;
1957
1961
  }
1958
1962
 
1959
- .breadcrumb-entity-create {
1963
+ .router-panel .breadcrumb-entity-create {
1960
1964
  border-left-color: #8b5cf6;
1961
1965
  }
1962
1966
 
1963
- .breadcrumb-entity-delete {
1967
+ .router-panel .breadcrumb-entity-delete {
1964
1968
  border-left-color: #ef4444;
1965
1969
  }
1966
1970
 
@@ -33,6 +33,3 @@ export {
33
33
 
34
34
  // Type exports
35
35
  export type { KeyValuePair, LanguageObject } from './transformers'
36
-
37
- // Debug Injector
38
- export { default as debugInjector, DebugInjector } from './debugInjector'
@@ -0,0 +1,401 @@
1
+ /**
2
+ * qdadm Vite dev plugin — exposes the running app's DebugBridge over HTTP.
3
+ *
4
+ * Endpoints (default prefix `/__qdadm`):
5
+ * GET /__qdadm/ — index of endpoints + cache stats
6
+ * GET /__qdadm/sessions — list of connected client sessions
7
+ * GET /__qdadm/describe.json — bridge.describe() (manifests)
8
+ * GET /__qdadm/snapshot.json — bridge.dump() (live state)
9
+ * POST /__qdadm/call — body {collector, action, args?}
10
+ *
11
+ * Session selection: every browser tab that loads the app gets a fresh
12
+ * session id (uuid in the injected client). All endpoints accept a
13
+ * `?session=<id|latest>` query param. Default is `latest` (the most recently
14
+ * active session).
15
+ *
16
+ * Wire-up: the plugin injects a small ESM script via `transformIndexHtml`.
17
+ * That inline script lives in Vite's HMR pipeline (so `import.meta.hot`
18
+ * works), polls for `window.__qdadm.debug.bridge`, then pushes describe +
19
+ * snapshot on every reactive tick. The plugin caches per-session payloads
20
+ * in memory; GETs are served from cache (no round-trip needed).
21
+ *
22
+ * `call` is always request-response — it mutates state in the live page.
23
+ *
24
+ * Cache responses include `X-Qdadm-Stale-Ms`, `X-Qdadm-Source: cache|live`,
25
+ * and `X-Qdadm-Session: <id>` headers.
26
+ */
27
+
28
+ import type { Plugin, ViteDevServer } from 'vite'
29
+ import type { IncomingMessage, ServerResponse } from 'node:http'
30
+
31
+ interface PendingRequest {
32
+ resolve: (data: unknown) => void
33
+ reject: (err: Error) => void
34
+ timeout: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ interface DebugRequest {
38
+ id: string
39
+ type: 'describe' | 'dump' | 'call'
40
+ payload?: unknown
41
+ /** Target a specific session (server → client). */
42
+ sessionId?: string
43
+ }
44
+
45
+ interface DebugReply {
46
+ id: string
47
+ ok: boolean
48
+ data?: unknown
49
+ error?: string
50
+ sessionId?: string
51
+ }
52
+
53
+ interface DebugPush {
54
+ type: 'describe' | 'snapshot' | 'hello' | 'bye'
55
+ data?: unknown
56
+ sessionId: string
57
+ /** Free-form metadata sent on hello (user agent, route, etc.). */
58
+ meta?: Record<string, unknown>
59
+ }
60
+
61
+ interface SessionState {
62
+ id: string
63
+ firstSeenAt: number
64
+ lastSeenAt: number
65
+ describe: { data: unknown; at: number } | null
66
+ snapshot: { data: unknown; at: number } | null
67
+ meta: Record<string, unknown>
68
+ }
69
+
70
+ export interface QdadmDebugPluginOptions {
71
+ /** URL prefix (default '/__qdadm') */
72
+ prefix?: string
73
+ /** Request timeout ms for live fallback (default 3000) */
74
+ timeoutMs?: number
75
+ /** Drop sessions inactive for this many ms (default 10 min) */
76
+ sessionTtlMs?: number
77
+ }
78
+
79
+ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin {
80
+ const prefix = options.prefix ?? '/__qdadm'
81
+ const timeoutMs = options.timeoutMs ?? 3000
82
+ const sessionTtlMs = options.sessionTtlMs ?? 10 * 60 * 1000
83
+
84
+ const sessions = new Map<string, SessionState>()
85
+ const pending = new Map<string, PendingRequest>()
86
+ let server: ViteDevServer | null = null
87
+ let nextId = 1
88
+
89
+ function pruneStaleSessions(): void {
90
+ const cutoff = Date.now() - sessionTtlMs
91
+ for (const [id, s] of sessions) {
92
+ if (s.lastSeenAt < cutoff) sessions.delete(id)
93
+ }
94
+ }
95
+
96
+ function pickSession(sessionParam: string | null): SessionState | null {
97
+ pruneStaleSessions()
98
+ if (!sessionParam || sessionParam === 'latest') {
99
+ let best: SessionState | null = null
100
+ for (const s of sessions.values()) {
101
+ if (!best || s.lastSeenAt > best.lastSeenAt) best = s
102
+ }
103
+ return best
104
+ }
105
+ return sessions.get(sessionParam) ?? null
106
+ }
107
+
108
+ function ask(
109
+ type: DebugRequest['type'],
110
+ payload?: unknown,
111
+ sessionId?: string
112
+ ): Promise<unknown> {
113
+ if (!server) return Promise.reject(new Error('[qdadm-debug] vite server not ready'))
114
+ const id = String(nextId++)
115
+ return new Promise((resolve, reject) => {
116
+ const timeout = setTimeout(() => {
117
+ pending.delete(id)
118
+ reject(
119
+ new Error(
120
+ `[qdadm-debug] timeout waiting for ${type} — open the app in a browser at least once`
121
+ )
122
+ )
123
+ }, timeoutMs)
124
+ pending.set(id, { resolve, reject, timeout })
125
+ server!.ws.send('qdadm:debug:request', { id, type, payload, sessionId } satisfies DebugRequest)
126
+ })
127
+ }
128
+
129
+ async function serveCachedOrLive(
130
+ res: ServerResponse,
131
+ sessionParam: string | null,
132
+ field: 'describe' | 'snapshot',
133
+ liveType: 'describe' | 'dump'
134
+ ): Promise<void> {
135
+ const session = pickSession(sessionParam)
136
+ if (session) {
137
+ const cache = session[field]
138
+ if (cache) {
139
+ res.setHeader('x-qdadm-stale-ms', String(Date.now() - cache.at))
140
+ res.setHeader('x-qdadm-source', 'cache')
141
+ res.setHeader('x-qdadm-session', session.id)
142
+ return sendJson(res, 200, cache.data)
143
+ }
144
+ }
145
+ try {
146
+ const data = await ask(liveType, undefined, session?.id)
147
+ res.setHeader('x-qdadm-stale-ms', '0')
148
+ res.setHeader('x-qdadm-source', 'live')
149
+ if (session) res.setHeader('x-qdadm-session', session.id)
150
+ return sendJson(res, 200, data)
151
+ } catch (e) {
152
+ return sendJson(res, 503, {
153
+ error: (e as Error).message,
154
+ sessions: listSessions(),
155
+ hint:
156
+ sessions.size === 0
157
+ ? 'No connected client. Load http://<dev>/ in a browser once.'
158
+ : 'No cached payload yet for the chosen session — try ?session=latest.',
159
+ })
160
+ }
161
+ }
162
+
163
+ function listSessions(): Array<{
164
+ id: string
165
+ firstSeenAt: number
166
+ lastSeenAt: number
167
+ ageMs: number
168
+ hasDescribe: boolean
169
+ hasSnapshot: boolean
170
+ meta: Record<string, unknown>
171
+ }> {
172
+ pruneStaleSessions()
173
+ const now = Date.now()
174
+ return Array.from(sessions.values())
175
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt)
176
+ .map((s) => ({
177
+ id: s.id,
178
+ firstSeenAt: s.firstSeenAt,
179
+ lastSeenAt: s.lastSeenAt,
180
+ ageMs: now - s.lastSeenAt,
181
+ hasDescribe: !!s.describe,
182
+ hasSnapshot: !!s.snapshot,
183
+ meta: s.meta,
184
+ }))
185
+ }
186
+
187
+ function ensureSession(id: string, meta?: Record<string, unknown>): SessionState {
188
+ let s = sessions.get(id)
189
+ if (!s) {
190
+ s = {
191
+ id,
192
+ firstSeenAt: Date.now(),
193
+ lastSeenAt: Date.now(),
194
+ describe: null,
195
+ snapshot: null,
196
+ meta: meta ?? {},
197
+ }
198
+ sessions.set(id, s)
199
+ } else {
200
+ s.lastSeenAt = Date.now()
201
+ if (meta) s.meta = { ...s.meta, ...meta }
202
+ }
203
+ return s
204
+ }
205
+
206
+ return {
207
+ name: 'qdadm-debug',
208
+ apply: 'serve',
209
+
210
+ transformIndexHtml: {
211
+ order: 'pre',
212
+ handler() {
213
+ return [
214
+ {
215
+ tag: 'script',
216
+ attrs: { type: 'module' },
217
+ injectTo: 'head' as const,
218
+ children: HMR_CLIENT_SCRIPT,
219
+ },
220
+ ]
221
+ },
222
+ },
223
+
224
+ configureServer(s) {
225
+ server = s
226
+
227
+ s.ws.on('qdadm:debug:reply', (data: DebugReply) => {
228
+ const p = pending.get(data.id)
229
+ if (!p) return
230
+ clearTimeout(p.timeout)
231
+ pending.delete(data.id)
232
+ if (data.ok) p.resolve(data.data)
233
+ else p.reject(new Error(data.error ?? '[qdadm-debug] unknown error'))
234
+ })
235
+
236
+ s.ws.on('qdadm:debug:push', (msg: DebugPush) => {
237
+ if (!msg?.sessionId) return
238
+ const session = ensureSession(msg.sessionId, msg.meta)
239
+ const at = Date.now()
240
+ if (msg.type === 'describe' && msg.data !== undefined) {
241
+ session.describe = { data: msg.data, at }
242
+ } else if (msg.type === 'snapshot' && msg.data !== undefined) {
243
+ session.snapshot = { data: msg.data, at }
244
+ } else if (msg.type === 'bye') {
245
+ sessions.delete(msg.sessionId)
246
+ }
247
+ })
248
+
249
+ s.middlewares.use(async (req, res, next) => {
250
+ if (!req.url || !req.url.startsWith(prefix)) return next()
251
+ const url = new URL(req.url, 'http://localhost')
252
+ const path = url.pathname.slice(prefix.length)
253
+ const sessionParam = url.searchParams.get('session')
254
+
255
+ try {
256
+ if (req.method === 'GET' && (path === '' || path === '/')) {
257
+ return sendJson(res, 200, {
258
+ version: '1',
259
+ description:
260
+ 'qdadm debug bridge — read collector manifests and snapshots, invoke actions.',
261
+ endpoints: {
262
+ 'GET /sessions': 'list connected client sessions',
263
+ 'GET /describe.json?session=<id|latest>':
264
+ 'manifest for every collector (entry shapes + actions)',
265
+ 'GET /snapshot.json?session=<id|latest>':
266
+ 'live JSON snapshot of every collector',
267
+ 'POST /call?session=<id|latest>':
268
+ 'invoke an action — body { collector, action, args? }',
269
+ },
270
+ sessions: listSessions(),
271
+ prefix,
272
+ })
273
+ }
274
+ if (req.method === 'GET' && path === '/sessions') {
275
+ return sendJson(res, 200, { sessions: listSessions() })
276
+ }
277
+ if (req.method === 'GET' && path === '/describe.json') {
278
+ return await serveCachedOrLive(res, sessionParam, 'describe', 'describe')
279
+ }
280
+ if (req.method === 'GET' && path === '/snapshot.json') {
281
+ return await serveCachedOrLive(res, sessionParam, 'snapshot', 'dump')
282
+ }
283
+ if (req.method === 'POST' && path === '/call') {
284
+ const body = await readBody(req)
285
+ const parsed = body ? JSON.parse(body) : {}
286
+ const session = pickSession(sessionParam)
287
+ if (!session) {
288
+ return sendJson(res, 503, {
289
+ error: 'no connected session',
290
+ sessions: listSessions(),
291
+ })
292
+ }
293
+ const data = await ask('call', parsed, session.id)
294
+ res.setHeader('x-qdadm-session', session.id)
295
+ return sendJson(res, 200, data)
296
+ }
297
+ return sendJson(res, 404, { error: `unknown ${req.method} ${path}` })
298
+ } catch (e) {
299
+ return sendJson(res, 500, { error: (e as Error).message })
300
+ }
301
+ })
302
+ },
303
+ }
304
+ }
305
+
306
+ export default qdadmDebugPlugin
307
+
308
+ function sendJson(res: ServerResponse, status: number, data: unknown): void {
309
+ res.statusCode = status
310
+ res.setHeader('content-type', 'application/json; charset=utf-8')
311
+ res.setHeader('cache-control', 'no-store')
312
+ res.end(JSON.stringify(data, null, 2))
313
+ }
314
+
315
+ function readBody(req: IncomingMessage): Promise<string> {
316
+ return new Promise((resolve, reject) => {
317
+ let buf = ''
318
+ req.setEncoding('utf-8')
319
+ req.on('data', (chunk: string) => {
320
+ buf += chunk
321
+ })
322
+ req.on('end', () => resolve(buf))
323
+ req.on('error', reject)
324
+ })
325
+ }
326
+
327
+ /**
328
+ * Client-side bridge — injected verbatim into index.html as an ES module.
329
+ * Lives in Vite's HMR pipeline (so `import.meta.hot` works here).
330
+ *
331
+ * Each tab gets its own session id (random uuid). Pushes describe + snapshot
332
+ * on every reactive tick of the bridge so the plugin's per-session cache
333
+ * stays fresh. Listens for ad-hoc requests targeted at this session.
334
+ */
335
+ const HMR_CLIENT_SCRIPT = String.raw`
336
+ const hot = import.meta.hot
337
+ if (!hot) {
338
+ console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
339
+ } else {
340
+ const sessionId = crypto.randomUUID()
341
+ const meta = { userAgent: navigator.userAgent, location: location.pathname }
342
+ let bridge = null
343
+ let lastTick = -1
344
+
345
+ const w = /** @type {any} */ (window)
346
+
347
+ function helloIfNeeded() {
348
+ try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
349
+ }
350
+ function pushDescribe() {
351
+ try { hot.send('qdadm:debug:push', { type: 'describe', sessionId, data: bridge.describe() }) }
352
+ catch (e) { console.warn('[qdadm-debug] describe push failed:', e) }
353
+ }
354
+ function pushSnapshot() {
355
+ try { hot.send('qdadm:debug:push', { type: 'snapshot', sessionId, data: bridge.dump() }) }
356
+ catch (e) { console.warn('[qdadm-debug] snapshot push failed:', e) }
357
+ }
358
+ function bye() {
359
+ try { hot.send('qdadm:debug:push', { type: 'bye', sessionId }) } catch (e) {}
360
+ }
361
+ window.addEventListener('beforeunload', bye)
362
+
363
+ hot.on('qdadm:debug:request', async (msg) => {
364
+ // Ignore requests addressed to a different session.
365
+ if (msg && msg.sessionId && msg.sessionId !== sessionId) return
366
+ const id = (msg && msg.id) || ''
367
+ try {
368
+ const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
369
+ if (!b) throw new Error('debug bridge not ready')
370
+ let data
371
+ if (msg.type === 'describe') data = b.describe()
372
+ else if (msg.type === 'dump') data = b.dump()
373
+ else if (msg.type === 'call') {
374
+ const p = msg.payload || {}
375
+ if (!p.collector || !p.action) throw new Error('call payload requires { collector, action, args? }')
376
+ data = await b.call(p.collector, p.action, p.args || {})
377
+ } else throw new Error('unknown request type "' + msg.type + '"')
378
+ hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId })
379
+ } catch (e) {
380
+ hot.send('qdadm:debug:reply', { id, ok: false, error: e && e.message ? e.message : String(e), sessionId })
381
+ }
382
+ })
383
+
384
+ let attachTimer = setInterval(() => {
385
+ bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
386
+ if (!bridge) return
387
+ clearInterval(attachTimer)
388
+ helloIfNeeded()
389
+ pushDescribe()
390
+ pushSnapshot()
391
+ lastTick = bridge.tick && bridge.tick.value
392
+ console.log('[qdadm-debug] HTTP bridge wired — session', sessionId.slice(0, 8))
393
+ setInterval(() => {
394
+ if (!bridge || !bridge.tick) return
395
+ if (bridge.tick.value === lastTick) return
396
+ lastTick = bridge.tick.value
397
+ pushSnapshot()
398
+ }, 250)
399
+ }, 100)
400
+ }
401
+ `
package/src/vite-env.d.ts CHANGED
@@ -10,3 +10,19 @@ declare module '*.vue' {
10
10
  const component: DefineComponent<object, object, unknown>
11
11
  export default component
12
12
  }
13
+
14
+ // `pluralize` ships as plain JS without bundled `.d.ts`. The official
15
+ // types live in `@types/pluralize` but pulling them in as a runtime
16
+ // dep here is overkill for a single-call usage. Declare the minimum
17
+ // surface we use so `strict` mode is happy without DefinitelyTyped.
18
+ declare module 'pluralize' {
19
+ interface PluralizeFn {
20
+ (word: string, count?: number, inclusive?: boolean): string
21
+ singular(word: string): string
22
+ plural(word: string, count?: number, inclusive?: boolean): string
23
+ isPlural(word: string): boolean
24
+ isSingular(word: string): boolean
25
+ }
26
+ const pluralize: PluralizeFn
27
+ export default pluralize
28
+ }