@forinda/kickjs-devtools 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +7 -0
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -87,10 +87,17 @@ declare class DevToolsAdapter implements AppAdapter {
|
|
|
87
87
|
readonly routeLatency: Record<string, RouteStats>;
|
|
88
88
|
private routes;
|
|
89
89
|
private container;
|
|
90
|
+
private appRef;
|
|
90
91
|
private adapterStatuses;
|
|
91
92
|
private stopErrorWatch;
|
|
92
93
|
private peerAdapters;
|
|
93
94
|
constructor(options?: DevToolsOptions);
|
|
95
|
+
/**
|
|
96
|
+
* Resolve peer adapters at request time. Prefers live adapters from the
|
|
97
|
+
* Application registry (survives HMR rebuild) and falls back to the
|
|
98
|
+
* constructor-provided refs.
|
|
99
|
+
*/
|
|
100
|
+
private getPeerAdapters;
|
|
94
101
|
beforeMount(app: any, container: Container): void;
|
|
95
102
|
middleware(): AdapterMiddleware[];
|
|
96
103
|
onRouteMount(controllerClass: any, mountPath: string): void;
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ var DevToolsAdapter = class {
|
|
|
41
41
|
// ── Internal State ───────────────────────────────────────────────────
|
|
42
42
|
routes = [];
|
|
43
43
|
container = null;
|
|
44
|
+
appRef = null;
|
|
44
45
|
adapterStatuses = {};
|
|
45
46
|
stopErrorWatch = null;
|
|
46
47
|
peerAdapters = [];
|
|
@@ -85,8 +86,21 @@ var DevToolsAdapter = class {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
// ── Adapter Lifecycle ────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Resolve peer adapters at request time. Prefers live adapters from the
|
|
91
|
+
* Application registry (survives HMR rebuild) and falls back to the
|
|
92
|
+
* constructor-provided refs.
|
|
93
|
+
*/
|
|
94
|
+
getPeerAdapters() {
|
|
95
|
+
const kickApp = this.appRef?.__kickApp;
|
|
96
|
+
if (kickApp && typeof kickApp.getAdapters === "function") {
|
|
97
|
+
return kickApp.getAdapters();
|
|
98
|
+
}
|
|
99
|
+
return this.peerAdapters;
|
|
100
|
+
}
|
|
88
101
|
beforeMount(app, container) {
|
|
89
102
|
if (!this.enabled) return;
|
|
103
|
+
this.appRef = app;
|
|
90
104
|
this.container = container;
|
|
91
105
|
this.startedAt.value = Date.now();
|
|
92
106
|
this.routes = [];
|
|
@@ -142,7 +156,7 @@ var DevToolsAdapter = class {
|
|
|
142
156
|
});
|
|
143
157
|
});
|
|
144
158
|
router.get("/state", (_req, res) => {
|
|
145
|
-
const wsAdapter = this.
|
|
159
|
+
const wsAdapter = this.getPeerAdapters().find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
146
160
|
res.json({
|
|
147
161
|
reactive: {
|
|
148
162
|
requestCount: this.requestCount.value,
|
|
@@ -161,7 +175,7 @@ var DevToolsAdapter = class {
|
|
|
161
175
|
});
|
|
162
176
|
});
|
|
163
177
|
router.get("/ws", (_req, res) => {
|
|
164
|
-
const wsAdapter = this.
|
|
178
|
+
const wsAdapter = this.getPeerAdapters().find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
165
179
|
if (!wsAdapter) {
|
|
166
180
|
res.json({
|
|
167
181
|
enabled: false,
|
|
@@ -175,7 +189,7 @@ var DevToolsAdapter = class {
|
|
|
175
189
|
});
|
|
176
190
|
});
|
|
177
191
|
router.get("/queues", async (_req, res) => {
|
|
178
|
-
const queueAdapter = this.
|
|
192
|
+
const queueAdapter = this.getPeerAdapters().find((a) => a.name === "QueueAdapter" && typeof a.getQueueNames === "function");
|
|
179
193
|
if (!queueAdapter) {
|
|
180
194
|
res.json({
|
|
181
195
|
enabled: false,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/adapter.ts"],"sourcesContent":["import 'reflect-metadata'\n\nexport { DevToolsAdapter, type DevToolsOptions } from './adapter'\n","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 AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\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 */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\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 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 beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\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 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: this.routeLatency,\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.peerAdapters.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.peerAdapters.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.peerAdapters.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 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 }\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 })\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: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\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(_server: any, _container: Container): 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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","names":["Router","static","serveStatic","dirname","join","fileURLToPath","existsSync","readFileSync","randomBytes","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","secret","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","peerAdapters","options","process","env","NODE_ENV","adapters","randomBytes","toString","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","token","use","req","res","next","provided","headers","query","path","method","endsWith","status","json","error","get","_req","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","uptime","wsAdapter","find","a","getStats","ws","message","queueAdapter","getQueueNames","names","queues","stats","getQueueStats","push","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","publicDir","resolvePublicDir","serveStatic","indexHtml","readFileSync","join","html","replace","type","send","info","middleware","handler","start","on","statusCode","routeKey","route","elapsed","totalMs","minMs","Infinity","maxMs","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","toUpperCase","controller","map","m","afterStart","_server","_container","shutdown","thisDir","dirname","fileURLToPath","url","candidates","dir","existsSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/adapter.ts"],"sourcesContent":["import 'reflect-metadata'\n\nexport { DevToolsAdapter, type DevToolsOptions } from './adapter'\n","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 AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\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 */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\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: any, container: Container): 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 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: this.routeLatency,\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 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 }\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 })\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: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\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(_server: any, _container: Container): 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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","names":["Router","static","serveStatic","dirname","join","fileURLToPath","existsSync","readFileSync","randomBytes","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","secret","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","appRef","adapterStatuses","stopErrorWatch","peerAdapters","options","process","env","NODE_ENV","adapters","randomBytes","toString","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","getPeerAdapters","kickApp","__kickApp","getAdapters","beforeMount","app","router","Router","token","use","req","res","next","provided","headers","query","path","method","endsWith","status","json","error","get","_req","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","uptime","wsAdapter","find","a","getStats","ws","message","queueAdapter","getQueueNames","names","queues","stats","getQueueStats","push","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","publicDir","resolvePublicDir","serveStatic","indexHtml","readFileSync","join","html","replace","type","send","info","middleware","handler","start","on","statusCode","routeKey","route","elapsed","totalMs","minMs","Infinity","maxMs","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","toUpperCase","controller","map","m","afterStart","_server","_container","shutdown","thisDir","dirname","fileURLToPath","url","candidates","dir","existsSync"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-devtools",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Development introspection dashboard for KickJS — routes, DI container, metrics, health",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"reflect-metadata": "^0.2.2",
|
|
30
|
-
"@forinda/kickjs-core": "1.2.
|
|
30
|
+
"@forinda/kickjs-core": "1.2.4"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"express": "^5.1.0"
|