@quazardous/qdadm 2.15.0 → 2.16.1

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,462 @@
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
+ export function qdadmDebugPlugin(options = {}) {
28
+ const prefix = options.prefix ?? '/__qdadm';
29
+ const timeoutMs = options.timeoutMs ?? 3000;
30
+ const sessionTtlMs = options.sessionTtlMs ?? 10 * 60 * 1000;
31
+ const sessions = new Map();
32
+ const pending = new Map();
33
+ let server = null;
34
+ let nextId = 1;
35
+ function pruneStaleSessions() {
36
+ const cutoff = Date.now() - sessionTtlMs;
37
+ for (const [id, s] of sessions) {
38
+ if (s.lastSeenAt < cutoff)
39
+ sessions.delete(id);
40
+ }
41
+ }
42
+ function pickSession(sessionParam) {
43
+ pruneStaleSessions();
44
+ if (!sessionParam || sessionParam === 'latest') {
45
+ let best = null;
46
+ for (const s of sessions.values()) {
47
+ if (!best || s.lastSeenAt > best.lastSeenAt)
48
+ best = s;
49
+ }
50
+ return best;
51
+ }
52
+ return sessions.get(sessionParam) ?? null;
53
+ }
54
+ function ask(type, payload, sessionId) {
55
+ if (!server)
56
+ return Promise.reject(new Error('[qdadm-debug] vite server not ready'));
57
+ const id = String(nextId++);
58
+ return new Promise((resolve, reject) => {
59
+ const timeout = setTimeout(() => {
60
+ pending.delete(id);
61
+ reject(new Error(`[qdadm-debug] timeout waiting for ${type} — open the app in a browser at least once`));
62
+ }, timeoutMs);
63
+ pending.set(id, { resolve, reject, timeout });
64
+ server.ws.send('qdadm:debug:request', { id, type, payload, sessionId });
65
+ });
66
+ }
67
+ async function serveCachedOrLive(res, sessionParam, field, liveType) {
68
+ const session = pickSession(sessionParam);
69
+ if (session) {
70
+ const cache = session[field];
71
+ if (cache) {
72
+ res.setHeader('x-qdadm-stale-ms', String(Date.now() - cache.at));
73
+ res.setHeader('x-qdadm-source', 'cache');
74
+ res.setHeader('x-qdadm-session', session.id);
75
+ return sendJson(res, 200, cache.data);
76
+ }
77
+ }
78
+ try {
79
+ const data = await ask(liveType, undefined, session?.id);
80
+ res.setHeader('x-qdadm-stale-ms', '0');
81
+ res.setHeader('x-qdadm-source', 'live');
82
+ if (session)
83
+ res.setHeader('x-qdadm-session', session.id);
84
+ return sendJson(res, 200, data);
85
+ }
86
+ catch (e) {
87
+ return sendJson(res, 503, {
88
+ error: e.message,
89
+ sessions: listSessions(),
90
+ hint: sessions.size === 0
91
+ ? 'No connected client. Load http://<dev>/ in a browser once.'
92
+ : 'No cached payload yet for the chosen session — try ?session=latest.',
93
+ });
94
+ }
95
+ }
96
+ function listSessions() {
97
+ pruneStaleSessions();
98
+ const now = Date.now();
99
+ return Array.from(sessions.values())
100
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt)
101
+ .map((s) => ({
102
+ id: s.id,
103
+ firstSeenAt: s.firstSeenAt,
104
+ lastSeenAt: s.lastSeenAt,
105
+ ageMs: now - s.lastSeenAt,
106
+ hasDescribe: !!s.describe,
107
+ hasSnapshot: !!s.snapshot,
108
+ meta: s.meta,
109
+ }));
110
+ }
111
+ function ensureSession(id, meta) {
112
+ let s = sessions.get(id);
113
+ if (!s) {
114
+ s = {
115
+ id,
116
+ firstSeenAt: Date.now(),
117
+ lastSeenAt: Date.now(),
118
+ describe: null,
119
+ snapshot: null,
120
+ meta: meta ?? {},
121
+ };
122
+ sessions.set(id, s);
123
+ }
124
+ else {
125
+ s.lastSeenAt = Date.now();
126
+ if (meta)
127
+ s.meta = { ...s.meta, ...meta };
128
+ }
129
+ return s;
130
+ }
131
+ /** Inter-plugin API (#1398) — consumed by @quazardous/qdmcp. */
132
+ const api = {
133
+ ask,
134
+ pickSession: (sessionParam) => {
135
+ const s = pickSession(sessionParam);
136
+ return s ? { id: s.id, lastSeenAt: s.lastSeenAt, meta: s.meta } : null;
137
+ },
138
+ listSessions,
139
+ prefix,
140
+ };
141
+ return {
142
+ name: 'qdadm-debug',
143
+ apply: 'serve',
144
+ api,
145
+ transformIndexHtml: {
146
+ order: 'pre',
147
+ handler() {
148
+ return [
149
+ {
150
+ tag: 'script',
151
+ attrs: { type: 'module' },
152
+ injectTo: 'head',
153
+ children: HMR_CLIENT_SCRIPT,
154
+ },
155
+ ];
156
+ },
157
+ },
158
+ configureServer(s) {
159
+ server = s;
160
+ s.ws.on('qdadm:debug:reply', (data) => {
161
+ const p = pending.get(data.id);
162
+ if (!p)
163
+ return;
164
+ clearTimeout(p.timeout);
165
+ pending.delete(data.id);
166
+ if (data.ok)
167
+ p.resolve(data.data);
168
+ else
169
+ p.reject(new Error(data.error ?? '[qdadm-debug] unknown error'));
170
+ });
171
+ s.ws.on('qdadm:debug:push', (msg) => {
172
+ if (!msg?.sessionId)
173
+ return;
174
+ const session = ensureSession(msg.sessionId, msg.meta);
175
+ const at = Date.now();
176
+ if (msg.type === 'describe' && msg.data !== undefined) {
177
+ session.describe = { data: msg.data, at };
178
+ }
179
+ else if (msg.type === 'snapshot' && msg.data !== undefined) {
180
+ session.snapshot = { data: msg.data, at };
181
+ }
182
+ else if (msg.type === 'bye') {
183
+ sessions.delete(msg.sessionId);
184
+ }
185
+ });
186
+ s.middlewares.use(async (req, res, next) => {
187
+ if (!req.url || !req.url.startsWith(prefix))
188
+ return next();
189
+ const url = new URL(req.url, 'http://localhost');
190
+ const path = url.pathname.slice(prefix.length);
191
+ const sessionParam = url.searchParams.get('session');
192
+ try {
193
+ if (req.method === 'GET' && (path === '' || path === '/')) {
194
+ return sendJson(res, 200, {
195
+ version: '1',
196
+ description: 'qdadm debug bridge — read collector manifests and snapshots, invoke actions.',
197
+ endpoints: {
198
+ 'GET /sessions': 'list connected client sessions',
199
+ 'GET /describe.json?session=<id|latest>': 'manifest for every collector (entry shapes + actions)',
200
+ 'GET /snapshot.json?session=<id|latest>': 'live JSON snapshot of every collector',
201
+ 'POST /call?session=<id|latest>': 'invoke an action — body { collector, action, args? }',
202
+ },
203
+ sessions: listSessions(),
204
+ prefix,
205
+ });
206
+ }
207
+ if (req.method === 'GET' && path === '/sessions') {
208
+ return sendJson(res, 200, { sessions: listSessions() });
209
+ }
210
+ if (req.method === 'GET' && path === '/describe.json') {
211
+ return await serveCachedOrLive(res, sessionParam, 'describe', 'describe');
212
+ }
213
+ if (req.method === 'GET' && path === '/snapshot.json') {
214
+ return await serveCachedOrLive(res, sessionParam, 'snapshot', 'dump');
215
+ }
216
+ if (req.method === 'POST' && path === '/call') {
217
+ const body = await readBody(req);
218
+ const parsed = body ? JSON.parse(body) : {};
219
+ const session = pickSession(sessionParam);
220
+ if (!session) {
221
+ return sendJson(res, 503, {
222
+ error: 'no connected session',
223
+ sessions: listSessions(),
224
+ });
225
+ }
226
+ const data = await ask('call', parsed, session.id);
227
+ res.setHeader('x-qdadm-session', session.id);
228
+ return sendJson(res, 200, data);
229
+ }
230
+ // Unknown subpath: fall through — sibling plugins (e.g.
231
+ // @quazardous/qdadm-mcp) mount their own endpoints under the
232
+ // same prefix (#1398).
233
+ return next();
234
+ }
235
+ catch (e) {
236
+ return sendJson(res, 500, { error: e.message });
237
+ }
238
+ });
239
+ },
240
+ };
241
+ }
242
+ export default qdadmDebugPlugin;
243
+ function sendJson(res, status, data) {
244
+ res.statusCode = status;
245
+ res.setHeader('content-type', 'application/json; charset=utf-8');
246
+ res.setHeader('cache-control', 'no-store');
247
+ res.end(JSON.stringify(data, null, 2));
248
+ }
249
+ function readBody(req) {
250
+ return new Promise((resolve, reject) => {
251
+ let buf = '';
252
+ req.setEncoding('utf-8');
253
+ req.on('data', (chunk) => {
254
+ buf += chunk;
255
+ });
256
+ req.on('end', () => resolve(buf));
257
+ req.on('error', reject);
258
+ });
259
+ }
260
+ /**
261
+ * Client-side bridge — injected verbatim into index.html as an ES module.
262
+ * Lives in Vite's HMR pipeline (so `import.meta.hot` works here).
263
+ *
264
+ * Each tab gets its own session id (random uuid). Pushes describe + snapshot
265
+ * on every reactive tick of the bridge so the plugin's per-session cache
266
+ * stays fresh. Listens for ad-hoc requests targeted at this session.
267
+ */
268
+ const HMR_CLIENT_SCRIPT = String.raw `
269
+ // ── Boot capture (#1398): this head module runs BEFORE the app's entry
270
+ // module, so console/window errors from the very first app statement are
271
+ // buffered — visible over MCP even when the app dies before the bridge
272
+ // exists (the class of failure a bridge-only design can never see).
273
+ const __bootAt = Date.now()
274
+ const __bootlog = { errors: [], warns: [], pageErrors: [], rejections: [] }
275
+ function __cap(arr, entry) { arr.push(entry); if (arr.length > 200) arr.shift() }
276
+ function __fmt(args) {
277
+ try {
278
+ return Array.from(args).map((a) => {
279
+ if (a instanceof Error) return a.stack || a.message
280
+ if (typeof a === 'object') { try { return JSON.stringify(a) } catch (e) { return String(a) } }
281
+ return String(a)
282
+ }).join(' ').slice(0, 2000)
283
+ } catch (e) { return '[unformattable]' }
284
+ }
285
+ const __origError = console.error.bind(console)
286
+ const __origWarn = console.warn.bind(console)
287
+ console.error = function () { __cap(__bootlog.errors, { at: Date.now(), msg: __fmt(arguments) }); __origError.apply(null, arguments) }
288
+ console.warn = function () { __cap(__bootlog.warns, { at: Date.now(), msg: __fmt(arguments) }); __origWarn.apply(null, arguments) }
289
+ window.addEventListener('error', (e) => {
290
+ __cap(__bootlog.pageErrors, { at: Date.now(), msg: String(e.message || e), source: e.filename ? e.filename + ':' + e.lineno : undefined })
291
+ })
292
+ window.addEventListener('unhandledrejection', (e) => {
293
+ let msg; try { msg = e.reason && (e.reason.stack || e.reason.message) || String(e.reason) } catch (err) { msg = '[unreadable reason]' }
294
+ __cap(__bootlog.rejections, { at: Date.now(), msg: String(msg).slice(0, 2000) })
295
+ })
296
+
297
+ const hot = import.meta.hot
298
+ if (!hot) {
299
+ console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
300
+ } else {
301
+ const sessionId = crypto.randomUUID()
302
+ const meta = { userAgent: navigator.userAgent, location: location.pathname }
303
+ let bridge = null
304
+ let lastTick = -1
305
+
306
+ const w = /** @type {any} */ (window)
307
+
308
+ // Signal ring buffer — armed once __qdadm.signals is reachable.
309
+ const __signals = []
310
+ let __signalsArmed = false
311
+ function __armSignals() {
312
+ if (__signalsArmed) return
313
+ const q = w.__qdadm
314
+ if (!q || !q.signals || typeof q.signals.on !== 'function') return
315
+ try {
316
+ q.signals.on('**', (event) => { __cap(__signals, { at: Date.now(), name: event && event.name }) ; if (__signals.length > 100) __signals.shift() })
317
+ __signalsArmed = true
318
+ } catch (e) {}
319
+ }
320
+
321
+ function __sessionInfo() {
322
+ const q = w.__qdadm || {}
323
+ let app = {}
324
+ try { app = (q.kernel && q.kernel.options && q.kernel.options.app) || {} } catch (e) {}
325
+ let route = null
326
+ try { const r = q.router && q.router.currentRoute && q.router.currentRoute.value; route = r ? { name: r.name, fullPath: r.fullPath } : null } catch (e) {}
327
+ return {
328
+ sessionId, bootAt: __bootAt, now: Date.now(), url: location.href,
329
+ app: { name: app.name, version: typeof app.version === 'function' ? app.version() : app.version },
330
+ route, bridgeReady: !!bridge, signalsArmed: __signalsArmed,
331
+ bootlogCounts: { errors: __bootlog.errors.length, warns: __bootlog.warns.length, pageErrors: __bootlog.pageErrors.length, rejections: __bootlog.rejections.length },
332
+ }
333
+ }
334
+
335
+ function __routes() {
336
+ const q = w.__qdadm
337
+ if (!q || !q.router) throw new Error('router not ready')
338
+ return q.router.getRoutes().map((r) => ({
339
+ name: r.name, path: r.path,
340
+ meta: { entity: r.meta && r.meta.entity, layout: r.meta && r.meta.layout, requiresAuth: r.meta && r.meta.requiresAuth, public: r.meta && r.meta.public },
341
+ }))
342
+ }
343
+
344
+ function __manager(entity) {
345
+ const q = w.__qdadm
346
+ if (!q || !q.orchestrator) throw new Error('orchestrator not ready')
347
+ if (!q.orchestrator.isRegistered(entity)) throw new Error('entity not registered: ' + entity + ' (known: ' + q.orchestrator.getRegisteredNames().join(', ') + ')')
348
+ return q.orchestrator.get(entity)
349
+ }
350
+
351
+ function __entityState(payload) {
352
+ const q = w.__qdadm
353
+ if (!payload || !payload.entity) {
354
+ return { entities: q && q.orchestrator ? q.orchestrator.getRegisteredNames() : [] }
355
+ }
356
+ const m = __manager(payload.entity)
357
+ const safe = (fn) => { try { return fn() } catch (e) { return '[error: ' + (e && e.message) + ']' } }
358
+ const fields = {}
359
+ try {
360
+ const fs = m.fields || m._fields || {}
361
+ for (const k in fs) fields[k] = { type: fs[k] && fs[k].type, label: fs[k] && fs[k].label, required: !!(fs[k] && fs[k].required) }
362
+ } catch (e) {}
363
+ return {
364
+ entity: payload.entity,
365
+ idField: safe(() => m.idField), labelField: safe(() => m.labelField),
366
+ label: safe(() => m.label), labelPlural: safe(() => m.labelPlural),
367
+ can: { create: safe(() => m.canCreate()), read: safe(() => m.canRead()), update: safe(() => m.canUpdate()), delete: safe(() => m.canDelete()) },
368
+ fields,
369
+ storage: safe(() => { const st = m.storage || m._storage; return st ? { kind: st.constructor && st.constructor.name, storageKey: st._storageKey } : null }),
370
+ }
371
+ }
372
+
373
+ async function __entityCall(payload) {
374
+ if (!payload || !payload.entity || !payload.op) throw new Error('entityCall requires { entity, op, ... }')
375
+ const m = __manager(payload.entity)
376
+ const op = payload.op
377
+ if (op === 'list') return await m.list(payload.params || {})
378
+ if (op === 'get') return await m.get(payload.id)
379
+ if (op === 'create') return await m.create(payload.data || {})
380
+ if (op === 'update') return await m.update(payload.id, payload.data || {})
381
+ if (op === 'delete') { await m.delete(payload.id); return { deleted: payload.id } }
382
+ throw new Error('unknown op "' + op + '" (list|get|create|update|delete)')
383
+ }
384
+
385
+ function __storageDump(payload) {
386
+ if (!payload || !payload.entity) throw new Error('storageDump requires { entity }')
387
+ const m = __manager(payload.entity)
388
+ const st = m.storage || m._storage
389
+ const key = st && st._storageKey
390
+ if (!key) return { storageKey: null, note: 'storage exposes no localStorage key (' + (st && st.constructor && st.constructor.name) + ')' }
391
+ const raw = localStorage.getItem(key)
392
+ let parsed = null
393
+ try { parsed = raw ? JSON.parse(raw) : null } catch (e) { parsed = '[unparsable]' }
394
+ return { storageKey: key, count: Array.isArray(parsed) ? parsed.length : null, data: parsed }
395
+ }
396
+
397
+ function helloIfNeeded() {
398
+ try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
399
+ }
400
+ function pushDescribe() {
401
+ try { hot.send('qdadm:debug:push', { type: 'describe', sessionId, data: bridge.describe() }) }
402
+ catch (e) { console.warn('[qdadm-debug] describe push failed:', e) }
403
+ }
404
+ function pushSnapshot() {
405
+ try { hot.send('qdadm:debug:push', { type: 'snapshot', sessionId, data: bridge.dump() }) }
406
+ catch (e) { console.warn('[qdadm-debug] snapshot push failed:', e) }
407
+ }
408
+ function bye() {
409
+ try { hot.send('qdadm:debug:push', { type: 'bye', sessionId }) } catch (e) {}
410
+ }
411
+ window.addEventListener('beforeunload', bye)
412
+
413
+ hot.on('qdadm:debug:request', async (msg) => {
414
+ // Ignore requests addressed to a different session.
415
+ if (msg && msg.sessionId && msg.sessionId !== sessionId) return
416
+ const id = (msg && msg.id) || ''
417
+ try {
418
+ // Typed queries (#1398): bridge-independent — they must answer even
419
+ // when the app died before the bridge existed.
420
+ if (msg.type === 'bootlog') { hot.send('qdadm:debug:reply', { id, ok: true, data: __bootlog, sessionId }); return }
421
+ if (msg.type === 'sessionInfo') { hot.send('qdadm:debug:reply', { id, ok: true, data: __sessionInfo(), sessionId }); return }
422
+ if (msg.type === 'recentSignals') { __armSignals(); hot.send('qdadm:debug:reply', { id, ok: true, data: { armed: __signalsArmed, events: __signals }, sessionId }); return }
423
+ if (msg.type === 'routes') { hot.send('qdadm:debug:reply', { id, ok: true, data: __routes(), sessionId }); return }
424
+ if (msg.type === 'entityState') { hot.send('qdadm:debug:reply', { id, ok: true, data: __entityState(msg.payload), sessionId }); return }
425
+ if (msg.type === 'entityCall') { const data = await __entityCall(msg.payload); hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId }); return }
426
+ if (msg.type === 'storageDump') { hot.send('qdadm:debug:reply', { id, ok: true, data: __storageDump(msg.payload), sessionId }); return }
427
+
428
+ const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
429
+ if (!b) throw new Error('debug bridge not ready')
430
+ let data
431
+ if (msg.type === 'describe') data = b.describe()
432
+ else if (msg.type === 'dump') data = b.dump()
433
+ else if (msg.type === 'call') {
434
+ const p = msg.payload || {}
435
+ if (!p.collector || !p.action) throw new Error('call payload requires { collector, action, args? }')
436
+ data = await b.call(p.collector, p.action, p.args || {})
437
+ } else throw new Error('unknown request type "' + msg.type + '"')
438
+ hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId })
439
+ } catch (e) {
440
+ hot.send('qdadm:debug:reply', { id, ok: false, error: e && e.message ? e.message : String(e), sessionId })
441
+ }
442
+ })
443
+
444
+ let attachTimer = setInterval(() => {
445
+ bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
446
+ if (!bridge) return
447
+ clearInterval(attachTimer)
448
+ __armSignals()
449
+ helloIfNeeded()
450
+ pushDescribe()
451
+ pushSnapshot()
452
+ lastTick = bridge.tick && bridge.tick.value
453
+ console.log('[qdadm-debug] HTTP bridge wired — session', sessionId.slice(0, 8))
454
+ setInterval(() => {
455
+ if (!bridge || !bridge.tick) return
456
+ if (bridge.tick.value === lastTick) return
457
+ lastTick = bridge.tick.value
458
+ pushSnapshot()
459
+ }, 250)
460
+ }, 100)
461
+ }
462
+ `;
@@ -0,0 +1,29 @@
1
+ import { type Plugin } from 'vite';
2
+ export interface QdadmVitePluginOptions {
3
+ /**
4
+ * Extra packages to dedupe on top of the defaults
5
+ * (vue, vue-router, primevue, pinia).
6
+ */
7
+ dedupe?: string[];
8
+ /**
9
+ * PrimeVue "flavor" (#1393): alias every `primevue/*` import to a
10
+ * compatible fork (e.g. OpenVue). qdadm officially maintains and tests
11
+ * `primevue@4` (MIT) only — a flavor keeping the v4 module layout/API is
12
+ * drop-in; divergences are the consumer's to absorb.
13
+ *
14
+ * ```ts
15
+ * qdadmVitePlugin({ primevue: { package: 'openvue' } })
16
+ * ```
17
+ */
18
+ primevue?: {
19
+ /** Package that replaces `primevue` (e.g. 'openvue'). */
20
+ package: string;
21
+ /**
22
+ * Optional package replacing `@primeuix/themes` — only needed if the
23
+ * flavor ships its own themes fork (none does today; OpenVue keeps the
24
+ * MIT @primeuix/* versions).
25
+ */
26
+ themes?: string;
27
+ };
28
+ }
29
+ export declare function qdadmVitePlugin(options?: QdadmVitePluginOptions): Plugin;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * qdadm Vite consumer plugin (#1385)
3
+ *
4
+ * qdadm publishes raw TS/.vue sources. Without help, Vite pre-bundles the
5
+ * host app's `primevue` import into `.vite/deps` while qdadm's raw sources
6
+ * import `node_modules/primevue/...` directly — two module instances whose
7
+ * injection symbols don't match, and the app dies at boot with
8
+ * `Error: No PrimeVue Toast provided!`.
9
+ *
10
+ * This plugin applies the required config in one line:
11
+ *
12
+ * ```ts
13
+ * // vite.config.ts
14
+ * import { qdadmVitePlugin } from '@quazardous/qdadm/vite'
15
+ *
16
+ * export default defineConfig({
17
+ * plugins: [vue(), qdadmVitePlugin()],
18
+ * })
19
+ * ```
20
+ *
21
+ * - `resolve.dedupe` collapses duplicate peer copies (also covers the
22
+ * `file:`/workspace-link scenario described in the package README).
23
+ * - `optimizeDeps.exclude` keeps primevue/@primeuix/themes/qdadm out of the
24
+ * pre-bundle so BOTH pipelines resolve the same raw modules. Excluding
25
+ * qdadm also keeps HMR sane when the package is consumed via a symlink.
26
+ * - qdadm has NO CJS transitives (pluralize is vendored as ESM, #1454), so
27
+ * there is nothing to `optimizeDeps.include` — which also means nothing
28
+ * for the optimizer to fail resolving in file:-linked installs where
29
+ * npm (install-links=false) never materializes qdadm's deps at the
30
+ * consumer root.
31
+ * - When `node_modules/@quazardous/qdadm` is a symlink (file:/workspace
32
+ * link — qdadm's own dev mode against a testbed app), its realpath is
33
+ * appended to `server.fs.allow` so qdadm's `?raw` dynamic imports (i18n
34
+ * YAML defaults) are servable from outside the consumer's workspace
35
+ * root. The workspace root is re-added explicitly because a populated
36
+ * `fs.allow` disables Vite's implicit default.
37
+ *
38
+ * Returned partial config is deep-merged by Vite (arrays concatenate), so
39
+ * consumer overrides in their own `defineConfig` still apply.
40
+ */
41
+ import fs from 'node:fs';
42
+ import path from 'node:path';
43
+ import { searchForWorkspaceRoot } from 'vite';
44
+ export function qdadmVitePlugin(options = {}) {
45
+ return {
46
+ name: 'qdadm',
47
+ config: (userConfig = {}) => {
48
+ const root = path.resolve(userConfig.root ?? process.cwd());
49
+ // Symlinked install (file:/workspace link)? Allow serving from the
50
+ // link target, which typically lives outside the consumer's root.
51
+ const fsAllow = [];
52
+ try {
53
+ const pkgDir = path.join(root, 'node_modules', '@quazardous', 'qdadm');
54
+ const real = fs.realpathSync(pkgDir);
55
+ if (real !== pkgDir) {
56
+ fsAllow.push(searchForWorkspaceRoot(root), real);
57
+ }
58
+ }
59
+ catch {
60
+ // not installed under root node_modules (aliased/hoisted) — nothing to allow
61
+ }
62
+ // Flavor aliasing (#1393): after the alias, no `primevue`/themes ids
63
+ // remain in the graph — dedupe/exclude lists must name the flavor.
64
+ const flavor = options.primevue ?? null;
65
+ const widgetPkg = flavor?.package ?? 'primevue';
66
+ const themesPkg = flavor?.themes ?? '@primeuix/themes';
67
+ const alias = [];
68
+ if (flavor) {
69
+ alias.push({ find: /^primevue(\/|$)/, replacement: `${flavor.package}$1` });
70
+ if (flavor.themes) {
71
+ alias.push({ find: /^@primeuix\/themes(\/|$)/, replacement: `${flavor.themes}$1` });
72
+ }
73
+ }
74
+ // With a flavor, the original packages may still be installed — keep
75
+ // them excluded too, or the optimizer pre-bundles a stray
76
+ // `primevue/*` id that bypassed the alias (split-instance again).
77
+ const widgetExcludes = flavor ? [widgetPkg, 'primevue'] : [widgetPkg];
78
+ const themesExcludes = flavor?.themes ? [themesPkg, '@primeuix/themes'] : [themesPkg];
79
+ return {
80
+ resolve: {
81
+ ...(alias.length > 0 ? { alias } : {}),
82
+ dedupe: ['vue', 'vue-router', widgetPkg, 'pinia', ...(options.dedupe ?? [])],
83
+ },
84
+ optimizeDeps: {
85
+ exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
86
+ },
87
+ ...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
88
+ };
89
+ },
90
+ };
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -23,7 +23,9 @@
23
23
  "lint": "eslint src/",
24
24
  "lint:fix": "eslint src/ --fix",
25
25
  "format": "prettier --write src/",
26
- "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types"
26
+ "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types",
27
+ "build:node": "tsc -p tsconfig.node.json",
28
+ "prepack": "npm run build:node"
27
29
  },
28
30
  "exports": {
29
31
  ".": "./src/index.ts",
@@ -41,18 +43,19 @@
41
43
  },
42
44
  "./styles/variables": "./src/styles/_variables.scss",
43
45
  "./gen": "./src/gen/index.ts",
44
- "./gen/vite-plugin": "./src/gen/vite-plugin.ts",
45
- "./vite": "./src/vite/qdadmPlugin.ts",
46
- "./vite-plugin-debug": "./src/vite/qdadmDebugPlugin.ts"
46
+ "./gen/vite-plugin": "./dist/node/gen/vite-plugin.js",
47
+ "./vite": "./dist/node/vite/qdadmPlugin.js",
48
+ "./vite-plugin-debug": "./dist/node/vite/qdadmDebugPlugin.js"
47
49
  },
48
50
  "files": [
49
51
  "src",
50
52
  "README.md",
51
- "LICENSE"
53
+ "LICENSE",
54
+ "dist/node"
52
55
  ],
53
56
  "dependencies": {
54
57
  "@quazardous/qdcore": "^1.1.0",
55
- "@quazardous/qddebug": "^1.0.0",
58
+ "@quazardous/qddebug": "^1.1.0",
56
59
  "@quazardous/quarkernel": "^2.1.0",
57
60
  "yaml": "^2.8.4"
58
61
  },
@@ -11,7 +11,10 @@
11
11
  * @module gen/vite-plugin
12
12
  */
13
13
 
14
- import { generateManagers, type GenerateManagersConfig } from './generateManagers'
14
+ // Explicit .js extension: this module is compiled for Node (#1895), and Node
15
+ // ESM will not resolve an extensionless relative import. TypeScript maps the
16
+ // .js specifier back to the .ts source, so the bundler path is unaffected.
17
+ import { generateManagers, type GenerateManagersConfig } from './generateManagers.js'
15
18
  import { pathToFileURL } from 'node:url'
16
19
  import { resolve } from 'node:path'
17
20
  import type { Plugin } from 'vite'