@warlock.js/devtools 5.31.0 → 5.33.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/CHANGELOG.md +15 -0
- package/cjs/ai-source-lia4YS30.cjs +212 -0
- package/cjs/ai-source-lia4YS30.cjs.map +1 -0
- package/cjs/index.cjs +2 -727
- package/cjs/{server-CQ0qhO8c.cjs → server-BNlyhwiR.cjs} +62 -3
- package/cjs/server-BNlyhwiR.cjs.map +1 -0
- package/cjs/src-CiNStw0i.cjs +699 -0
- package/cjs/src-CiNStw0i.cjs.map +1 -0
- package/esm/caller-location.mjs.map +1 -1
- package/esm/devtools-connector.d.mts +2 -4
- package/esm/devtools-connector.d.mts.map +1 -1
- package/esm/devtools-connector.mjs +6 -0
- package/esm/devtools-connector.mjs.map +1 -1
- package/esm/index.d.mts +2 -1
- package/esm/process-panel.mjs +16 -0
- package/esm/process-panel.mjs.map +1 -0
- package/esm/server/ai-routes.mjs +32 -0
- package/esm/server/ai-routes.mjs.map +1 -0
- package/esm/server/guard.mjs +1 -1
- package/esm/server/guard.mjs.map +1 -1
- package/esm/server/index.mjs +4 -0
- package/esm/server/index.mjs.map +1 -1
- package/esm/server/process-routes.mjs +11 -0
- package/esm/server/process-routes.mjs.map +1 -0
- package/esm/server/requests-routes.mjs +1 -0
- package/esm/server/requests-routes.mjs.map +1 -1
- package/esm/server/stream-route.mjs +4 -0
- package/esm/server/stream-route.mjs.map +1 -1
- package/esm/sources/ai-source.d.mts +29 -0
- package/esm/sources/ai-source.d.mts.map +1 -0
- package/esm/sources/ai-source.mjs +136 -0
- package/esm/sources/ai-source.mjs.map +1 -0
- package/esm/sources/log-source.mjs +1 -1
- package/esm/sources/query-source.mjs +1 -1
- package/esm/types.d.mts +45 -30
- package/esm/types.d.mts.map +1 -1
- package/esm/ui-root.mjs.map +1 -1
- package/llms-full.txt +85 -1
- package/llms.txt +1 -1
- package/package.json +9 -5
- package/skills/devtools-overview/SKILL.md +85 -1
- package/ui/app.css +13 -0
- package/ui/app.js +213 -16
- package/ui/index.html +2 -0
- package/cjs/index.cjs.map +0 -1
- package/cjs/server-CQ0qhO8c.cjs.map +0 -1
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
const require_ai_source = require("./ai-source-lia4YS30.cjs");
|
|
2
|
+
const require_server = require("./server-BNlyhwiR.cjs");
|
|
3
|
+
let _warlock_js_core = require("@warlock.js/core");
|
|
4
|
+
let _warlock_js_logger = require("@warlock.js/logger");
|
|
5
|
+
let node_url = require("node:url");
|
|
6
|
+
|
|
7
|
+
//#region ../../packages/devtools/src/process-panel.ts
|
|
8
|
+
/** Load app-owned generated registration only when devtools boots in development. */
|
|
9
|
+
async function loadProcessPanel() {
|
|
10
|
+
const core = await import("@warlock.js/core");
|
|
11
|
+
if (!("srcPath" in core)) return void 0;
|
|
12
|
+
const { srcPath } = core;
|
|
13
|
+
const { existsSync } = await import("node:fs");
|
|
14
|
+
const { pathToFileURL } = await import("node:url");
|
|
15
|
+
const file = srcPath("app/processes/devtools.ts");
|
|
16
|
+
if (!existsSync(file)) return void 0;
|
|
17
|
+
return (await import(pathToFileURL(file).href)).processPanel;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region ../../packages/devtools/src/n-plus-one.ts
|
|
22
|
+
/** Finds repeated query shapes whose occurrence count reaches the supplied threshold. */
|
|
23
|
+
function detectNPlusOne(queries, threshold) {
|
|
24
|
+
const groups = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const query of queries) groups.set(query.shape, [...groups.get(query.shape) ?? [], query]);
|
|
26
|
+
return [...groups.entries()].filter(([, grouped]) => grouped.length >= threshold).sort(([, left], [, right]) => right.length - left.length).map(([shape, grouped]) => ({
|
|
27
|
+
type: "n+1",
|
|
28
|
+
shape,
|
|
29
|
+
count: grouped.length,
|
|
30
|
+
message: `N+1: ${grouped.length}× "${truncate(shape, 120)}"`,
|
|
31
|
+
...mostFrequentCaller(grouped) ? { caller: mostFrequentCaller(grouped) } : {}
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
function truncate(value, maximum) {
|
|
35
|
+
return value.length > maximum ? `${value.slice(0, maximum - 1)}…` : value;
|
|
36
|
+
}
|
|
37
|
+
function mostFrequentCaller(queries) {
|
|
38
|
+
const callers = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const query of queries) {
|
|
40
|
+
if (!query.caller) continue;
|
|
41
|
+
const key = `${query.caller.file}:${query.caller.line}`;
|
|
42
|
+
const found = callers.get(key);
|
|
43
|
+
callers.set(key, found ? {
|
|
44
|
+
...found,
|
|
45
|
+
count: found.count + 1
|
|
46
|
+
} : {
|
|
47
|
+
caller: query.caller,
|
|
48
|
+
count: 1
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return [...callers.values()].sort((left, right) => right.count - left.count)[0]?.caller;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region ../../packages/devtools/src/query-shape.ts
|
|
56
|
+
const numericLiteral = /(?<![\w$])[-+]?(?:\d+(?:\.\d+)?|\.\d+)(?![\w$])/g;
|
|
57
|
+
const quotedLiteral = /'(?:''|[^'])*'/g;
|
|
58
|
+
/** Returns a binding-independent shape for a database query. */
|
|
59
|
+
function queryShape(query) {
|
|
60
|
+
if (query.driver === "postgres") return (query.sql ?? "").replace(quotedLiteral, "?").replace(numericLiteral, "?").replace(/\s+/g, " ").trim();
|
|
61
|
+
return `${query.collection ?? ""}.${query.command ?? ""} ${JSON.stringify(normalizeMongoValue(query.pipeline))}`;
|
|
62
|
+
}
|
|
63
|
+
function normalizeMongoValue(value) {
|
|
64
|
+
if (isMongoLeaf(value)) return "?";
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
if (value.every(isMongoLeaf)) return ["?"];
|
|
67
|
+
return value.map(normalizeMongoValue);
|
|
68
|
+
}
|
|
69
|
+
const record = value;
|
|
70
|
+
return Object.fromEntries(Object.keys(record).map((key) => [key, normalizeMongoValue(record[key])]));
|
|
71
|
+
}
|
|
72
|
+
function isMongoLeaf(value) {
|
|
73
|
+
if (value === null || typeof value !== "object") return true;
|
|
74
|
+
if (value instanceof Date) return true;
|
|
75
|
+
const record = value;
|
|
76
|
+
return typeof record.toHexString === "function" || typeof record.$oid === "string";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region ../../packages/devtools/src/collector.ts
|
|
81
|
+
const mailLimit = 100;
|
|
82
|
+
const logLimit = 1e3;
|
|
83
|
+
const backgroundLimit = 500;
|
|
84
|
+
/**
|
|
85
|
+
* Creates the in-memory devtools store.
|
|
86
|
+
*
|
|
87
|
+
* Read methods return shallow structural copies, including copied arrays, so consumers
|
|
88
|
+
* cannot mutate collector-owned arrays. The individual nested entries are retained.
|
|
89
|
+
*/
|
|
90
|
+
function createCollector(options) {
|
|
91
|
+
const requests = /* @__PURE__ */ new Map();
|
|
92
|
+
const mails = /* @__PURE__ */ new Map();
|
|
93
|
+
const logs = [];
|
|
94
|
+
const background = {
|
|
95
|
+
queries: [],
|
|
96
|
+
cache: [],
|
|
97
|
+
logs: []
|
|
98
|
+
};
|
|
99
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
100
|
+
let querySequence = 0;
|
|
101
|
+
const emit = (event) => {
|
|
102
|
+
for (const listener of listeners) try {
|
|
103
|
+
listener(event);
|
|
104
|
+
} catch {}
|
|
105
|
+
};
|
|
106
|
+
const addBackground = (list, value) => {
|
|
107
|
+
list.push(value);
|
|
108
|
+
if (list.length > backgroundLimit) list.shift();
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
startRequest(request) {
|
|
112
|
+
requests.delete(request.id);
|
|
113
|
+
requests.set(request.id, {
|
|
114
|
+
...request,
|
|
115
|
+
phases: [],
|
|
116
|
+
queries: [],
|
|
117
|
+
cache: [],
|
|
118
|
+
logs: [],
|
|
119
|
+
mails: [],
|
|
120
|
+
warnings: []
|
|
121
|
+
});
|
|
122
|
+
while (requests.size > options.maxRequests) requests.delete(requests.keys().next().value);
|
|
123
|
+
},
|
|
124
|
+
endRequest(id, result) {
|
|
125
|
+
const request = requests.get(id);
|
|
126
|
+
if (!request) return;
|
|
127
|
+
request.status = result.status;
|
|
128
|
+
request.durationMs = result.durationMs;
|
|
129
|
+
request.error = result.error;
|
|
130
|
+
if (result.route !== void 0) request.route = result.route;
|
|
131
|
+
request.warnings = detectNPlusOne(request.queries, options.nPlusOneThreshold);
|
|
132
|
+
emit({
|
|
133
|
+
type: "request",
|
|
134
|
+
data: summary(request)
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
addPhase(requestId, phase) {
|
|
138
|
+
requests.get(requestId)?.phases.push(phase);
|
|
139
|
+
},
|
|
140
|
+
addQuery(requestId, query) {
|
|
141
|
+
const stored = {
|
|
142
|
+
...query,
|
|
143
|
+
id: `q${++querySequence}`,
|
|
144
|
+
shape: queryShape(query)
|
|
145
|
+
};
|
|
146
|
+
const request = requestId ? requests.get(requestId) : void 0;
|
|
147
|
+
if (request) request.queries.push(stored);
|
|
148
|
+
else addBackground(background.queries, stored);
|
|
149
|
+
return stored;
|
|
150
|
+
},
|
|
151
|
+
addCacheOperation(requestId, operation) {
|
|
152
|
+
const request = requestId ? requests.get(requestId) : void 0;
|
|
153
|
+
if (request) request.cache.push(operation);
|
|
154
|
+
else addBackground(background.cache, operation);
|
|
155
|
+
},
|
|
156
|
+
addLog(requestId, entry) {
|
|
157
|
+
const request = requestId ? requests.get(requestId) : void 0;
|
|
158
|
+
if (request) request.logs.push(entry);
|
|
159
|
+
else addBackground(background.logs, entry);
|
|
160
|
+
logs.unshift(entry);
|
|
161
|
+
if (logs.length > logLimit) logs.pop();
|
|
162
|
+
emit({
|
|
163
|
+
type: "log",
|
|
164
|
+
data: entry
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
addMail(mail) {
|
|
168
|
+
mails.delete(mail.id);
|
|
169
|
+
mails.set(mail.id, mail);
|
|
170
|
+
while (mails.size > mailLimit) mails.delete(mails.keys().next().value);
|
|
171
|
+
if (mail.requestId) requests.get(mail.requestId)?.mails.push(mail.id);
|
|
172
|
+
emit({
|
|
173
|
+
type: "mail",
|
|
174
|
+
data: mailSummary(mail)
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
listRequests() {
|
|
178
|
+
return [...requests.values()].reverse().map(summary);
|
|
179
|
+
},
|
|
180
|
+
getRequest(id) {
|
|
181
|
+
const request = requests.get(id);
|
|
182
|
+
return request && copyRequest(request);
|
|
183
|
+
},
|
|
184
|
+
listMails() {
|
|
185
|
+
return [...mails.values()].reverse().map(mailSummary);
|
|
186
|
+
},
|
|
187
|
+
getMail(id) {
|
|
188
|
+
const mail = mails.get(id);
|
|
189
|
+
return mail && {
|
|
190
|
+
...mail,
|
|
191
|
+
to: [...mail.to],
|
|
192
|
+
cc: [...mail.cc],
|
|
193
|
+
bcc: [...mail.bcc],
|
|
194
|
+
headers: { ...mail.headers },
|
|
195
|
+
attachments: [...mail.attachments]
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
listLogs(filter = {}) {
|
|
199
|
+
const module = filter.module?.toLowerCase();
|
|
200
|
+
return logs.filter((entry) => (!filter.level || entry.level === filter.level) && (!module || entry.module.toLowerCase().includes(module))).map((entry) => ({ ...entry }));
|
|
201
|
+
},
|
|
202
|
+
getBackground() {
|
|
203
|
+
return {
|
|
204
|
+
queries: [...background.queries],
|
|
205
|
+
cache: [...background.cache],
|
|
206
|
+
logs: [...background.logs]
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
subscribe(listener) {
|
|
210
|
+
listeners.add(listener);
|
|
211
|
+
return () => listeners.delete(listener);
|
|
212
|
+
},
|
|
213
|
+
clear() {
|
|
214
|
+
requests.clear();
|
|
215
|
+
mails.clear();
|
|
216
|
+
logs.length = 0;
|
|
217
|
+
background.queries.length = 0;
|
|
218
|
+
background.cache.length = 0;
|
|
219
|
+
background.logs.length = 0;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function summary(request) {
|
|
224
|
+
const { id, method, path, route, status, startedAt, durationMs } = request;
|
|
225
|
+
return {
|
|
226
|
+
id,
|
|
227
|
+
method,
|
|
228
|
+
path,
|
|
229
|
+
route,
|
|
230
|
+
status,
|
|
231
|
+
startedAt,
|
|
232
|
+
durationMs,
|
|
233
|
+
queryCount: request.queries.length,
|
|
234
|
+
warningCount: request.warnings.length
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function mailSummary(mail) {
|
|
238
|
+
const { id, requestId, capturedAt, to, subject } = mail;
|
|
239
|
+
return {
|
|
240
|
+
id,
|
|
241
|
+
requestId,
|
|
242
|
+
capturedAt,
|
|
243
|
+
to: [...to],
|
|
244
|
+
subject
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function copyRequest(request) {
|
|
248
|
+
return {
|
|
249
|
+
...request,
|
|
250
|
+
phases: [...request.phases],
|
|
251
|
+
queries: [...request.queries],
|
|
252
|
+
cache: [...request.cache],
|
|
253
|
+
logs: [...request.logs],
|
|
254
|
+
mails: [...request.mails],
|
|
255
|
+
warnings: [...request.warnings]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region ../../packages/devtools/src/options.ts
|
|
261
|
+
/** Resolves dashboard options and rejects invalid capacity or URL-prefix values. */
|
|
262
|
+
function resolveDevtoolsOptions(options = {}) {
|
|
263
|
+
const path = (options.path ?? "/__warlock").replace(/\/+$/, "");
|
|
264
|
+
const maxRequests = options.maxRequests ?? 200;
|
|
265
|
+
const nPlusOneThreshold = options.nPlusOneThreshold ?? 5;
|
|
266
|
+
if (!path || !path.startsWith("/")) throw new Error("Devtools option \"path\" must be a non-root path starting with \"/\".");
|
|
267
|
+
if (maxRequests < 1) throw new Error("Devtools option \"maxRequests\" must be at least 1.");
|
|
268
|
+
if (nPlusOneThreshold < 2) throw new Error("Devtools option \"nPlusOneThreshold\" must be at least 2.");
|
|
269
|
+
return {
|
|
270
|
+
path,
|
|
271
|
+
maxRequests,
|
|
272
|
+
nPlusOneThreshold
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region ../../packages/devtools/src/current-request-id.ts
|
|
278
|
+
var current_request_id_exports = /* @__PURE__ */ require_server.__exportAll({ currentRequestId: () => currentRequestId });
|
|
279
|
+
/**
|
|
280
|
+
* The current request's id (core's `request.id`, threaded through
|
|
281
|
+
* `AsyncLocalStorage`), or `undefined` outside a request — sources feed
|
|
282
|
+
* devtools' background bucket in that case.
|
|
283
|
+
*
|
|
284
|
+
* Only ever reached through `./sources/*`, which the connector imports
|
|
285
|
+
* dynamically inside `boot()` — by then core is already running, so this
|
|
286
|
+
* static import never drags core's runtime graph into config-load time (see
|
|
287
|
+
* `devtools-connector.ts`).
|
|
288
|
+
*/
|
|
289
|
+
function currentRequestId() {
|
|
290
|
+
try {
|
|
291
|
+
return _warlock_js_core.requestContext.getRequest()?.id;
|
|
292
|
+
} catch {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region ../../packages/devtools/src/sources/cache-source.ts
|
|
299
|
+
/** Cache events the dashboard's waterfall and background bucket care about. */
|
|
300
|
+
const EVENT_TYPES = [
|
|
301
|
+
"hit",
|
|
302
|
+
"miss",
|
|
303
|
+
"set",
|
|
304
|
+
"removed",
|
|
305
|
+
"invalidated",
|
|
306
|
+
"flushed"
|
|
307
|
+
];
|
|
308
|
+
/**
|
|
309
|
+
* Feeds cache hits, misses and mutations, from `@warlock.js/cache`, into the
|
|
310
|
+
* collector. A no-op when `@warlock.js/cache` isn't installed — it is an
|
|
311
|
+
* optional peer.
|
|
312
|
+
*/
|
|
313
|
+
async function attachCacheSource(collector) {
|
|
314
|
+
let cache;
|
|
315
|
+
try {
|
|
316
|
+
({cache} = await import("@warlock.js/cache"));
|
|
317
|
+
} catch {
|
|
318
|
+
return () => {};
|
|
319
|
+
}
|
|
320
|
+
const handlers = EVENT_TYPES.map((type) => {
|
|
321
|
+
const handler = (data) => {
|
|
322
|
+
const operation = {
|
|
323
|
+
type,
|
|
324
|
+
driver: data.driver,
|
|
325
|
+
key: data.key,
|
|
326
|
+
tags: data.tags,
|
|
327
|
+
keys: data.keys,
|
|
328
|
+
at: Date.now(),
|
|
329
|
+
durationMs: data.durationMs
|
|
330
|
+
};
|
|
331
|
+
collector.addCacheOperation(currentRequestId(), operation);
|
|
332
|
+
};
|
|
333
|
+
cache.on(type, handler);
|
|
334
|
+
return {
|
|
335
|
+
type,
|
|
336
|
+
handler
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
return () => {
|
|
340
|
+
for (const { type, handler } of handlers) cache.off(type, handler);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region ../../packages/devtools/src/sources/log-source.ts
|
|
346
|
+
/** `LoggingData.type` includes `fatal`, which devtools folds into `error`. */
|
|
347
|
+
function toDevtoolsLevel(type) {
|
|
348
|
+
return type === "fatal" ? "error" : type;
|
|
349
|
+
}
|
|
350
|
+
var DevtoolsLogChannel = class extends _warlock_js_logger.LogChannel {
|
|
351
|
+
constructor(collector) {
|
|
352
|
+
super();
|
|
353
|
+
this.collector = collector;
|
|
354
|
+
this.name = "devtools";
|
|
355
|
+
this.active = true;
|
|
356
|
+
}
|
|
357
|
+
log(data) {
|
|
358
|
+
if (!this.active) return;
|
|
359
|
+
const entry = {
|
|
360
|
+
level: toDevtoolsLevel(data.type),
|
|
361
|
+
module: data.module,
|
|
362
|
+
action: data.action,
|
|
363
|
+
message: data.message,
|
|
364
|
+
at: Date.now(),
|
|
365
|
+
context: require_ai_source.toSafeValue(data.context)
|
|
366
|
+
};
|
|
367
|
+
this.collector.addLog(currentRequestId(), entry);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Feeds every log entry, from `@warlock.js/logger`, into the collector by
|
|
372
|
+
* registering an additional channel. `Logger` has no way to remove a
|
|
373
|
+
* channel, so the disposer flag-ignores further entries instead — the
|
|
374
|
+
* channel stays registered for the process's lifetime.
|
|
375
|
+
*/
|
|
376
|
+
async function attachLogSource(collector) {
|
|
377
|
+
const channel = new DevtoolsLogChannel(collector);
|
|
378
|
+
_warlock_js_logger.log.addChannel(channel);
|
|
379
|
+
return () => {
|
|
380
|
+
channel.active = false;
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region ../../packages/devtools/src/sources/mail-source.ts
|
|
386
|
+
function addressToString(address) {
|
|
387
|
+
if (!address) return void 0;
|
|
388
|
+
return typeof address === "string" ? address : `${address.name} <${address.address}>`;
|
|
389
|
+
}
|
|
390
|
+
function attachmentSize(attachment) {
|
|
391
|
+
if (typeof attachment.content === "string") return Buffer.byteLength(attachment.content);
|
|
392
|
+
if (Buffer.isBuffer(attachment.content)) return attachment.content.length;
|
|
393
|
+
}
|
|
394
|
+
function toDevtoolsAttachment(attachment) {
|
|
395
|
+
return {
|
|
396
|
+
filename: attachment.filename,
|
|
397
|
+
contentType: attachment.contentType,
|
|
398
|
+
size: attachmentSize(attachment)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Feeds mails captured in development mode, from core's `mailEvents`, into
|
|
403
|
+
* the collector. The captured attachments carry their content, but only
|
|
404
|
+
* metadata is stored — the bytes are never kept.
|
|
405
|
+
*/
|
|
406
|
+
async function attachMailSource(collector) {
|
|
407
|
+
const { mailEvents } = await import("@warlock.js/core");
|
|
408
|
+
const subscription = mailEvents.onCaptured((mail) => {
|
|
409
|
+
const { normalized } = mail;
|
|
410
|
+
collector.addMail({
|
|
411
|
+
id: mail.id,
|
|
412
|
+
requestId: currentRequestId(),
|
|
413
|
+
capturedAt: mail.timestamp.getTime(),
|
|
414
|
+
from: addressToString(normalized.from),
|
|
415
|
+
to: normalized.to,
|
|
416
|
+
cc: normalized.cc,
|
|
417
|
+
bcc: normalized.bcc,
|
|
418
|
+
subject: normalized.subject,
|
|
419
|
+
html: normalized.html,
|
|
420
|
+
text: normalized.text,
|
|
421
|
+
headers: normalized.headers,
|
|
422
|
+
attachments: normalized.attachments.map(toDevtoolsAttachment)
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
return () => subscription.unsubscribe();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region ../../packages/devtools/src/caller-location.ts
|
|
430
|
+
const packageDirectory = normalizePath((0, node_url.fileURLToPath)(new URL("../", require("url").pathToFileURL(__filename).href)));
|
|
431
|
+
/** Returns the first non-runtime, non-devtools frame in a V8 stack trace. */
|
|
432
|
+
function captureCallerLocation(stack = (/* @__PURE__ */ new Error()).stack ?? "") {
|
|
433
|
+
for (const line of stack.split("\n")) {
|
|
434
|
+
const frame = parseFrame(line);
|
|
435
|
+
if (!frame || shouldSkip(frame.file)) continue;
|
|
436
|
+
return frame;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function parseFrame(frame) {
|
|
440
|
+
const [, rawFile, line, column] = frame.match(/\bat\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?\s*$/) ?? [];
|
|
441
|
+
if (!rawFile || !line || !column) return void 0;
|
|
442
|
+
return {
|
|
443
|
+
file: normalizePath(rawFile),
|
|
444
|
+
line: Number(line),
|
|
445
|
+
column: Number(column)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/** A drive-letter file URL names a Windows path on whichever host decodes it. */
|
|
449
|
+
const WINDOWS_DRIVE_FILE_URL = /^file:\/\/\/[A-Za-z]:/;
|
|
450
|
+
function normalizePath(path) {
|
|
451
|
+
return (!path.startsWith("file:") ? path : WINDOWS_DRIVE_FILE_URL.test(path) ? (0, node_url.fileURLToPath)(path, { windows: true }) : (0, node_url.fileURLToPath)(path)).replace(/\\/g, "/");
|
|
452
|
+
}
|
|
453
|
+
function shouldSkip(file) {
|
|
454
|
+
return file.startsWith("node:") || file.includes("/node_modules/") || file.startsWith("internal/") || file.startsWith(packageDirectory);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region ../../packages/devtools/src/sources/query-source.ts
|
|
459
|
+
/** Redacts query bindings through the logger's redaction rules. */
|
|
460
|
+
function redactBindings(bindings) {
|
|
461
|
+
if (!bindings) return bindings;
|
|
462
|
+
return (0, _warlock_js_logger.applyRedact)({
|
|
463
|
+
type: "debug",
|
|
464
|
+
module: "devtools",
|
|
465
|
+
action: "query",
|
|
466
|
+
message: void 0,
|
|
467
|
+
context: { bindings }
|
|
468
|
+
}, _warlock_js_logger.log.getRedact()).context.bindings;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Feeds every settled database query, from cascade's data source registry,
|
|
472
|
+
* into the collector. A no-op when `@warlock.js/cascade` isn't installed —
|
|
473
|
+
* it is an optional peer.
|
|
474
|
+
*/
|
|
475
|
+
async function attachQuerySource(collector) {
|
|
476
|
+
let dataSourceRegistry;
|
|
477
|
+
try {
|
|
478
|
+
({dataSourceRegistry} = await import("@warlock.js/cascade"));
|
|
479
|
+
} catch {
|
|
480
|
+
return () => {};
|
|
481
|
+
}
|
|
482
|
+
const listener = (event) => {
|
|
483
|
+
collector.addQuery(currentRequestId(), {
|
|
484
|
+
connection: event.connection,
|
|
485
|
+
driver: event.driver,
|
|
486
|
+
sql: event.sql,
|
|
487
|
+
bindings: require_ai_source.toSafeValue(redactBindings(event.bindings)),
|
|
488
|
+
collection: event.collection,
|
|
489
|
+
command: event.command,
|
|
490
|
+
pipeline: event.pipeline,
|
|
491
|
+
startedAt: event.startedAt,
|
|
492
|
+
durationMs: event.durationMs,
|
|
493
|
+
rowCount: event.rowCount,
|
|
494
|
+
error: event.error === void 0 ? void 0 : event.error instanceof Error ? event.error.message : String(event.error),
|
|
495
|
+
caller: captureCallerLocation()
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
dataSourceRegistry.on("query", listener);
|
|
499
|
+
return () => dataSourceRegistry.off("query", listener);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region ../../packages/devtools/src/sources/tracing-source.ts
|
|
504
|
+
/**
|
|
505
|
+
* Feeds request timelines from core's tracing hooks into the collector: one
|
|
506
|
+
* `startRequest`/`endRequest` pair per HTTP request, with every named phase
|
|
507
|
+
* in between. Requests to the dashboard's own path are never recorded, so
|
|
508
|
+
* the dashboard never watches itself.
|
|
509
|
+
*/
|
|
510
|
+
async function attachTracingSource(collector, options) {
|
|
511
|
+
const { registerTracingHooks } = await import("@warlock.js/core");
|
|
512
|
+
const isDashboardPath = (path) => path.startsWith(options.path);
|
|
513
|
+
return registerTracingHooks({
|
|
514
|
+
onRequestStart(ctx) {
|
|
515
|
+
if (isDashboardPath(ctx.path)) return;
|
|
516
|
+
collector.startRequest({
|
|
517
|
+
id: ctx.requestId,
|
|
518
|
+
method: ctx.method,
|
|
519
|
+
path: ctx.path,
|
|
520
|
+
route: ctx.route,
|
|
521
|
+
startedAt: performance.timeOrigin + performance.now()
|
|
522
|
+
});
|
|
523
|
+
},
|
|
524
|
+
onPhase(ctx, phase) {
|
|
525
|
+
if (isDashboardPath(ctx.path)) return;
|
|
526
|
+
collector.addPhase(ctx.requestId, {
|
|
527
|
+
name: phase.name,
|
|
528
|
+
durationMs: phase.durationMs,
|
|
529
|
+
startedAt: phase.startedAt ?? performance.timeOrigin + performance.now() - phase.durationMs,
|
|
530
|
+
attrs: phase.attrs === void 0 ? void 0 : require_ai_source.toSafeValue(phase.attrs)
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
onRequestEnd(ctx, result) {
|
|
534
|
+
if (isDashboardPath(ctx.path)) return;
|
|
535
|
+
collector.endRequest(ctx.requestId, {
|
|
536
|
+
status: result.status,
|
|
537
|
+
durationMs: result.durationMs,
|
|
538
|
+
error: result.error === void 0 ? void 0 : result.error instanceof Error ? result.error.message : String(result.error),
|
|
539
|
+
route: ctx.route
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region ../../packages/devtools/src/devtools-connector.ts
|
|
547
|
+
/**
|
|
548
|
+
* Boots after the queue connector (priority 11), which is the last
|
|
549
|
+
* built-in/framework connector to claim a priority; devtools has nothing
|
|
550
|
+
* that depends on it, so it simply goes last.
|
|
551
|
+
*/
|
|
552
|
+
const DEVTOOLS_CONNECTOR_PRIORITY = 12;
|
|
553
|
+
/**
|
|
554
|
+
* Construct the devtools connector.
|
|
555
|
+
*
|
|
556
|
+
* Refuses to run outside development: `boot()` checks
|
|
557
|
+
* `Application.runtimeStrategy` and, when it isn't `"development"`, logs one
|
|
558
|
+
* warning and returns without mounting a route or subscribing to anything.
|
|
559
|
+
* In development it attaches every source (tracing, queries, cache, mail,
|
|
560
|
+
* logs), then mounts the dashboard's routes on the HTTP server — before it
|
|
561
|
+
* starts listening, the only point in the boot sequence where mounting is
|
|
562
|
+
* possible (see `queue/src/dashboard-boot.ts` for the same constraint).
|
|
563
|
+
*
|
|
564
|
+
* Apps don't call this: `warlock dev` registers it automatically when the
|
|
565
|
+
* package is installed (`warlock add devtools`). Keep it OUT of
|
|
566
|
+
* `warlock.config.ts` — devtools is a dev dependency, so a production
|
|
567
|
+
* install doesn't have it and a config import of it would fail there.
|
|
568
|
+
*/
|
|
569
|
+
function devtoolsConnector(options = {}) {
|
|
570
|
+
let active = false;
|
|
571
|
+
const disposers = [];
|
|
572
|
+
const connector = {
|
|
573
|
+
name: "devtools",
|
|
574
|
+
priority: 12,
|
|
575
|
+
lifecyclePhase: "late",
|
|
576
|
+
isActive: () => active,
|
|
577
|
+
async boot() {
|
|
578
|
+
const { Application, getHttpServer } = await import("@warlock.js/core");
|
|
579
|
+
const { log } = await import("@warlock.js/logger");
|
|
580
|
+
if (Application.runtimeStrategy !== "development") {
|
|
581
|
+
log.warn("devtools", "disabled", "@warlock.js/devtools only runs in development; it is not mounted");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const resolved = resolveDevtoolsOptions(options);
|
|
585
|
+
const collector = createCollector(resolved);
|
|
586
|
+
await attachSource(log, "tracing", () => attachTracingSource(collector, resolved));
|
|
587
|
+
await attachSource(log, "query", () => attachQuerySource(collector));
|
|
588
|
+
await attachSource(log, "cache", () => attachCacheSource(collector));
|
|
589
|
+
await attachSource(log, "mail", () => attachMailSource(collector));
|
|
590
|
+
await attachSource(log, "log", () => attachLogSource(collector));
|
|
591
|
+
const ai = await require_ai_source.attachAiSource(resolved.maxRequests);
|
|
592
|
+
disposers.push(ai.dispose);
|
|
593
|
+
const { mountDevtoolsRoutes } = await Promise.resolve().then(() => require("./server-BNlyhwiR.cjs"));
|
|
594
|
+
const postmanBuilder = await findPostmanBuilder();
|
|
595
|
+
mountDevtoolsRoutes(getHttpServer(), {
|
|
596
|
+
collector,
|
|
597
|
+
ai,
|
|
598
|
+
processPanel: await loadProcessPanel(),
|
|
599
|
+
options: resolved,
|
|
600
|
+
isDevelopment: () => Application.runtimeStrategy === "development",
|
|
601
|
+
explain: (query) => explainQuery(query),
|
|
602
|
+
listRoutes: () => listRoutes(),
|
|
603
|
+
getOpenApiDocument: () => getOpenApiDocument(),
|
|
604
|
+
getPostmanCollection: postmanBuilder ? async () => unwrapCollection(await postmanBuilder()) : void 0
|
|
605
|
+
});
|
|
606
|
+
disposers.push(collector.subscribe((event) => {
|
|
607
|
+
if (event.type !== "request" || event.data.warningCount === 0) return;
|
|
608
|
+
for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {
|
|
609
|
+
const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : "";
|
|
610
|
+
log.warn("devtools", "n+1", `${warning.message}${where} (${event.data.method} ${event.data.path})`);
|
|
611
|
+
}
|
|
612
|
+
}));
|
|
613
|
+
active = true;
|
|
614
|
+
log.info("devtools", "ready", `devtools ready at ${resolved.path}`);
|
|
615
|
+
},
|
|
616
|
+
async start() {},
|
|
617
|
+
async restart() {
|
|
618
|
+
await connector.shutdown();
|
|
619
|
+
await connector.start();
|
|
620
|
+
},
|
|
621
|
+
async shutdown() {
|
|
622
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
623
|
+
active = false;
|
|
624
|
+
},
|
|
625
|
+
shouldRestart: () => false
|
|
626
|
+
};
|
|
627
|
+
/** Attaches one source, logging and continuing when it fails to attach. */
|
|
628
|
+
async function attachSource(logger, name, attach) {
|
|
629
|
+
try {
|
|
630
|
+
disposers.push(await attach());
|
|
631
|
+
} catch (error) {
|
|
632
|
+
logger.warn("devtools", "source-failed", `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return connector;
|
|
636
|
+
}
|
|
637
|
+
/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */
|
|
638
|
+
async function explainQuery(query) {
|
|
639
|
+
const { dataSourceRegistry } = await import("@warlock.js/cascade");
|
|
640
|
+
const { rows } = await dataSourceRegistry.get(query.connection).raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);
|
|
641
|
+
return rows;
|
|
642
|
+
}
|
|
643
|
+
/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */
|
|
644
|
+
async function listRoutes() {
|
|
645
|
+
const { router } = await import("@warlock.js/core");
|
|
646
|
+
return router.list().map((route) => ({
|
|
647
|
+
method: route.method.toUpperCase(),
|
|
648
|
+
path: route.path,
|
|
649
|
+
name: route.name,
|
|
650
|
+
middleware: route.middleware?.map((middleware) => middleware.name || "anonymous")
|
|
651
|
+
}));
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Builds the OpenAPI 3.1 document from the routes registered in this process —
|
|
655
|
+
* the same generator behind `warlock generate.openapi`. Core is imported
|
|
656
|
+
* lazily so this module stays free of a load-time dependency on it.
|
|
657
|
+
*/
|
|
658
|
+
async function getOpenApiDocument() {
|
|
659
|
+
const { getDevelopmentOpenApiDocument } = await import("@warlock.js/core");
|
|
660
|
+
const { document } = await getDevelopmentOpenApiDocument();
|
|
661
|
+
return document;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Looks up core's Postman collection builder, `getDevelopmentPostmanCollection`.
|
|
665
|
+
* Older cores do not export it; the route then answers 503. The lookup is
|
|
666
|
+
* guarded because a namespace without the export may throw on access. Core is
|
|
667
|
+
* imported lazily, like the OpenAPI document above.
|
|
668
|
+
*/
|
|
669
|
+
async function findPostmanBuilder() {
|
|
670
|
+
try {
|
|
671
|
+
const build = (await import("@warlock.js/core")).getDevelopmentPostmanCollection;
|
|
672
|
+
return typeof build === "function" ? build : void 0;
|
|
673
|
+
} catch {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Core may return the collection itself or wrap it in `{ collection, warnings }`,
|
|
679
|
+
* like the OpenAPI generator's `{ document, warnings }`.
|
|
680
|
+
*/
|
|
681
|
+
function unwrapCollection(result) {
|
|
682
|
+
if (result && typeof result === "object" && "collection" in result) return result.collection;
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
//#endregion
|
|
687
|
+
Object.defineProperty(exports, "current_request_id_exports", {
|
|
688
|
+
enumerable: true,
|
|
689
|
+
get: function () {
|
|
690
|
+
return current_request_id_exports;
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
Object.defineProperty(exports, "devtoolsConnector", {
|
|
694
|
+
enumerable: true,
|
|
695
|
+
get: function () {
|
|
696
|
+
return devtoolsConnector;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
//# sourceMappingURL=src-CiNStw0i.cjs.map
|