@forinda/kickjs-devtools 2.0.1 → 2.2.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.
@@ -0,0 +1,118 @@
1
+
2
+ import { AdapterContext, AdapterMiddleware, AppAdapter, ComputedRef, Ref } from "@forinda/kickjs";
3
+
4
+ //#region src/adapter.d.ts
5
+ /** Per-route latency stats with percentile tracking */
6
+ interface RouteStats {
7
+ count: number;
8
+ totalMs: number;
9
+ minMs: number;
10
+ maxMs: number;
11
+ /** Ring buffer of last N samples for percentile computation */
12
+ samples: number[];
13
+ }
14
+ interface DevToolsOptions {
15
+ /** Base path for debug endpoints (default: '/_debug') */
16
+ basePath?: string;
17
+ /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */
18
+ enabled?: boolean;
19
+ /** Include environment variables (sanitized) at /_debug/config (default: false) */
20
+ exposeConfig?: boolean;
21
+ /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */
22
+ configPrefixes?: string[];
23
+ /** Callback when error rate exceeds threshold */
24
+ onErrorRateExceeded?: (rate: number) => void;
25
+ /** Error rate threshold (default: 0.5 = 50%) */
26
+ errorRateThreshold?: number;
27
+ /** Other adapters to discover stats from (e.g., WsAdapter) */
28
+ adapters?: any[];
29
+ /**
30
+ * Secret token to guard DevTools access. When set, all requests must
31
+ * include this token as `x-devtools-token` header or `?token=` query param.
32
+ *
33
+ * Auto-generated on startup if not provided. The token is logged to the console.
34
+ * Set to `false` to disable the guard entirely (not recommended).
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * new DevToolsAdapter({ secret: process.env.DEVTOOLS_SECRET })
39
+ * ```
40
+ */
41
+ secret?: string | false;
42
+ }
43
+ /**
44
+ * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.
45
+ *
46
+ * Exposes debug endpoints powered by reactive state (ref, computed, watch):
47
+ * - `GET /_debug/routes` — all registered routes with middleware
48
+ * - `GET /_debug/container` — DI registry with scopes and instantiation status
49
+ * - `GET /_debug/metrics` — live request/error counts, error rate, uptime
50
+ * - `GET /_debug/health` — deep health check with adapter status
51
+ * - `GET /_debug/config` — sanitized environment variables (opt-in)
52
+ * - `GET /_debug/state` — full reactive state snapshot
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { DevToolsAdapter } from '@forinda/kickjs-devtools'
57
+ *
58
+ * bootstrap({
59
+ * modules: [UserModule],
60
+ * adapters: [
61
+ * new DevToolsAdapter({
62
+ * enabled: process.env.NODE_ENV !== 'production',
63
+ * exposeConfig: true,
64
+ * configPrefixes: ['APP_', 'DATABASE_'],
65
+ * }),
66
+ * ],
67
+ * })
68
+ * ```
69
+ */
70
+ declare class DevToolsAdapter implements AppAdapter {
71
+ readonly name = "DevToolsAdapter";
72
+ private basePath;
73
+ private enabled;
74
+ private exposeConfig;
75
+ private configPrefixes;
76
+ private errorRateThreshold;
77
+ private secret;
78
+ /** Total requests received */
79
+ readonly requestCount: Ref<number>;
80
+ /** Total responses with status >= 500 */
81
+ readonly errorCount: Ref<number>;
82
+ /** Total responses with status >= 400 and < 500 */
83
+ readonly clientErrorCount: Ref<number>;
84
+ /** Server start time */
85
+ readonly startedAt: Ref<number>;
86
+ /** Computed error rate (server errors / total requests) */
87
+ readonly errorRate: ComputedRef<number>;
88
+ /** Computed uptime in seconds */
89
+ readonly uptimeSeconds: ComputedRef<number>;
90
+ /** Per-route latency tracking */
91
+ readonly routeLatency: Record<string, RouteStats>;
92
+ private routes;
93
+ private container;
94
+ private appRef;
95
+ private adapterStatuses;
96
+ private stopErrorWatch;
97
+ private peerAdapters;
98
+ constructor(options?: DevToolsOptions);
99
+ /**
100
+ * Resolve peer adapters at request time. Prefers live adapters from the
101
+ * Application registry (survives HMR rebuild) and falls back to the
102
+ * constructor-provided refs.
103
+ */
104
+ private getPeerAdapters;
105
+ beforeMount({
106
+ app,
107
+ container
108
+ }: AdapterContext): void;
109
+ middleware(): AdapterMiddleware[];
110
+ onRouteMount(controllerClass: any, mountPath: string): void;
111
+ afterStart({}: AdapterContext): void;
112
+ shutdown(): void;
113
+ /** Find the public/devtools directory relative to the built dist or source */
114
+ private resolvePublicDir;
115
+ }
116
+ //#endregion
117
+ export { DevToolsAdapter, type DevToolsOptions };
118
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter.ts"],"mappings":";;;;;UAmCU,UAAA;EACR,KAAA;EACA,OAAA;EACA,KAAA;EACA,KAAA;EAJkB;EAMlB,OAAA;AAAA;AAAA,UAsBe,eAAA;EAxBf;EA0BA,QAAA;EAxBO;EA0BP,OAAA;EAJe;EAMf,YAAA;;EAEA,cAAA;EANA;EAQA,mBAAA,IAAuB,IAAA;EAJvB;EAMA,kBAAA;EAFA;EAIA,QAAA;EAFA;;;;;AA8CF;;;;;;;EA9BE,MAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BW,eAAA,YAA2B,UAAA;EAAA,SAC7B,IAAA;EAAA,QAED,QAAA;EAAA,QACA,OAAA;EAAA,QACA,YAAA;EAAA,QACA,cAAA;EAAA,QACA,kBAAA;EAAA,QACA,MAAA;EAoBA;EAAA,SAhBC,YAAA,EAAc,GAAA;EAkBf;EAAA,SAhBC,UAAA,EAAY,GAAA;EAkBb;EAAA,SAhBC,gBAAA,EAAkB,GAAA;EAkBN;EAAA,SAhBZ,SAAA,EAAW,GAAA;EAwEZ;EAAA,SAtEC,SAAA,EAAW,WAAA;EA8EN;EAAA,SA5EL,aAAA,EAAe,WAAA;EA4EL;EAAA,SA1EV,YAAA,EAAc,MAAA,SAAe,UAAA;EAAA,QAG9B,MAAA;EAAA,QACA,SAAA;EAAA,QACA,MAAA;EAAA,QACA,eAAA;EAAA,QACA,cAAA;EAAA,QACA,YAAA;cAEI,OAAA,GAAS,eAAA;EAuXN;;;;;EAAA,QA/TP,eAAA;EAQR,WAAA,CAAA;IAAc,GAAA;IAAK;EAAA,GAAa,cAAA;EA6OhC,UAAA,CAAA,GAAc,iBAAA;EA0Cd,YAAA,CAAa,eAAA,OAAsB,SAAA;EAgCnC,UAAA,CAAA,IAAe,cAAA;EAQf,QAAA,CAAA;;UAMQ,gBAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,396 @@
1
+ /**
2
+ * @forinda/kickjs-devtools v2.2.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import "reflect-metadata";
12
+ import { Router, static as static$1 } from "express";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { randomBytes } from "node:crypto";
17
+ import { METADATA, computed, createLogger, getClassMeta, getMethodMeta, reactive, ref, watch } from "@forinda/kickjs";
18
+ //#region src/adapter.ts
19
+ const log = createLogger("DevTools");
20
+ const MAX_SAMPLES = 1e3;
21
+ /** Compute a percentile from a sorted array of numbers */
22
+ function percentile(sorted, p) {
23
+ if (sorted.length === 0) return 0;
24
+ const idx = Math.ceil(p * sorted.length) - 1;
25
+ return sorted[Math.max(0, idx)];
26
+ }
27
+ /** Compute p50, p95, p99 from a RouteStats samples buffer */
28
+ function computePercentiles(stats) {
29
+ const sorted = [...stats.samples].sort((a, b) => a - b);
30
+ return {
31
+ p50: percentile(sorted, .5),
32
+ p95: percentile(sorted, .95),
33
+ p99: percentile(sorted, .99)
34
+ };
35
+ }
36
+ /**
37
+ * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.
38
+ *
39
+ * Exposes debug endpoints powered by reactive state (ref, computed, watch):
40
+ * - `GET /_debug/routes` — all registered routes with middleware
41
+ * - `GET /_debug/container` — DI registry with scopes and instantiation status
42
+ * - `GET /_debug/metrics` — live request/error counts, error rate, uptime
43
+ * - `GET /_debug/health` — deep health check with adapter status
44
+ * - `GET /_debug/config` — sanitized environment variables (opt-in)
45
+ * - `GET /_debug/state` — full reactive state snapshot
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { DevToolsAdapter } from '@forinda/kickjs-devtools'
50
+ *
51
+ * bootstrap({
52
+ * modules: [UserModule],
53
+ * adapters: [
54
+ * new DevToolsAdapter({
55
+ * enabled: process.env.NODE_ENV !== 'production',
56
+ * exposeConfig: true,
57
+ * configPrefixes: ['APP_', 'DATABASE_'],
58
+ * }),
59
+ * ],
60
+ * })
61
+ * ```
62
+ */
63
+ var DevToolsAdapter = class {
64
+ name = "DevToolsAdapter";
65
+ basePath;
66
+ enabled;
67
+ exposeConfig;
68
+ configPrefixes;
69
+ errorRateThreshold;
70
+ secret;
71
+ /** Total requests received */
72
+ requestCount;
73
+ /** Total responses with status >= 500 */
74
+ errorCount;
75
+ /** Total responses with status >= 400 and < 500 */
76
+ clientErrorCount;
77
+ /** Server start time */
78
+ startedAt;
79
+ /** Computed error rate (server errors / total requests) */
80
+ errorRate;
81
+ /** Computed uptime in seconds */
82
+ uptimeSeconds;
83
+ /** Per-route latency tracking */
84
+ routeLatency;
85
+ routes = [];
86
+ container = null;
87
+ appRef = null;
88
+ adapterStatuses = {};
89
+ stopErrorWatch = null;
90
+ peerAdapters = [];
91
+ constructor(options = {}) {
92
+ this.basePath = options.basePath ?? "/_debug";
93
+ this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
94
+ this.exposeConfig = options.exposeConfig ?? false;
95
+ this.configPrefixes = options.configPrefixes ?? ["APP_", "NODE_ENV"];
96
+ this.errorRateThreshold = options.errorRateThreshold ?? .5;
97
+ this.peerAdapters = options.adapters ?? [];
98
+ if (options.secret === false) this.secret = false;
99
+ else if (options.secret) this.secret = options.secret;
100
+ else this.secret = randomBytes(16).toString("hex");
101
+ this.requestCount = ref(0);
102
+ this.errorCount = ref(0);
103
+ this.clientErrorCount = ref(0);
104
+ this.startedAt = ref(Date.now());
105
+ this.routeLatency = reactive({});
106
+ this.errorRate = computed(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0);
107
+ this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1e3));
108
+ if (options.onErrorRateExceeded) {
109
+ const callback = options.onErrorRateExceeded;
110
+ const threshold = this.errorRateThreshold;
111
+ this.stopErrorWatch = watch(this.errorRate, (rate) => {
112
+ if (rate > threshold) callback(rate);
113
+ });
114
+ } else this.stopErrorWatch = watch(this.errorRate, (rate) => {
115
+ if (rate > this.errorRateThreshold) log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`);
116
+ });
117
+ }
118
+ /**
119
+ * Resolve peer adapters at request time. Prefers live adapters from the
120
+ * Application registry (survives HMR rebuild) and falls back to the
121
+ * constructor-provided refs.
122
+ */
123
+ getPeerAdapters() {
124
+ const kickApp = this.appRef?.__kickApp;
125
+ if (kickApp && typeof kickApp.getAdapters === "function") return kickApp.getAdapters();
126
+ return this.peerAdapters;
127
+ }
128
+ beforeMount({ app, container }) {
129
+ if (!this.enabled) return;
130
+ this.appRef = app;
131
+ this.container = container;
132
+ this.startedAt.value = Date.now();
133
+ this.routes = [];
134
+ this.adapterStatuses[this.name] = "running";
135
+ const router = Router();
136
+ if (this.secret !== false) {
137
+ const token = this.secret;
138
+ router.use((req, res, next) => {
139
+ if ((req.headers["x-devtools-token"] ?? req.query?.token) === token) return next();
140
+ if (req.path === "/" && req.method === "GET" && !req.query?.token) return next();
141
+ if (req.path.endsWith(".js") || req.path.endsWith(".css")) return next();
142
+ res.status(403).json({ error: "Forbidden — invalid or missing devtools token" });
143
+ });
144
+ }
145
+ router.get("/routes", (_req, res) => {
146
+ res.json({ routes: this.routes });
147
+ });
148
+ router.get("/container", (_req, res) => {
149
+ const registrations = this.container?.getRegistrations() ?? [];
150
+ res.json({
151
+ registrations,
152
+ count: registrations.length
153
+ });
154
+ });
155
+ router.get("/metrics", (_req, res) => {
156
+ const latency = {};
157
+ for (const [key, stats] of Object.entries(this.routeLatency)) {
158
+ const { samples: _, ...rest } = stats;
159
+ latency[key] = {
160
+ ...rest,
161
+ ...computePercentiles(stats)
162
+ };
163
+ }
164
+ res.json({
165
+ requests: this.requestCount.value,
166
+ serverErrors: this.errorCount.value,
167
+ clientErrors: this.clientErrorCount.value,
168
+ errorRate: this.errorRate.value,
169
+ uptimeSeconds: this.uptimeSeconds.value,
170
+ startedAt: new Date(this.startedAt.value).toISOString(),
171
+ routeLatency: latency
172
+ });
173
+ });
174
+ router.get("/health", (_req, res) => {
175
+ const healthy = this.errorRate.value < this.errorRateThreshold;
176
+ const status = healthy ? "healthy" : "degraded";
177
+ res.status(healthy ? 200 : 503).json({
178
+ status,
179
+ errorRate: this.errorRate.value,
180
+ uptime: this.uptimeSeconds.value,
181
+ adapters: this.adapterStatuses
182
+ });
183
+ });
184
+ router.get("/state", (_req, res) => {
185
+ const wsAdapter = this.getPeerAdapters().find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
186
+ res.json({
187
+ reactive: {
188
+ requestCount: this.requestCount.value,
189
+ errorCount: this.errorCount.value,
190
+ clientErrorCount: this.clientErrorCount.value,
191
+ errorRate: this.errorRate.value,
192
+ uptimeSeconds: this.uptimeSeconds.value,
193
+ startedAt: new Date(this.startedAt.value).toISOString()
194
+ },
195
+ routes: this.routes.length,
196
+ container: this.container?.getRegistrations().length ?? 0,
197
+ routeLatency: this.routeLatency,
198
+ ...wsAdapter ? { ws: wsAdapter.getStats() } : {}
199
+ });
200
+ });
201
+ router.get("/ws", (_req, res) => {
202
+ const wsAdapter = this.getPeerAdapters().find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
203
+ if (!wsAdapter) {
204
+ res.json({
205
+ enabled: false,
206
+ message: "WsAdapter not found"
207
+ });
208
+ return;
209
+ }
210
+ res.json({
211
+ enabled: true,
212
+ ...wsAdapter.getStats()
213
+ });
214
+ });
215
+ router.get("/queues", async (_req, res) => {
216
+ const queueAdapter = this.getPeerAdapters().find((a) => a.name === "QueueAdapter" && typeof a.getQueueNames === "function");
217
+ if (!queueAdapter) {
218
+ res.json({
219
+ enabled: false,
220
+ message: "QueueAdapter not found"
221
+ });
222
+ return;
223
+ }
224
+ try {
225
+ const names = queueAdapter.getQueueNames?.() ?? [];
226
+ const queues = [];
227
+ for (const name of names) {
228
+ const stats = await queueAdapter.getQueueStats?.(name);
229
+ queues.push({
230
+ name,
231
+ ...stats
232
+ });
233
+ }
234
+ res.json({
235
+ enabled: true,
236
+ queues
237
+ });
238
+ } catch {
239
+ res.json({
240
+ enabled: true,
241
+ queues: [],
242
+ error: "Failed to fetch queue stats"
243
+ });
244
+ }
245
+ });
246
+ router.get("/graph", (_req, res) => {
247
+ const registrations = this.container?.getRegistrations() ?? [];
248
+ const nodes = registrations.filter((r) => !r.token.startsWith("__hmr__")).map((r) => ({
249
+ id: r.token,
250
+ kind: r.kind,
251
+ scope: r.scope,
252
+ resolveCount: r.resolveCount
253
+ }));
254
+ const nodeIds = new Set(nodes.map((n) => n.id));
255
+ const edges = [];
256
+ for (const r of registrations) {
257
+ if (r.token.startsWith("__hmr__")) continue;
258
+ for (const dep of r.dependencies) if (nodeIds.has(dep)) edges.push({
259
+ from: r.token,
260
+ to: dep
261
+ });
262
+ }
263
+ res.json({
264
+ nodes,
265
+ edges
266
+ });
267
+ });
268
+ router.get("/stream", (req, res) => {
269
+ res.writeHead(200, {
270
+ "Content-Type": "text/event-stream",
271
+ "Cache-Control": "no-cache",
272
+ Connection: "keep-alive"
273
+ });
274
+ const sendMetrics = () => {
275
+ const data = {
276
+ type: "metrics",
277
+ requestCount: this.requestCount.value,
278
+ errorCount: this.errorCount.value,
279
+ clientErrorCount: this.clientErrorCount.value,
280
+ errorRate: this.errorRate.value,
281
+ uptimeSeconds: this.uptimeSeconds.value
282
+ };
283
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
284
+ };
285
+ sendMetrics();
286
+ const unsubContainer = this.container?.onChange?.((changes) => {
287
+ res.write(`data: ${JSON.stringify({
288
+ type: "container",
289
+ changes,
290
+ timestamp: Date.now()
291
+ })}\n\n`);
292
+ sendMetrics();
293
+ });
294
+ const stopRequestWatch = watch(this.requestCount, () => sendMetrics());
295
+ const stopErrorWatch = watch(this.errorCount, () => sendMetrics());
296
+ const heartbeat = setInterval(() => {
297
+ res.write(`: heartbeat\n\n`);
298
+ }, 3e4);
299
+ req.on("close", () => {
300
+ unsubContainer?.();
301
+ stopRequestWatch();
302
+ stopErrorWatch();
303
+ clearInterval(heartbeat);
304
+ });
305
+ });
306
+ if (this.exposeConfig) router.get("/config", (_req, res) => {
307
+ const config = {};
308
+ for (const [key, value] of Object.entries(process.env)) {
309
+ if (value === void 0) continue;
310
+ config[key] = this.configPrefixes.some((prefix) => key.startsWith(prefix)) ? value : "[REDACTED]";
311
+ }
312
+ res.json({ config });
313
+ });
314
+ const publicDir = this.resolvePublicDir();
315
+ if (publicDir) {
316
+ router.use(static$1(publicDir));
317
+ const indexHtml = readFileSync(join(publicDir, "index.html"), "utf-8");
318
+ router.get("/", (_req, res) => {
319
+ const html = indexHtml.replace("<body", `<body data-base="${this.basePath}"`);
320
+ res.type("html").send(html);
321
+ });
322
+ } else router.get("/", (_req, res) => {
323
+ res.type("html").send("<h1>DevTools: public directory not found</h1>");
324
+ });
325
+ app.use(this.basePath, router);
326
+ if (this.secret) {
327
+ log.info(`DevTools mounted at ${this.basePath} [token: ${this.secret}]`);
328
+ log.info(`Access: ${this.basePath}?token=${this.secret}`);
329
+ } else log.info(`DevTools mounted at ${this.basePath} [no guard]`);
330
+ }
331
+ middleware() {
332
+ if (!this.enabled) return [];
333
+ return [{
334
+ handler: (req, res, next) => {
335
+ const start = Date.now();
336
+ this.requestCount.value++;
337
+ res.on("finish", () => {
338
+ if (res.statusCode >= 500) this.errorCount.value++;
339
+ else if (res.statusCode >= 400) this.clientErrorCount.value++;
340
+ const routeKey = `${req.method} ${req.route?.path ?? req.path}`;
341
+ const elapsed = Date.now() - start;
342
+ if (!this.routeLatency[routeKey]) this.routeLatency[routeKey] = {
343
+ count: 0,
344
+ totalMs: 0,
345
+ minMs: Infinity,
346
+ maxMs: 0,
347
+ samples: []
348
+ };
349
+ const stats = this.routeLatency[routeKey];
350
+ stats.count++;
351
+ stats.totalMs += elapsed;
352
+ stats.minMs = Math.min(stats.minMs, elapsed);
353
+ stats.maxMs = Math.max(stats.maxMs, elapsed);
354
+ stats.samples.push(elapsed);
355
+ if (stats.samples.length > MAX_SAMPLES) stats.samples.shift();
356
+ });
357
+ next();
358
+ },
359
+ phase: "beforeGlobal"
360
+ }];
361
+ }
362
+ onRouteMount(controllerClass, mountPath) {
363
+ if (!this.enabled) return;
364
+ const routes = getClassMeta(METADATA.ROUTES, controllerClass, []);
365
+ const classMiddleware = getClassMeta(METADATA.CLASS_MIDDLEWARES, controllerClass, []);
366
+ for (const route of routes) {
367
+ const methodMiddleware = getMethodMeta(METADATA.METHOD_MIDDLEWARES, controllerClass.prototype, route.handlerName, []);
368
+ this.routes.push({
369
+ method: route.method.toUpperCase(),
370
+ path: `${mountPath}${route.path === "/" ? "" : route.path}`,
371
+ controller: controllerClass.name,
372
+ handler: route.handlerName,
373
+ middleware: [...classMiddleware.map((m) => m.name || "anonymous"), ...methodMiddleware.map((m) => m.name || "anonymous")]
374
+ });
375
+ }
376
+ }
377
+ afterStart({}) {
378
+ if (!this.enabled) return;
379
+ log.info(`DevTools ready — ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
380
+ }
381
+ shutdown() {
382
+ this.stopErrorWatch?.();
383
+ this.adapterStatuses[this.name] = "stopped";
384
+ }
385
+ /** Find the public/devtools directory relative to the built dist or source */
386
+ resolvePublicDir() {
387
+ const thisDir = dirname(fileURLToPath(import.meta.url));
388
+ const candidates = [join(thisDir, "..", "public", "devtools"), join(thisDir, "..", "..", "public", "devtools")];
389
+ for (const dir of candidates) if (existsSync(join(dir, "index.html"))) return dir;
390
+ return null;
391
+ }
392
+ };
393
+ //#endregion
394
+ export { DevToolsAdapter };
395
+
396
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["serveStatic"],"sources":["../src/adapter.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router, static as serveStatic } from 'express'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { randomBytes } from 'node:crypto'\nimport {\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n getClassMeta,\n getMethodMeta,\n} from '@forinda/kickjs'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats with percentile tracking */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n /** Ring buffer of last N samples for percentile computation */\n samples: number[]\n}\n\nconst MAX_SAMPLES = 1000\n\n/** Compute a percentile from a sorted array of numbers */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0\n const idx = Math.ceil(p * sorted.length) - 1\n return sorted[Math.max(0, idx)]\n}\n\n/** Compute p50, p95, p99 from a RouteStats samples buffer */\nfunction computePercentiles(stats: RouteStats): { p50: number; p95: number; p99: number } {\n const sorted = [...stats.samples].sort((a, b) => a - b)\n return {\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n }\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n\n /**\n * Secret token to guard DevTools access. When set, all requests must\n * include this token as `x-devtools-token` header or `?token=` query param.\n *\n * Auto-generated on startup if not provided. The token is logged to the console.\n * Set to `false` to disable the guard entirely (not recommended).\n *\n * @example\n * ```ts\n * new DevToolsAdapter({ secret: process.env.DEVTOOLS_SECRET })\n * ```\n */\n secret?: string | false\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n private secret: string | false\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private appRef: any = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n private peerAdapters: any[] = []\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n this.peerAdapters = options.adapters ?? []\n\n // Secret token guard\n if (options.secret === false) {\n this.secret = false\n } else if (options.secret) {\n this.secret = options.secret\n } else {\n // Auto-generate a random token\n this.secret = randomBytes(16).toString('hex')\n }\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n /**\n * Resolve peer adapters at request time. Prefers live adapters from the\n * Application registry (survives HMR rebuild) and falls back to the\n * constructor-provided refs.\n */\n private getPeerAdapters(): any[] {\n const kickApp = this.appRef?.__kickApp\n if (kickApp && typeof kickApp.getAdapters === 'function') {\n return kickApp.getAdapters()\n }\n return this.peerAdapters\n }\n\n beforeMount({ app, container }: AdapterContext): void {\n if (!this.enabled) return\n\n this.appRef = app\n this.container = container\n this.startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\n\n const router = Router()\n\n // ── Access guard — require secret token ──────────────────────────\n if (this.secret !== false) {\n const token = this.secret\n router.use((req: Request, res: Response, next: NextFunction) => {\n const provided = req.headers['x-devtools-token'] ?? req.query?.token\n if (provided === token) return next()\n // Allow the dashboard HTML itself (it will include the token in API calls)\n if (req.path === '/' && req.method === 'GET' && !req.query?.token) {\n return next() // serve dashboard, it handles auth via token\n }\n // Serve static assets for the dashboard (js files)\n if (req.path.endsWith('.js') || req.path.endsWith('.css')) {\n return next()\n }\n res.status(403).json({ error: 'Forbidden — invalid or missing devtools token' })\n })\n }\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n // Build latency with percentiles, omitting raw samples from response\n const latency: Record<string, any> = {}\n for (const [key, stats] of Object.entries(this.routeLatency)) {\n const { samples: _, ...rest } = stats\n latency[key] = { ...rest, ...computePercentiles(stats) }\n }\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: latency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = this.getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = this.getPeerAdapters().find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n router.get('/queues', async (_req: Request, res: Response) => {\n const queueAdapter = this.getPeerAdapters().find(\n (a) => a.name === 'QueueAdapter' && typeof a.getQueueNames === 'function',\n )\n if (!queueAdapter) {\n res.json({ enabled: false, message: 'QueueAdapter not found' })\n return\n }\n try {\n const names: string[] = queueAdapter.getQueueNames?.() ?? []\n const queues: any[] = []\n for (const name of names) {\n const stats = await queueAdapter.getQueueStats?.(name)\n queues.push({ name, ...stats })\n }\n res.json({ enabled: true, queues })\n } catch {\n res.json({ enabled: true, queues: [], error: 'Failed to fetch queue stats' })\n }\n })\n\n // ── Dependency graph ────────────────────────────────────────────\n router.get('/graph', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n const nodes = registrations\n .filter((r) => !r.token.startsWith('__hmr__'))\n .map((r) => ({\n id: r.token,\n kind: r.kind,\n scope: r.scope,\n resolveCount: r.resolveCount,\n }))\n const nodeIds = new Set(nodes.map((n) => n.id))\n const edges: Array<{ from: string; to: string }> = []\n for (const r of registrations) {\n if (r.token.startsWith('__hmr__')) continue\n for (const dep of r.dependencies) {\n if (nodeIds.has(dep)) {\n edges.push({ from: r.token, to: dep })\n }\n }\n }\n res.json({ nodes, edges })\n })\n\n // ── SSE stream for real-time updates ───────────────────────\n // Uses container.onChange() to push events only when something\n // actually changes — no polling. Also sends periodic heartbeats\n // to keep the connection alive through proxies.\n router.get('/stream', (req: Request, res: Response) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n })\n\n // Send initial state\n const sendMetrics = () => {\n const data = {\n type: 'metrics',\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n }\n res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n }\n sendMetrics()\n\n // Subscribe to container changes (batched, ~50ms debounce)\n const unsubContainer = this.container?.onChange?.((changes) => {\n res.write(\n `data: ${JSON.stringify({ type: 'container', changes, timestamp: Date.now() })}\\n\\n`,\n )\n // Also send updated metrics after container changes\n sendMetrics()\n })\n\n // Watch reactive metrics — push on change (request count, errors)\n const stopRequestWatch = watch(this.requestCount, () => sendMetrics())\n const stopErrorWatch = watch(this.errorCount, () => sendMetrics())\n\n // Heartbeat every 30s to keep connection alive through proxies/LBs\n const heartbeat = setInterval(() => {\n res.write(`: heartbeat\\n\\n`)\n }, 30000)\n\n req.on('close', () => {\n unsubContainer?.()\n stopRequestWatch()\n stopErrorWatch()\n clearInterval(heartbeat)\n })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n // Dashboard UI — Vue + Tailwind from public/devtools directory\n const publicDir = this.resolvePublicDir()\n if (publicDir) {\n // Serve static assets (vue.global.min.js, tailwind-cdn.js)\n router.use(serveStatic(publicDir))\n\n // Serve index.html with base path injected\n const indexHtml = readFileSync(join(publicDir, 'index.html'), 'utf-8')\n router.get('/', (_req: Request, res: Response) => {\n // Inject basePath as data attribute for the Vue app\n const html = indexHtml.replace('<body', `<body data-base=\"${this.basePath}\"`)\n res.type('html').send(html)\n })\n } else {\n router.get('/', (_req: Request, res: Response) => {\n res.type('html').send('<h1>DevTools: public directory not found</h1>')\n })\n }\n\n app.use(this.basePath, router)\n\n if (this.secret) {\n log.info(`DevTools mounted at ${this.basePath} [token: ${this.secret}]`)\n log.info(`Access: ${this.basePath}?token=${this.secret}`)\n } else {\n log.info(`DevTools mounted at ${this.basePath} [no guard]`)\n }\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n samples: [],\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n stats.samples.push(elapsed)\n if (stats.samples.length > MAX_SAMPLES) stats.samples.shift()\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes = getClassMeta<Array<{ method: string; path: string; handlerName: string }>>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n const classMiddleware = getClassMeta<any[]>(METADATA.CLASS_MIDDLEWARES, controllerClass, [])\n\n for (const route of routes) {\n const methodMiddleware = getMethodMeta<any[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n [],\n )\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n afterStart({}: AdapterContext): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n\n /** Find the public/devtools directory relative to the built dist or source */\n private resolvePublicDir(): string | null {\n // Try relative to this file's location (works in dist/)\n const thisDir = dirname(fileURLToPath(import.meta.url))\n const candidates = [\n join(thisDir, '..', 'public', 'devtools'), // dist/ -> public/devtools\n join(thisDir, '..', '..', 'public', 'devtools'), // src/ -> public/devtools\n ]\n for (const dir of candidates) {\n if (existsSync(join(dir, 'index.html'))) return dir\n }\n return null\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-devtools",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "Development introspection dashboard for KickJS — routes, DI container, metrics, health",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -39,21 +39,39 @@
39
39
  "vite"
40
40
  ],
