@forinda/kickjs-devtools 1.3.1 → 1.4.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/dist/adapter.d.ts +109 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/devtools/index.html +575 -0
- package/dist/devtools/tailwind.global.js +897 -0
- package/dist/devtools/vue.global.min.js +25 -0
- package/dist/index.d.ts +3 -110
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +205 -1
- package/package.json +7 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,110 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
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 appRef;
|
|
91
|
-
private adapterStatuses;
|
|
92
|
-
private stopErrorWatch;
|
|
93
|
-
private peerAdapters;
|
|
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;
|
|
101
|
-
beforeMount(app: any, container: Container): void;
|
|
102
|
-
middleware(): AdapterMiddleware[];
|
|
103
|
-
onRouteMount(controllerClass: any, mountPath: string): void;
|
|
104
|
-
afterStart(_server: any, _container: Container): void;
|
|
105
|
-
shutdown(): void;
|
|
106
|
-
/** Find the public/devtools directory relative to the built dist or source */
|
|
107
|
-
private resolvePublicDir;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export { DevToolsAdapter, type DevToolsOptions };
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
export { DevToolsAdapter, type DevToolsOptions } from './adapter';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAA;AAEzB,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,205 @@
|
|
|
1
|
-
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { Router as v, static as A } from "express";
|
|
3
|
+
import { dirname as b, join as c } from "node:path";
|
|
4
|
+
import { fileURLToPath as R } from "node:url";
|
|
5
|
+
import { existsSync as y, readFileSync as E } from "node:fs";
|
|
6
|
+
import { randomBytes as D } from "node:crypto";
|
|
7
|
+
import { METADATA as f, computed as p, createLogger as S, reactive as C, ref as l, watch as m } from "@forinda/kickjs-core";
|
|
8
|
+
var h = S("DevTools"), j = class {
|
|
9
|
+
name = "DevToolsAdapter";
|
|
10
|
+
basePath;
|
|
11
|
+
enabled;
|
|
12
|
+
exposeConfig;
|
|
13
|
+
configPrefixes;
|
|
14
|
+
errorRateThreshold;
|
|
15
|
+
secret;
|
|
16
|
+
requestCount;
|
|
17
|
+
errorCount;
|
|
18
|
+
clientErrorCount;
|
|
19
|
+
startedAt;
|
|
20
|
+
errorRate;
|
|
21
|
+
uptimeSeconds;
|
|
22
|
+
routeLatency;
|
|
23
|
+
routes = [];
|
|
24
|
+
container = null;
|
|
25
|
+
appRef = null;
|
|
26
|
+
adapterStatuses = {};
|
|
27
|
+
stopErrorWatch = null;
|
|
28
|
+
peerAdapters = [];
|
|
29
|
+
constructor(r = {}) {
|
|
30
|
+
if (this.basePath = r.basePath ?? "/_debug", this.enabled = r.enabled ?? process.env.NODE_ENV !== "production", this.exposeConfig = r.exposeConfig ?? !1, this.configPrefixes = r.configPrefixes ?? ["APP_", "NODE_ENV"], this.errorRateThreshold = r.errorRateThreshold ?? 0.5, this.peerAdapters = r.adapters ?? [], r.secret === !1 ? this.secret = !1 : r.secret ? this.secret = r.secret : this.secret = D(16).toString("hex"), this.requestCount = l(0), this.errorCount = l(0), this.clientErrorCount = l(0), this.startedAt = l(Date.now()), this.routeLatency = C({}), this.errorRate = p(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0), this.uptimeSeconds = p(() => Math.floor((Date.now() - this.startedAt.value) / 1e3)), r.onErrorRateExceeded) {
|
|
31
|
+
const n = r.onErrorRateExceeded, s = this.errorRateThreshold;
|
|
32
|
+
this.stopErrorWatch = m(this.errorRate, (i) => {
|
|
33
|
+
i > s && n(i);
|
|
34
|
+
});
|
|
35
|
+
} else this.stopErrorWatch = m(this.errorRate, (n) => {
|
|
36
|
+
n > this.errorRateThreshold && h.warn(`Error rate elevated: ${(n * 100).toFixed(1)}%`);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
getPeerAdapters() {
|
|
40
|
+
const r = this.appRef?.__kickApp;
|
|
41
|
+
return r && typeof r.getAdapters == "function" ? r.getAdapters() : this.peerAdapters;
|
|
42
|
+
}
|
|
43
|
+
beforeMount(r, n) {
|
|
44
|
+
if (!this.enabled) return;
|
|
45
|
+
this.appRef = r, this.container = n, this.startedAt.value = Date.now(), this.routes = [], this.adapterStatuses[this.name] = "running";
|
|
46
|
+
const s = v();
|
|
47
|
+
if (this.secret !== !1) {
|
|
48
|
+
const o = this.secret;
|
|
49
|
+
s.use((e, t, a) => {
|
|
50
|
+
if ((e.headers["x-devtools-token"] ?? e.query?.token) === o || e.path === "/" && e.method === "GET" && !e.query?.token || e.path.endsWith(".js") || e.path.endsWith(".css")) return a();
|
|
51
|
+
t.status(403).json({ error: "Forbidden — invalid or missing devtools token" });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
s.get("/routes", (o, e) => {
|
|
55
|
+
e.json({ routes: this.routes });
|
|
56
|
+
}), s.get("/container", (o, e) => {
|
|
57
|
+
const t = this.container?.getRegistrations() ?? [];
|
|
58
|
+
e.json({
|
|
59
|
+
registrations: t,
|
|
60
|
+
count: t.length
|
|
61
|
+
});
|
|
62
|
+
}), s.get("/metrics", (o, e) => {
|
|
63
|
+
e.json({
|
|
64
|
+
requests: this.requestCount.value,
|
|
65
|
+
serverErrors: this.errorCount.value,
|
|
66
|
+
clientErrors: this.clientErrorCount.value,
|
|
67
|
+
errorRate: this.errorRate.value,
|
|
68
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
69
|
+
startedAt: new Date(this.startedAt.value).toISOString(),
|
|
70
|
+
routeLatency: this.routeLatency
|
|
71
|
+
});
|
|
72
|
+
}), s.get("/health", (o, e) => {
|
|
73
|
+
const t = this.errorRate.value < this.errorRateThreshold, a = t ? "healthy" : "degraded";
|
|
74
|
+
e.status(t ? 200 : 503).json({
|
|
75
|
+
status: a,
|
|
76
|
+
errorRate: this.errorRate.value,
|
|
77
|
+
uptime: this.uptimeSeconds.value,
|
|
78
|
+
adapters: this.adapterStatuses
|
|
79
|
+
});
|
|
80
|
+
}), s.get("/state", (o, e) => {
|
|
81
|
+
const t = this.getPeerAdapters().find((a) => a.name === "WsAdapter" && typeof a.getStats == "function");
|
|
82
|
+
e.json({
|
|
83
|
+
reactive: {
|
|
84
|
+
requestCount: this.requestCount.value,
|
|
85
|
+
errorCount: this.errorCount.value,
|
|
86
|
+
clientErrorCount: this.clientErrorCount.value,
|
|
87
|
+
errorRate: this.errorRate.value,
|
|
88
|
+
uptimeSeconds: this.uptimeSeconds.value,
|
|
89
|
+
startedAt: new Date(this.startedAt.value).toISOString()
|
|
90
|
+
},
|
|
91
|
+
routes: this.routes.length,
|
|
92
|
+
container: this.container?.getRegistrations().length ?? 0,
|
|
93
|
+
routeLatency: this.routeLatency,
|
|
94
|
+
...t ? { ws: t.getStats() } : {}
|
|
95
|
+
});
|
|
96
|
+
}), s.get("/ws", (o, e) => {
|
|
97
|
+
const t = this.getPeerAdapters().find((a) => a.name === "WsAdapter" && typeof a.getStats == "function");
|
|
98
|
+
if (!t) {
|
|
99
|
+
e.json({
|
|
100
|
+
enabled: !1,
|
|
101
|
+
message: "WsAdapter not found"
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
e.json({
|
|
106
|
+
enabled: !0,
|
|
107
|
+
...t.getStats()
|
|
108
|
+
});
|
|
109
|
+
}), s.get("/queues", async (o, e) => {
|
|
110
|
+
const t = this.getPeerAdapters().find((a) => a.name === "QueueAdapter" && typeof a.getQueueNames == "function");
|
|
111
|
+
if (!t) {
|
|
112
|
+
e.json({
|
|
113
|
+
enabled: !1,
|
|
114
|
+
message: "QueueAdapter not found"
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const a = t.getQueueNames?.() ?? [], u = [];
|
|
120
|
+
for (const d of a) {
|
|
121
|
+
const g = await t.getQueueStats?.(d);
|
|
122
|
+
u.push({
|
|
123
|
+
name: d,
|
|
124
|
+
...g
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
e.json({
|
|
128
|
+
enabled: !0,
|
|
129
|
+
queues: u
|
|
130
|
+
});
|
|
131
|
+
} catch {
|
|
132
|
+
e.json({
|
|
133
|
+
enabled: !0,
|
|
134
|
+
queues: [],
|
|
135
|
+
error: "Failed to fetch queue stats"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}), this.exposeConfig && s.get("/config", (o, e) => {
|
|
139
|
+
const t = {};
|
|
140
|
+
for (const [a, u] of Object.entries(process.env))
|
|
141
|
+
u !== void 0 && (t[a] = this.configPrefixes.some((d) => a.startsWith(d)) ? u : "[REDACTED]");
|
|
142
|
+
e.json({ config: t });
|
|
143
|
+
});
|
|
144
|
+
const i = this.resolvePublicDir();
|
|
145
|
+
if (i) {
|
|
146
|
+
s.use(A(i));
|
|
147
|
+
const o = E(c(i, "index.html"), "utf-8");
|
|
148
|
+
s.get("/", (e, t) => {
|
|
149
|
+
const a = o.replace("<body", `<body data-base="${this.basePath}"`);
|
|
150
|
+
t.type("html").send(a);
|
|
151
|
+
});
|
|
152
|
+
} else s.get("/", (o, e) => {
|
|
153
|
+
e.type("html").send("<h1>DevTools: public directory not found</h1>");
|
|
154
|
+
});
|
|
155
|
+
r.use(this.basePath, s), this.secret ? (h.info(`DevTools mounted at ${this.basePath} [token: ${this.secret}]`), h.info(`Access: ${this.basePath}?token=${this.secret}`)) : h.info(`DevTools mounted at ${this.basePath} [no guard]`);
|
|
156
|
+
}
|
|
157
|
+
middleware() {
|
|
158
|
+
return this.enabled ? [{
|
|
159
|
+
handler: (r, n, s) => {
|
|
160
|
+
const i = Date.now();
|
|
161
|
+
this.requestCount.value++, n.on("finish", () => {
|
|
162
|
+
n.statusCode >= 500 ? this.errorCount.value++ : n.statusCode >= 400 && this.clientErrorCount.value++;
|
|
163
|
+
const o = `${r.method} ${r.route?.path ?? r.path}`, e = Date.now() - i;
|
|
164
|
+
this.routeLatency[o] || (this.routeLatency[o] = {
|
|
165
|
+
count: 0,
|
|
166
|
+
totalMs: 0,
|
|
167
|
+
minMs: 1 / 0,
|
|
168
|
+
maxMs: 0
|
|
169
|
+
});
|
|
170
|
+
const t = this.routeLatency[o];
|
|
171
|
+
t.count++, t.totalMs += e, t.minMs = Math.min(t.minMs, e), t.maxMs = Math.max(t.maxMs, e);
|
|
172
|
+
}), s();
|
|
173
|
+
},
|
|
174
|
+
phase: "beforeGlobal"
|
|
175
|
+
}] : [];
|
|
176
|
+
}
|
|
177
|
+
onRouteMount(r, n) {
|
|
178
|
+
if (!this.enabled) return;
|
|
179
|
+
const s = Reflect.getMetadata(f.ROUTES, r) ?? [], i = Reflect.getMetadata(f.CLASS_MIDDLEWARES, r) ?? [];
|
|
180
|
+
for (const o of s) {
|
|
181
|
+
const e = Reflect.getMetadata(f.METHOD_MIDDLEWARES, r.prototype, o.handlerName) ?? [];
|
|
182
|
+
this.routes.push({
|
|
183
|
+
method: o.method.toUpperCase(),
|
|
184
|
+
path: `${n}${o.path === "/" ? "" : o.path}`,
|
|
185
|
+
controller: r.name,
|
|
186
|
+
handler: o.handlerName,
|
|
187
|
+
middleware: [...i.map((t) => t.name || "anonymous"), ...e.map((t) => t.name || "anonymous")]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
afterStart(r, n) {
|
|
192
|
+
this.enabled && h.info(`DevTools ready — ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
|
|
193
|
+
}
|
|
194
|
+
shutdown() {
|
|
195
|
+
this.stopErrorWatch?.(), this.adapterStatuses[this.name] = "stopped";
|
|
196
|
+
}
|
|
197
|
+
resolvePublicDir() {
|
|
198
|
+
const r = b(R(import.meta.url)), n = [c(r, "..", "public", "devtools"), c(r, "..", "..", "public", "devtools")];
|
|
199
|
+
for (const s of n) if (y(c(s, "index.html"))) return s;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
export {
|
|
204
|
+
j as DevToolsAdapter
|
|
205
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-devtools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Development introspection dashboard for KickJS — routes, DI container, metrics, health",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"@forinda/kickjs-mailer",
|
|
36
36
|
"@forinda/kickjs-queue",
|
|
37
37
|
"@forinda/kickjs-multi-tenant",
|
|
38
|
-
"@forinda/kickjs-notifications"
|
|
38
|
+
"@forinda/kickjs-notifications",
|
|
39
|
+
"vite"
|
|
39
40
|
],
|
|
40
41
|
"type": "module",
|
|
41
42
|
"main": "dist/index.js",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
],
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"reflect-metadata": "^0.2.2",
|
|
55
|
-
"@forinda/kickjs-core": "1.
|
|
56
|
+
"@forinda/kickjs-core": "1.4.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"express": "^5.1.0"
|
|
@@ -61,7 +62,6 @@
|
|
|
61
62
|
"@types/express": "^5.0.6",
|
|
62
63
|
"@types/node": "^24.5.2",
|
|
63
64
|
"express": "^5.1.0",
|
|
64
|
-
"tsup": "^8.5.0",
|
|
65
65
|
"typescript": "^5.9.2",
|
|
66
66
|
"vitest": "^3.2.4"
|
|
67
67
|
},
|
|
@@ -83,8 +83,9 @@
|
|
|
83
83
|
"url": "https://github.com/forinda/kick-js/issues"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
|
-
"build": "
|
|
87
|
-
"
|
|
86
|
+
"build": "vite build && pnpm build:types",
|
|
87
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
88
|
+
"dev": "vite build --watch",
|
|
88
89
|
"test": "vitest run --passWithNoTests",
|
|
89
90
|
"typecheck": "tsc --noEmit",
|
|
90
91
|
"clean": "rm -rf dist .turbo"
|