@forinda/kickjs-devtools 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +323 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
- package/public/devtools/index.html +575 -0
- package/public/devtools/tailwind.global.js +897 -0
- package/public/devtools/vue.global.min.js +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Felix Orinda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AppAdapter, Ref, ComputedRef, Container, AdapterMiddleware } from '@forinda/kickjs-core';
|
|
2
|
+
|
|
3
|
+
/** Per-route latency stats */
|
|
4
|
+
interface RouteStats {
|
|
5
|
+
count: number;
|
|
6
|
+
totalMs: number;
|
|
7
|
+
minMs: number;
|
|
8
|
+
maxMs: number;
|
|
9
|
+
}
|
|
10
|
+
interface DevToolsOptions {
|
|
11
|
+
/** Base path for debug endpoints (default: '/_debug') */
|
|
12
|
+
basePath?: string;
|
|
13
|
+
/** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/** Include environment variables (sanitized) at /_debug/config (default: false) */
|
|
16
|
+
exposeConfig?: boolean;
|
|
17
|
+
/** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */
|
|
18
|
+
configPrefixes?: string[];
|
|
19
|
+
/** Callback when error rate exceeds threshold */
|
|
20
|
+
onErrorRateExceeded?: (rate: number) => void;
|
|
21
|
+
/** Error rate threshold (default: 0.5 = 50%) */
|
|
22
|
+
errorRateThreshold?: number;
|
|
23
|
+
/** Other adapters to discover stats from (e.g., WsAdapter) */
|
|
24
|
+
adapters?: any[];
|
|
25
|
+
/**
|
|
26
|
+
* Secret token to guard DevTools access. When set, all requests must
|
|
27
|
+
* include this token as `x-devtools-token` header or `?token=` query param.
|
|
28
|
+
*
|
|
29
|
+
* Auto-generated on startup if not provided. The token is logged to the console.
|
|
30
|
+
* Set to `false` to disable the guard entirely (not recommended).
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* new DevToolsAdapter({ secret: process.env.DEVTOOLS_SECRET })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
secret?: string | false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* DevToolsAdapter — Vue-style reactive introspection for KickJS applications.
|
|
41
|
+
*
|
|
42
|
+
* Exposes debug endpoints powered by reactive state (ref, computed, watch):
|
|
43
|
+
* - `GET /_debug/routes` — all registered routes with middleware
|
|
44
|
+
* - `GET /_debug/container` — DI registry with scopes and instantiation status
|
|
45
|
+
* - `GET /_debug/metrics` — live request/error counts, error rate, uptime
|
|
46
|
+
* - `GET /_debug/health` — deep health check with adapter status
|
|
47
|
+
* - `GET /_debug/config` — sanitized environment variables (opt-in)
|
|
48
|
+
* - `GET /_debug/state` — full reactive state snapshot
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { DevToolsAdapter } from '@forinda/kickjs-devtools'
|
|
53
|
+
*
|
|
54
|
+
* bootstrap({
|
|
55
|
+
* modules: [UserModule],
|
|
56
|
+
* adapters: [
|
|
57
|
+
* new DevToolsAdapter({
|
|
58
|
+
* enabled: process.env.NODE_ENV !== 'production',
|
|
59
|
+
* exposeConfig: true,
|
|
60
|
+
* configPrefixes: ['APP_', 'DATABASE_'],
|
|
61
|
+
* }),
|
|
62
|
+
* ],
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare class DevToolsAdapter implements AppAdapter {
|
|
67
|
+
readonly name = "DevToolsAdapter";
|
|
68
|
+
private basePath;
|
|
69
|
+
private enabled;
|
|
70
|
+
private exposeConfig;
|
|
71
|
+
private configPrefixes;
|
|
72
|
+
private errorRateThreshold;
|
|
73
|
+
private secret;
|
|
74
|
+
/** Total requests received */
|
|
75
|
+
readonly requestCount: Ref<number>;
|
|
76
|
+
/** Total responses with status >= 500 */
|
|
77
|
+
readonly errorCount: Ref<number>;
|
|
78
|
+
/** Total responses with status >= 400 and < 500 */
|
|
79
|
+
readonly clientErrorCount: Ref<number>;
|
|
80
|
+
/** Server start time */
|
|
81
|
+
readonly startedAt: Ref<number>;
|
|
82
|
+
/** Computed error rate (server errors / total requests) */
|
|
83
|
+
readonly errorRate: ComputedRef<number>;
|
|
84
|
+
/** Computed uptime in seconds */
|
|
85
|
+
readonly uptimeSeconds: ComputedRef<number>;
|
|
86
|
+
/** Per-route latency tracking */
|
|
87
|
+
readonly routeLatency: Record<string, RouteStats>;
|
|
88
|
+
private routes;
|
|
89
|
+
private container;
|
|
90
|
+
private adapterStatuses;
|
|
91
|
+
private stopErrorWatch;
|
|
92
|
+
private peerAdapters;
|
|
93
|
+
constructor(options?: DevToolsOptions);
|
|
94
|
+
beforeMount(app: any, container: Container): void;
|
|
95
|
+
middleware(): AdapterMiddleware[];
|
|
96
|
+
onRouteMount(controllerClass: any, mountPath: string): void;
|
|
97
|
+
afterStart(_server: any, _container: Container): void;
|
|
98
|
+
shutdown(): void;
|
|
99
|
+
/** Find the public/devtools directory relative to the built dist or source */
|
|
100
|
+
private resolvePublicDir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { DevToolsAdapter, type DevToolsOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
import "reflect-metadata";
|
|
12
|
+
|
|
13
|
+
// src/adapter.ts
|
|
14
|
+
import { Router } from "express";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { existsSync, readFileSync } from "fs";
|
|
18
|
+
import { randomBytes } from "crypto";
|
|
19
|
+
import { METADATA, ref, computed, reactive, watch, createLogger } from "@forinda/kickjs-core";
|
|
20
|
+
var log = createLogger("DevTools");
|
|
21
|
+
var DevToolsAdapter = class {
|
|
22
|
+
static {
|
|
23
|
+
__name(this, "DevToolsAdapter");
|
|
24
|
+
}
|
|
25
|
+
name = "DevToolsAdapter";
|
|
26
|
+
basePath;
|
|
27
|
+
enabled;
|
|
28
|
+
exposeConfig;
|
|
29
|
+
configPrefixes;
|
|
30
|
+
errorRateThreshold;
|
|
31
|
+
secret;
|
|
32
|
+
// ── Reactive State ───────────────────────────────────────────────────
|
|
33
|
+
/** Total requests received */
|
|
34
|
+
requestCount;
|
|
35
|
+
/** Total responses with status >= 500 */
|
|
36
|
+
errorCount;
|
|
37
|
+
/** Total responses with status >= 400 and < 500 */
|
|
38
|
+
clientErrorCount;
|
|
39
|
+
/** Server start time */
|
|
40
|
+
startedAt;
|
|
41
|
+
/** Computed error rate (server errors / total requests) */
|
|
42
|
+
errorRate;
|
|
43
|
+
/** Computed uptime in seconds */
|
|
44
|
+
uptimeSeconds;
|
|
45
|
+
/** Per-route latency tracking */
|
|
46
|
+
routeLatency;
|
|
47
|
+
// ── Internal State ───────────────────────────────────────────────────
|
|
48
|
+
routes = [];
|
|
49
|
+
container = null;
|
|
50
|
+
adapterStatuses = {};
|
|
51
|
+
stopErrorWatch = null;
|
|
52
|
+
peerAdapters = [];
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.basePath = options.basePath ?? "/_debug";
|
|
55
|
+
this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
|
|
56
|
+
this.exposeConfig = options.exposeConfig ?? false;
|
|
57
|
+
this.configPrefixes = options.configPrefixes ?? [
|
|
58
|
+
"APP_",
|
|
59
|
+
"NODE_ENV"
|
|
60
|
+
];
|
|
61
|
+
this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
|
|
62
|
+
this.peerAdapters = options.adapters ?? [];
|
|
63
|
+
if (options.secret === false) {
|
|
64
|
+
this.secret = false;
|
|
65
|
+
} else if (options.secret) {
|
|
66
|
+
this.secret = options.secret;
|
|
67
|
+
} else {
|
|
68
|
+
this.secret = randomBytes(16).toString("hex");
|
|
69
|
+
}
|
|
70
|
+
this.requestCount = ref(0);
|
|
71
|
+
this.errorCount = ref(0);
|
|
72
|
+
this.clientErrorCount = ref(0);
|
|
73
|
+
this.startedAt = ref(Date.now());
|
|
74
|
+
this.routeLatency = reactive({});
|
|
75
|
+
this.errorRate = computed(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0);
|
|
76
|
+
this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1e3));
|
|
77
|
+
if (options.onErrorRateExceeded) {
|
|
78
|
+
const callback = options.onErrorRateExceeded;
|
|
79
|
+
const threshold = this.errorRateThreshold;
|
|
80
|
+
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
81
|
+
if (rate > threshold) {
|
|
82
|
+
callback(rate);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
this.stopErrorWatch = watch(this.errorRate, (rate) => {
|
|
87
|
+
if (rate > this.errorRateThreshold) {
|
|
88
|
+
log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Adapter Lifecycle ────────────────────────────────────────────────
|
|
94
|
+
beforeMount(app, container) {
|
|
95
|
+
if (!this.enabled) return;
|
|
96
|
+
this.container = container;
|
|
97
|
+
this.startedAt.value = Date.now();
|
|
98
|
+
this.routes = [];
|
|
99
|
+
this.adapterStatuses[this.name] = "running";
|
|
100
|
+
const router = Router();
|
|
101
|
+
if (this.secret !== false) {
|
|
102
|
+
const token = this.secret;
|
|
103
|
+
router.use((req, res, next) => {
|
|
104
|
+
const provided = req.headers["x-devtools-token"] ?? req.query?.token;
|
|
105
|
+
if (provided === token) return next();
|
|
106
|
+
if (req.path === "/" && req.method === "GET" && !req.query?.token) {
|
|
107
|
+
return next();
|
|
108
|
+
}
|
|
109
|
+
if (req.path.endsWith(".js") || req.path.endsWith(".css")) {
|
|
110
|
+
return next();
|
|
111
|
+
}
|
|
112
|
+
res.status(403).json({
|
|
113
|
+
error: "Forbidden \u2014 invalid or missing devtools token"
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
router.get("/routes", (_req, res) => {
|
|
118
|
+
res.json({
|
|
119
|
+
routes: this.routes
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
router.get("/container", (_req, res) => {
|
|
123
|
+
const registrations = this.container?.getRegistrations() ?? [];
|
|
124
|
+
res.json({
|
|
125
|
+
registrations,
|
|
126
|
+
count: registrations.length
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
router.get("/metrics", (_req, res) => {
|
|
130
|
+
res.json({
|
|
131
|
+
requests: this.requestCount.value,
|
|
132
|
+
serverErrors: this.errorCount.value,
|
|
133
|
+
clientErrors: this.clientErrorCount.value,
|
|
134
|
+
errorRate: this.errorRate.value,
|
|
135
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
136
|
+
startedAt: new Date(this.startedAt.value).toISOString(),
|
|
137
|
+
routeLatency: this.routeLatency
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
router.get("/health", (_req, res) => {
|
|
141
|
+
const healthy = this.errorRate.value < this.errorRateThreshold;
|
|
142
|
+
const status = healthy ? "healthy" : "degraded";
|
|
143
|
+
res.status(healthy ? 200 : 503).json({
|
|
144
|
+
status,
|
|
145
|
+
errorRate: this.errorRate.value,
|
|
146
|
+
uptime: this.uptimeSeconds.value,
|
|
147
|
+
adapters: this.adapterStatuses
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
router.get("/state", (_req, res) => {
|
|
151
|
+
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
152
|
+
res.json({
|
|
153
|
+
reactive: {
|
|
154
|
+
requestCount: this.requestCount.value,
|
|
155
|
+
errorCount: this.errorCount.value,
|
|
156
|
+
clientErrorCount: this.clientErrorCount.value,
|
|
157
|
+
errorRate: this.errorRate.value,
|
|
158
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
159
|
+
startedAt: new Date(this.startedAt.value).toISOString()
|
|
160
|
+
},
|
|
161
|
+
routes: this.routes.length,
|
|
162
|
+
container: this.container?.getRegistrations().length ?? 0,
|
|
163
|
+
routeLatency: this.routeLatency,
|
|
164
|
+
...wsAdapter ? {
|
|
165
|
+
ws: wsAdapter.getStats()
|
|
166
|
+
} : {}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
router.get("/ws", (_req, res) => {
|
|
170
|
+
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
171
|
+
if (!wsAdapter) {
|
|
172
|
+
res.json({
|
|
173
|
+
enabled: false,
|
|
174
|
+
message: "WsAdapter not found"
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
res.json({
|
|
179
|
+
enabled: true,
|
|
180
|
+
...wsAdapter.getStats()
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
router.get("/queues", async (_req, res) => {
|
|
184
|
+
const queueAdapter = this.peerAdapters.find((a) => a.name === "QueueAdapter" && typeof a.getQueueNames === "function");
|
|
185
|
+
if (!queueAdapter) {
|
|
186
|
+
res.json({
|
|
187
|
+
enabled: false,
|
|
188
|
+
message: "QueueAdapter not found"
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const names = queueAdapter.getQueueNames?.() ?? [];
|
|
194
|
+
const queues = [];
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
const stats = await queueAdapter.getQueueStats?.(name);
|
|
197
|
+
queues.push({
|
|
198
|
+
name,
|
|
199
|
+
...stats
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
res.json({
|
|
203
|
+
enabled: true,
|
|
204
|
+
queues
|
|
205
|
+
});
|
|
206
|
+
} catch {
|
|
207
|
+
res.json({
|
|
208
|
+
enabled: true,
|
|
209
|
+
queues: [],
|
|
210
|
+
error: "Failed to fetch queue stats"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (this.exposeConfig) {
|
|
215
|
+
router.get("/config", (_req, res) => {
|
|
216
|
+
const config = {};
|
|
217
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
218
|
+
if (value === void 0) continue;
|
|
219
|
+
const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix));
|
|
220
|
+
config[key] = allowed ? value : "[REDACTED]";
|
|
221
|
+
}
|
|
222
|
+
res.json({
|
|
223
|
+
config
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const publicDir = this.resolvePublicDir();
|
|
228
|
+
if (publicDir) {
|
|
229
|
+
const express = __require("express");
|
|
230
|
+
router.use(express.static(publicDir));
|
|
231
|
+
const indexHtml = readFileSync(join(publicDir, "index.html"), "utf-8");
|
|
232
|
+
router.get("/", (_req, res) => {
|
|
233
|
+
const html = indexHtml.replace("<body", `<body data-base="${this.basePath}"`);
|
|
234
|
+
res.type("html").send(html);
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
router.get("/", (_req, res) => {
|
|
238
|
+
res.type("html").send("<h1>DevTools: public directory not found</h1>");
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
app.use(this.basePath, router);
|
|
242
|
+
if (this.secret) {
|
|
243
|
+
log.info(`DevTools mounted at ${this.basePath} [token: ${this.secret}]`);
|
|
244
|
+
log.info(`Access: ${this.basePath}?token=${this.secret}`);
|
|
245
|
+
} else {
|
|
246
|
+
log.info(`DevTools mounted at ${this.basePath} [no guard]`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
middleware() {
|
|
250
|
+
if (!this.enabled) return [];
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
handler: /* @__PURE__ */ __name((req, res, next) => {
|
|
254
|
+
const start = Date.now();
|
|
255
|
+
this.requestCount.value++;
|
|
256
|
+
res.on("finish", () => {
|
|
257
|
+
if (res.statusCode >= 500) this.errorCount.value++;
|
|
258
|
+
else if (res.statusCode >= 400) this.clientErrorCount.value++;
|
|
259
|
+
const routeKey = `${req.method} ${req.route?.path ?? req.path}`;
|
|
260
|
+
const elapsed = Date.now() - start;
|
|
261
|
+
if (!this.routeLatency[routeKey]) {
|
|
262
|
+
this.routeLatency[routeKey] = {
|
|
263
|
+
count: 0,
|
|
264
|
+
totalMs: 0,
|
|
265
|
+
minMs: Infinity,
|
|
266
|
+
maxMs: 0
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const stats = this.routeLatency[routeKey];
|
|
270
|
+
stats.count++;
|
|
271
|
+
stats.totalMs += elapsed;
|
|
272
|
+
stats.minMs = Math.min(stats.minMs, elapsed);
|
|
273
|
+
stats.maxMs = Math.max(stats.maxMs, elapsed);
|
|
274
|
+
});
|
|
275
|
+
next();
|
|
276
|
+
}, "handler"),
|
|
277
|
+
phase: "beforeGlobal"
|
|
278
|
+
}
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
onRouteMount(controllerClass, mountPath) {
|
|
282
|
+
if (!this.enabled) return;
|
|
283
|
+
const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
|
|
284
|
+
const classMiddleware = Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? [];
|
|
285
|
+
for (const route of routes) {
|
|
286
|
+
const methodMiddleware = Reflect.getMetadata(METADATA.METHOD_MIDDLEWARES, controllerClass.prototype, route.handlerName) ?? [];
|
|
287
|
+
this.routes.push({
|
|
288
|
+
method: route.method.toUpperCase(),
|
|
289
|
+
path: `${mountPath}${route.path === "/" ? "" : route.path}`,
|
|
290
|
+
controller: controllerClass.name,
|
|
291
|
+
handler: route.handlerName,
|
|
292
|
+
middleware: [
|
|
293
|
+
...classMiddleware.map((m) => m.name || "anonymous"),
|
|
294
|
+
...methodMiddleware.map((m) => m.name || "anonymous")
|
|
295
|
+
]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
afterStart(_server, _container) {
|
|
300
|
+
if (!this.enabled) return;
|
|
301
|
+
log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
|
|
302
|
+
}
|
|
303
|
+
shutdown() {
|
|
304
|
+
this.stopErrorWatch?.();
|
|
305
|
+
this.adapterStatuses[this.name] = "stopped";
|
|
306
|
+
}
|
|
307
|
+
/** Find the public/devtools directory relative to the built dist or source */
|
|
308
|
+
resolvePublicDir() {
|
|
309
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
310
|
+
const candidates = [
|
|
311
|
+
join(thisDir, "..", "public", "devtools"),
|
|
312
|
+
join(thisDir, "..", "..", "public", "devtools")
|
|
313
|
+
];
|
|
314
|
+
for (const dir of candidates) {
|
|
315
|
+
if (existsSync(join(dir, "index.html"))) return dir;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
export {
|
|
321
|
+
DevToolsAdapter
|
|
322
|
+
};
|
|
323
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 } 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 const express = require('express')\n router.use(express.static(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","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","express","require","static","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
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forinda/kickjs-devtools",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Development introspection dashboard for KickJS — routes, DI container, metrics, health",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kickjs",
|
|
7
|
+
"devtools",
|
|
8
|
+
"dashboard",
|
|
9
|
+
"debug",
|
|
10
|
+
"introspection",
|
|
11
|
+
"routes",
|
|
12
|
+
"container",
|
|
13
|
+
"metrics"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"public"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"reflect-metadata": "^0.2.2",
|
|
30
|
+
"@forinda/kickjs-core": "1.1.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"express": "^5.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/express": "^5.0.6",
|
|
37
|
+
"@types/node": "^24.5.2",
|
|
38
|
+
"express": "^5.1.0",
|
|
39
|
+
"tsup": "^8.5.0",
|
|
40
|
+
"typescript": "^5.9.2",
|
|
41
|
+
"vitest": "^3.2.4"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"author": "Felix Orinda",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://forinda.github.io/kick-js/",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/forinda/kick-js.git",
|
|
55
|
+
"directory": "packages/devtools"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/forinda/kick-js/issues"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"dev": "tsup --watch",
|
|
63
|
+
"test": "vitest run --passWithNoTests",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"clean": "rm -rf dist .turbo"
|
|
66
|
+
}
|
|
67
|
+
}
|