41
41
  "type": "module",
42
- "main": "dist/index.js",
43
- "types": "dist/index.d.ts",
42
+ "main": "dist/index.mjs",
43
+ "types": "dist/index.d.mts",
44
44
  "exports": {
45
45
  ".": {
46
- "import": "./dist/index.js",
47
- "types": "./dist/index.d.ts"
46
+ "import": "./dist/index.mjs",
47
+ "types": "./dist/index.d.mts"
48
48
  }
49
49
  },
50
50
  "files": [
51
51
  "dist",
52
52
  "public"
53
53
  ],
54
+ "wireit": {
55
+ "build": {
56
+ "command": "tsdown",
57
+ "files": [
58
+ "src/**/*.ts",
59
+ "tsdown.config.ts",
60
+ "tsconfig.json",
61
+ "package.json"
62
+ ],
63
+ "output": [
64
+ "dist/**"
65
+ ],
66
+ "dependencies": [
67
+ "../core:build",
68
+ "../http:build"
69
+ ]
70
+ }
71
+ },
54
72
  "dependencies": {
55
73
  "reflect-metadata": "^0.2.2",
56
- "@forinda/kickjs": "2.0.1"
74
+ "@forinda/kickjs": "2.2.0"
57
75
  },
58
76
  "peerDependencies": {
59
77
  "express": "^5.1.0"
@@ -63,7 +81,7 @@
63
81
  "@types/node": "^25.0.0",
64
82
  "express": "^5.1.0",
65
83
  "typescript": "^5.9.2",
66
- "vitest": "^4.1.1"
84
+ "vitest": "^4.1.2"
67
85
  },
68
86
  "publishConfig": {
69
87
  "access": "public"
@@ -83,11 +101,10 @@
83
101
  "url": "https://github.com/forinda/kick-js/issues"
84
102
  },
85
103
  "scripts": {
86
- "build": "vite build && pnpm build:types",
87
- "build:types": "tsc -p tsconfig.build.json",
88
- "dev": "vite build --watch",
104
+ "build": "wireit",
105
+ "dev": "tsdown --watch",
89
106
  "test": "vitest run --passWithNoTests",
90
107
  "typecheck": "tsc --noEmit",
91
- "clean": "rm -rf dist .turbo"
108
+ "clean": "rm -rf dist .wireit"
92
109
  }
93
110
  }