@katajs/core 0.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/dist/index.js ADDED
@@ -0,0 +1,838 @@
1
+ // src/module.ts
2
+ function defineModule(spec) {
3
+ if (spec.routes && spec.prefix) {
4
+ return {
5
+ name: spec.name,
6
+ provides: spec.provides,
7
+ requires: spec.requires,
8
+ routes: spec.routes,
9
+ prefix: spec.prefix,
10
+ consumer: spec.consumer
11
+ };
12
+ }
13
+ return {
14
+ name: spec.name,
15
+ provides: spec.provides,
16
+ requires: spec.requires,
17
+ consumer: spec.consumer
18
+ };
19
+ }
20
+
21
+ // src/app.ts
22
+ import { Hono } from "hono";
23
+
24
+ // src/container.ts
25
+ function makeResolver(registry, containerView) {
26
+ const cache = /* @__PURE__ */ new Map();
27
+ const inProgress = /* @__PURE__ */ new Set();
28
+ return function resolve(key) {
29
+ if (cache.has(key)) {
30
+ return cache.get(key);
31
+ }
32
+ if (inProgress.has(key)) {
33
+ const stack = [...inProgress, key].join(" -> ");
34
+ throw new Error(
35
+ `[katajs] Circular dependency detected while resolving '${key}'. Resolution stack: ${stack}`
36
+ );
37
+ }
38
+ const factory = registry.get(key);
39
+ if (!factory) {
40
+ const known = [...registry.keys()].sort().join(", ") || "(none)";
41
+ throw new Error(
42
+ `[katajs] No service registered for key '${key}'. Registered keys: ${known}`
43
+ );
44
+ }
45
+ inProgress.add(key);
46
+ try {
47
+ const instance = factory(containerView);
48
+ cache.set(key, instance);
49
+ return instance;
50
+ } finally {
51
+ inProgress.delete(key);
52
+ }
53
+ };
54
+ }
55
+ function buildRegistry(providesMaps) {
56
+ const registry = /* @__PURE__ */ new Map();
57
+ for (const provides of providesMaps) {
58
+ for (const [key, factory] of Object.entries(provides)) {
59
+ registry.set(key, factory);
60
+ }
61
+ }
62
+ return registry;
63
+ }
64
+ var queueContextStub = new Proxy({}, {
65
+ get(_, prop) {
66
+ throw new Error(
67
+ `[katajs] This service tried to access Hono Context property '${String(prop)}', but it's running in a queue handler (no HTTP context exists). Refactor the service to read what it needs from \`container.env\` / \`container.requestId\` instead.`
68
+ );
69
+ }
70
+ });
71
+ function buildContainer(args) {
72
+ const container = {
73
+ env: args.env,
74
+ c: args.c ?? queueContextStub,
75
+ requestId: args.requestId,
76
+ db: args.db
77
+ };
78
+ container.resolve = makeResolver(
79
+ args.registry,
80
+ container
81
+ );
82
+ container.withTransaction = async (fn) => {
83
+ if (args.inTransaction) {
84
+ return fn(container);
85
+ }
86
+ if (!args.runTransaction) {
87
+ throw new Error(
88
+ "[katajs] withTransaction was called but the configured db adapter does not support transactions. Use an adapter with a 'runTransaction' method."
89
+ );
90
+ }
91
+ return args.runTransaction(args.db, async (txDb) => {
92
+ const txContainer = buildContainer({
93
+ ...args,
94
+ db: txDb,
95
+ inTransaction: true
96
+ });
97
+ return fn(txContainer);
98
+ });
99
+ };
100
+ return container;
101
+ }
102
+
103
+ // src/errors.ts
104
+ var AppError = class extends Error {
105
+ /**
106
+ * Optional structured payload merged into the response body. Subclasses may
107
+ * override as either a property or a getter.
108
+ */
109
+ get publicPayload() {
110
+ return void 0;
111
+ }
112
+ constructor(message) {
113
+ super(message);
114
+ this.name = this.constructor.name;
115
+ }
116
+ };
117
+ var ValidationError = class extends AppError {
118
+ constructor(issues) {
119
+ super(`Validation failed: ${issues.length} issue(s)`);
120
+ this.issues = issues;
121
+ }
122
+ issues;
123
+ status = 400;
124
+ code = "validation_failed";
125
+ publicMessage = "Request validation failed";
126
+ get publicPayload() {
127
+ return { issues: this.issues };
128
+ }
129
+ };
130
+ function errorMapper(opts = {}) {
131
+ return (err, c) => {
132
+ const requestId = c.get("requestId") ?? c.req.header("X-Request-Id") ?? "unknown";
133
+ if (err instanceof AppError) {
134
+ return c.json(
135
+ {
136
+ error: err.code,
137
+ message: err.publicMessage,
138
+ ...err.publicPayload ?? {},
139
+ requestId
140
+ },
141
+ err.status
142
+ );
143
+ }
144
+ opts.onUnhandled?.(err, { c, requestId });
145
+ return c.json(
146
+ {
147
+ error: "internal_error",
148
+ message: "Something went wrong. Please try again.",
149
+ requestId
150
+ },
151
+ 500
152
+ );
153
+ };
154
+ }
155
+
156
+ // src/queues-producer.ts
157
+ function buildTypedQueues(declarations, env) {
158
+ const out = {};
159
+ for (const [name, decl] of Object.entries(declarations)) {
160
+ out[name] = makeTypedQueue(name, decl, env);
161
+ }
162
+ return out;
163
+ }
164
+ function makeTypedQueue(name, decl, env) {
165
+ const binding = env?.[decl.binding];
166
+ return {
167
+ async send(body, options) {
168
+ const validated = validateOrThrow(name, decl, body);
169
+ assertBinding(name, decl.binding, binding);
170
+ await binding.send(validated, options);
171
+ },
172
+ async sendBatch(bodies, options) {
173
+ const validated = bodies.map((b) => validateOrThrow(name, decl, b));
174
+ assertBinding(name, decl.binding, binding);
175
+ if (typeof binding.sendBatch === "function") {
176
+ const messages = validated.map((body) => ({ body }));
177
+ await binding.sendBatch(messages, options);
178
+ return;
179
+ }
180
+ for (const body of validated) {
181
+ await binding.send(body, options);
182
+ }
183
+ }
184
+ };
185
+ }
186
+ function validateOrThrow(queueName, decl, body) {
187
+ try {
188
+ return decl.schema.parse(body);
189
+ } catch (err) {
190
+ const issues = err && typeof err === "object" && "issues" in err && Array.isArray(err.issues) ? err.issues.map((i) => ({
191
+ path: [
192
+ `queues.${queueName}`,
193
+ ...Array.isArray(i.path) ? i.path : []
194
+ ],
195
+ message: typeof i.message === "string" ? i.message : "Invalid"
196
+ })) : [
197
+ {
198
+ path: [`queues.${queueName}`],
199
+ message: err instanceof Error ? err.message : String(err)
200
+ }
201
+ ];
202
+ throw new ValidationError(issues);
203
+ }
204
+ }
205
+ function assertBinding(queueName, bindingName, binding) {
206
+ if (!binding || typeof binding.send !== "function") {
207
+ throw new Error(
208
+ `[katajs] Queue '${queueName}': binding '${bindingName}' is not registered as a producer in wrangler.jsonc, or env doesn't have it. Add a producer entry for this binding in your wrangler config.`
209
+ );
210
+ }
211
+ }
212
+
213
+ // src/middleware.ts
214
+ function containerMiddleware(config) {
215
+ return async (c, next) => {
216
+ const requestId = config.generateRequestId ? config.generateRequestId() : crypto.randomUUID();
217
+ const db = config.db.create(c.env);
218
+ const container = buildContainer({
219
+ env: c.env,
220
+ c,
221
+ requestId,
222
+ db,
223
+ registry: config.registry,
224
+ runTransaction: config.db.runTransaction,
225
+ inTransaction: false
226
+ });
227
+ c.set("container", container);
228
+ c.set("requestId", requestId);
229
+ c.set("resolve", container.resolve);
230
+ c.set("withTransaction", container.withTransaction);
231
+ if (config.queues && Object.keys(config.queues).length > 0) {
232
+ c.set(
233
+ "queues",
234
+ buildTypedQueues(config.queues, c.env)
235
+ );
236
+ } else {
237
+ c.set("queues", {});
238
+ }
239
+ c.header("X-Request-Id", requestId);
240
+ await next();
241
+ };
242
+ }
243
+ function defineMiddleware(handler) {
244
+ return handler;
245
+ }
246
+
247
+ // src/queue.ts
248
+ function defineConsumer(spec) {
249
+ return spec;
250
+ }
251
+ var DEFAULT_MAX_RETRIES = 3;
252
+ function buildQueueHandler(config) {
253
+ const consumersByQueue = /* @__PURE__ */ new Map();
254
+ for (const m of config.modules) {
255
+ if (!m.consumer) continue;
256
+ if (consumersByQueue.has(m.consumer.queue)) {
257
+ const owner = consumersByQueue.get(m.consumer.queue).module.name;
258
+ throw new Error(
259
+ `[katajs] Duplicate queue consumer for binding '${m.consumer.queue}': modules '${owner}' and '${m.name}' both consume it.`
260
+ );
261
+ }
262
+ consumersByQueue.set(m.consumer.queue, { module: m, consumer: m.consumer });
263
+ }
264
+ if (consumersByQueue.size === 0) return void 0;
265
+ return async function queue(batch, env) {
266
+ const entry = consumersByQueue.get(batch.queue);
267
+ if (!entry) {
268
+ const known = [...consumersByQueue.keys()].join(", ") || "(none)";
269
+ throw new Error(
270
+ `[katajs] No consumer registered for queue '${batch.queue}'. Known consumer bindings: ${known}.`
271
+ );
272
+ }
273
+ const { consumer } = entry;
274
+ const db = config.db.create(env);
275
+ const sharedContainerArgs = {
276
+ env,
277
+ registry: config.registry,
278
+ db,
279
+ runTransaction: config.db.runTransaction,
280
+ inTransaction: false
281
+ };
282
+ if ("handleBatch" in consumer && consumer.handleBatch) {
283
+ const requestId = config.generateRequestId?.() ?? cryptoRandomUUID();
284
+ const validated = [];
285
+ for (const msg of batch.messages) {
286
+ try {
287
+ const body = consumer.schema.parse(msg.body);
288
+ validated.push(makeValidatedMessage(msg, body));
289
+ } catch (err) {
290
+ await onMessageFailure(msg, consumer, env, err, config.errorMapper, batch.queue);
291
+ }
292
+ }
293
+ if (validated.length === 0) return;
294
+ const container = buildContainer({
295
+ ...sharedContainerArgs,
296
+ requestId
297
+ });
298
+ const validatedBatch = {
299
+ queue: batch.queue,
300
+ messages: validated,
301
+ ackAll: () => batch.ackAll(),
302
+ retryAll: (opts) => batch.retryAll(opts)
303
+ };
304
+ try {
305
+ await consumer.handleBatch(validatedBatch, container);
306
+ } catch (err) {
307
+ config.errorMapper?.onUnhandled?.(err, { queue: batch.queue });
308
+ batch.retryAll();
309
+ }
310
+ return;
311
+ }
312
+ if (!("handle" in consumer) || !consumer.handle) {
313
+ throw new Error(
314
+ `[katajs] Consumer for '${batch.queue}' (module '${entry.module.name}') has neither 'handle' nor 'handleBatch'.`
315
+ );
316
+ }
317
+ const handle = consumer.handle;
318
+ for (const msg of batch.messages) {
319
+ let body;
320
+ try {
321
+ body = consumer.schema.parse(msg.body);
322
+ } catch (err) {
323
+ const wrapped = err instanceof ValidationError ? err : err;
324
+ await onMessageFailure(msg, consumer, env, wrapped, config.errorMapper, batch.queue);
325
+ continue;
326
+ }
327
+ const container = buildContainer({
328
+ ...sharedContainerArgs,
329
+ requestId: msg.id
330
+ });
331
+ try {
332
+ await handle(makeValidatedMessage(msg, body), container);
333
+ msg.ack();
334
+ } catch (err) {
335
+ await onMessageFailure(msg, consumer, env, err, config.errorMapper, batch.queue);
336
+ }
337
+ }
338
+ };
339
+ }
340
+ function makeValidatedMessage(raw, body) {
341
+ return {
342
+ id: raw.id,
343
+ timestamp: raw.timestamp,
344
+ body,
345
+ attempts: raw.attempts,
346
+ ack: () => raw.ack(),
347
+ retry: (opts) => raw.retry(opts)
348
+ };
349
+ }
350
+ async function onMessageFailure(msg, consumer, env, err, errorMapper2, queue) {
351
+ errorMapper2?.onUnhandled?.(err, {
352
+ queue,
353
+ messageId: msg.id,
354
+ attempts: msg.attempts
355
+ });
356
+ const maxRetries = consumer.maxRetries ?? DEFAULT_MAX_RETRIES;
357
+ if (msg.attempts >= maxRetries) {
358
+ if (consumer.dlq) {
359
+ const dlqBinding = env?.[consumer.dlq];
360
+ if (dlqBinding && typeof dlqBinding.send === "function") {
361
+ try {
362
+ await dlqBinding.send({
363
+ originalQueue: queue,
364
+ messageId: msg.id,
365
+ body: msg.body,
366
+ error: err instanceof Error ? err.message : String(err),
367
+ attempts: msg.attempts,
368
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
369
+ });
370
+ msg.ack();
371
+ return;
372
+ } catch (dlqErr) {
373
+ errorMapper2?.onUnhandled?.(dlqErr, {
374
+ queue: consumer.dlq,
375
+ messageId: msg.id
376
+ });
377
+ }
378
+ } else {
379
+ errorMapper2?.onUnhandled?.(
380
+ new Error(
381
+ `[katajs] DLQ binding '${consumer.dlq}' not found on env. Acking message ${msg.id} to avoid loops.`
382
+ ),
383
+ { queue, messageId: msg.id }
384
+ );
385
+ msg.ack();
386
+ return;
387
+ }
388
+ } else {
389
+ msg.ack();
390
+ return;
391
+ }
392
+ }
393
+ msg.retry();
394
+ }
395
+ function cryptoRandomUUID() {
396
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
397
+ return crypto.randomUUID();
398
+ }
399
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
400
+ }
401
+
402
+ // src/app.ts
403
+ function createApp(config) {
404
+ validateModules(config.modules);
405
+ const registry = buildRegistry(config.modules.map((m) => m.provides));
406
+ const base = new Hono();
407
+ base.use(
408
+ "*",
409
+ containerMiddleware({
410
+ registry,
411
+ db: config.db,
412
+ generateRequestId: config.generateRequestId,
413
+ queues: config.queues
414
+ })
415
+ );
416
+ for (const mw of config.middleware ?? []) {
417
+ base.use("*", mw);
418
+ }
419
+ base.onError(errorMapper(config.errorMapper));
420
+ const app = config.routes ? config.routes(base) : base;
421
+ const queue = buildQueueHandler({
422
+ modules: config.modules,
423
+ registry,
424
+ db: config.db,
425
+ errorMapper: config.queueErrorMapper,
426
+ generateRequestId: config.generateRequestId
427
+ });
428
+ return { app, modules: config.modules, queue };
429
+ }
430
+ function validateModules(modules) {
431
+ const provideOwners = /* @__PURE__ */ new Map();
432
+ for (const m of modules) {
433
+ for (const key of Object.keys(m.provides)) {
434
+ const existing = provideOwners.get(key);
435
+ if (existing) {
436
+ throw new Error(
437
+ `[katajs] Duplicate provides key '${key}'.
438
+ Provided by: '${existing}' and '${m.name}'.
439
+ Pick one module to own this key.`
440
+ );
441
+ }
442
+ provideOwners.set(key, m.name);
443
+ }
444
+ }
445
+ const allKeys = [...provideOwners.keys()];
446
+ const moduleNames = modules.map((m) => m.name);
447
+ for (const m of modules) {
448
+ for (const key of m.requires) {
449
+ if (!provideOwners.has(key)) {
450
+ throw new Error(
451
+ `[katajs] Module '${m.name}' requires '${key}', but no module provides it.
452
+ Modules registered: ${moduleNames.join(", ") || "(none)"}.
453
+ Provided keys: ${allKeys.join(", ") || "(none)"}.
454
+ Did you forget to add the providing module to createApp({ modules: [...] })?`
455
+ );
456
+ }
457
+ }
458
+ }
459
+ const moduleByName = new Map(modules.map((m) => [m.name, m]));
460
+ const edges = /* @__PURE__ */ new Map();
461
+ for (const m of modules) {
462
+ const deps = /* @__PURE__ */ new Set();
463
+ for (const key of m.requires) {
464
+ const ownerName = provideOwners.get(key);
465
+ if (ownerName && ownerName !== m.name) deps.add(ownerName);
466
+ }
467
+ edges.set(m.name, deps);
468
+ }
469
+ const cycle = findCycle(edges);
470
+ if (cycle) {
471
+ throw new Error(
472
+ `[katajs] Module dependency cycle detected.
473
+ Cycle: ${cycle.join(" -> ")}.
474
+ Modules cannot transitively require services from each other.`
475
+ );
476
+ }
477
+ void moduleByName;
478
+ }
479
+ function findCycle(edges) {
480
+ const VISITING = 1;
481
+ const VISITED = 2;
482
+ const state = /* @__PURE__ */ new Map();
483
+ const stack = [];
484
+ function visit(node) {
485
+ const s = state.get(node);
486
+ if (s === VISITED) return void 0;
487
+ if (s === VISITING) {
488
+ const idx = stack.indexOf(node);
489
+ return [...stack.slice(idx), node];
490
+ }
491
+ state.set(node, VISITING);
492
+ stack.push(node);
493
+ for (const next of edges.get(node) ?? []) {
494
+ const found = visit(next);
495
+ if (found) return found;
496
+ }
497
+ stack.pop();
498
+ state.set(node, VISITED);
499
+ return void 0;
500
+ }
501
+ for (const node of edges.keys()) {
502
+ const found = visit(node);
503
+ if (found) return found;
504
+ }
505
+ return void 0;
506
+ }
507
+
508
+ // src/validate.ts
509
+ import { zValidator } from "@hono/zod-validator";
510
+ var targetByLabel = {
511
+ body: "json",
512
+ query: "query",
513
+ param: "param"
514
+ };
515
+ function makeHook(label) {
516
+ return (result, _c) => {
517
+ if (!result.success && result.error) {
518
+ const issues = result.error.issues.map((i) => ({
519
+ path: [label, ...i.path],
520
+ message: i.message
521
+ }));
522
+ throw new ValidationError(issues);
523
+ }
524
+ };
525
+ }
526
+ function validate(opts) {
527
+ const middlewares = [];
528
+ if (opts.body) {
529
+ middlewares.push(zValidator(targetByLabel.body, opts.body, makeHook("body")));
530
+ }
531
+ if (opts.query) {
532
+ middlewares.push(zValidator(targetByLabel.query, opts.query, makeHook("query")));
533
+ }
534
+ if (opts.param) {
535
+ middlewares.push(zValidator(targetByLabel.param, opts.param, makeHook("param")));
536
+ }
537
+ return middlewares;
538
+ }
539
+
540
+ // src/inspect.ts
541
+ function inspectModules(modules, options = {}) {
542
+ const provideOwners = /* @__PURE__ */ new Map();
543
+ for (const m of modules) {
544
+ for (const key of Object.keys(m.provides)) {
545
+ provideOwners.set(key, m.name);
546
+ }
547
+ }
548
+ const inspectedModules = modules.map((m) => {
549
+ const consumer = m.consumer ? { queue: m.consumer.queue, dlq: m.consumer.dlq } : void 0;
550
+ return {
551
+ name: m.name,
552
+ provides: Object.keys(m.provides).sort(),
553
+ requires: [...m.requires].sort(),
554
+ prefix: isRoutedModule(m) ? m.prefix : void 0,
555
+ hasRoutes: isRoutedModule(m),
556
+ ...consumer ? { consumer } : {}
557
+ };
558
+ });
559
+ const producers = options.producers ? Object.entries(options.producers).map(([name, p]) => ({ name, binding: p.binding })).sort((a, b) => a.name.localeCompare(b.name)) : [];
560
+ const edges = [];
561
+ for (const m of modules) {
562
+ for (const key of m.requires) {
563
+ const owner = provideOwners.get(key);
564
+ if (owner && owner !== m.name) {
565
+ edges.push({ from: m.name, to: owner, via: key });
566
+ }
567
+ }
568
+ }
569
+ const routes = [];
570
+ const seenRoutes = /* @__PURE__ */ new Set();
571
+ for (const m of modules) {
572
+ if (!isRoutedModule(m)) continue;
573
+ const honoRoutes = m.routes.routes;
574
+ if (!Array.isArray(honoRoutes)) continue;
575
+ for (const r of honoRoutes) {
576
+ if (typeof r.method !== "string" || typeof r.path !== "string") continue;
577
+ if (r.method === "ALL" && r.path === "*") continue;
578
+ const fullPath = mergePath(m.prefix, r.path);
579
+ const key = `${r.method} ${fullPath} ${m.name}`;
580
+ if (seenRoutes.has(key)) continue;
581
+ seenRoutes.add(key);
582
+ routes.push({ method: r.method, path: fullPath, module: m.name });
583
+ }
584
+ }
585
+ routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
586
+ edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
587
+ return {
588
+ modules: inspectedModules,
589
+ edges,
590
+ routes,
591
+ producers,
592
+ mermaid: () => buildMermaid(inspectedModules, edges),
593
+ json: () => JSON.stringify(
594
+ { modules: inspectedModules, edges, routes, producers },
595
+ null,
596
+ 2
597
+ ),
598
+ html: (opts) => buildHtml(inspectedModules, edges, routes, opts)
599
+ };
600
+ }
601
+ function isRoutedModule(m) {
602
+ return "routes" in m && "prefix" in m;
603
+ }
604
+ function mergePath(base, sub) {
605
+ if (!base || base === "/") return sub === "" ? "/" : sub;
606
+ if (!sub || sub === "/") return base;
607
+ const a = base.endsWith("/") ? base.slice(0, -1) : base;
608
+ const b = sub.startsWith("/") ? sub : `/${sub}`;
609
+ return `${a}${b}`;
610
+ }
611
+ function buildMermaid(modules, edges) {
612
+ const lines = ["graph TD"];
613
+ for (const m of modules) {
614
+ const meta = [];
615
+ if (m.prefix) meta.push(m.prefix);
616
+ if (m.provides.length > 0) meta.push(`+${m.provides.length} services`);
617
+ const sub = meta.length > 0 ? `<br/><small>${meta.join(" \u2022 ")}</small>` : "";
618
+ lines.push(` ${m.name}["<b>${m.name}</b>${sub}"]`);
619
+ }
620
+ for (const e of edges) {
621
+ lines.push(` ${e.from} -->|${e.via}| ${e.to}`);
622
+ }
623
+ return lines.join("\n");
624
+ }
625
+ function buildHtml(modules, edges, routes, opts = {}) {
626
+ const title = opts.title ?? "katajs \u2014 module graph";
627
+ const mermaidSrc = buildMermaid(modules, edges);
628
+ return `<!doctype html>
629
+ <html lang="en">
630
+ <head>
631
+ <meta charset="utf-8">
632
+ <meta name="viewport" content="width=device-width, initial-scale=1">
633
+ <title>${escapeHtml(title)}</title>
634
+ <style>
635
+ :root {
636
+ --bg: #0f1115;
637
+ --panel: #161922;
638
+ --panel-2: #1d2230;
639
+ --text: #e8ebf2;
640
+ --muted: #8b93a7;
641
+ --accent: #7aa2ff;
642
+ --accent-2: #57c7b9;
643
+ --border: #2a3041;
644
+ --get: #57c7b9;
645
+ --post: #7aa2ff;
646
+ --put: #f5a623;
647
+ --patch: #f5a623;
648
+ --delete: #ff6b6b;
649
+ }
650
+ * { box-sizing: border-box; }
651
+ body {
652
+ font: 14px/1.55 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
653
+ margin: 0;
654
+ background: var(--bg);
655
+ color: var(--text);
656
+ }
657
+ header {
658
+ padding: 1.75rem 2rem 1.25rem;
659
+ border-bottom: 1px solid var(--border);
660
+ }
661
+ header h1 {
662
+ margin: 0;
663
+ font-size: 1.4rem;
664
+ font-weight: 600;
665
+ }
666
+ header .meta {
667
+ color: var(--muted);
668
+ margin-top: 0.25rem;
669
+ font-size: 0.85rem;
670
+ }
671
+ main {
672
+ padding: 1.5rem 2rem 4rem;
673
+ max-width: 1280px;
674
+ margin: 0 auto;
675
+ }
676
+ section + section { margin-top: 2.5rem; }
677
+ section h2 {
678
+ font-size: 0.85rem;
679
+ text-transform: uppercase;
680
+ letter-spacing: 0.08em;
681
+ color: var(--muted);
682
+ font-weight: 600;
683
+ margin: 0 0 0.75rem;
684
+ }
685
+ .graph {
686
+ background: var(--panel);
687
+ border: 1px solid var(--border);
688
+ border-radius: 12px;
689
+ padding: 1.5rem;
690
+ overflow: auto;
691
+ }
692
+ .graph .mermaid {
693
+ text-align: center;
694
+ color: var(--text);
695
+ }
696
+ table {
697
+ width: 100%;
698
+ border-collapse: collapse;
699
+ background: var(--panel);
700
+ border: 1px solid var(--border);
701
+ border-radius: 12px;
702
+ overflow: hidden;
703
+ }
704
+ th, td {
705
+ text-align: left;
706
+ padding: 0.7rem 1rem;
707
+ border-bottom: 1px solid var(--border);
708
+ }
709
+ tr:last-child td { border-bottom: none; }
710
+ th {
711
+ background: var(--panel-2);
712
+ color: var(--muted);
713
+ font-size: 0.75rem;
714
+ text-transform: uppercase;
715
+ letter-spacing: 0.06em;
716
+ font-weight: 600;
717
+ }
718
+ td.method { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; }
719
+ td.method[data-m="GET"] { color: var(--get); }
720
+ td.method[data-m="POST"] { color: var(--post); }
721
+ td.method[data-m="PUT"] { color: var(--put); }
722
+ td.method[data-m="PATCH"] { color: var(--patch); }
723
+ td.method[data-m="DELETE"] { color: var(--delete); }
724
+ code {
725
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
726
+ font-size: 0.9em;
727
+ background: var(--panel-2);
728
+ padding: 0.1rem 0.4rem;
729
+ border-radius: 4px;
730
+ }
731
+ .pill {
732
+ display: inline-block;
733
+ padding: 0.1rem 0.5rem;
734
+ background: var(--panel-2);
735
+ border: 1px solid var(--border);
736
+ border-radius: 999px;
737
+ font-size: 0.8rem;
738
+ margin-right: 0.25rem;
739
+ margin-bottom: 0.25rem;
740
+ }
741
+ .pill.requires { color: var(--accent); border-color: rgba(122, 162, 255, 0.3); }
742
+ .pill.provides { color: var(--accent-2); border-color: rgba(87, 199, 185, 0.3); }
743
+ .empty { color: var(--muted); font-style: italic; }
744
+ footer {
745
+ padding: 2rem;
746
+ text-align: center;
747
+ color: var(--muted);
748
+ font-size: 0.8rem;
749
+ border-top: 1px solid var(--border);
750
+ }
751
+ </style>
752
+ </head>
753
+ <body>
754
+ <header>
755
+ <h1>${escapeHtml(title)}</h1>
756
+ <div class="meta">${modules.length} modules \u2022 ${edges.length} dependencies \u2022 ${routes.length} routes</div>
757
+ </header>
758
+ <main>
759
+ <section>
760
+ <h2>Module graph</h2>
761
+ <div class="graph">
762
+ <pre class="mermaid">${escapeHtml(mermaidSrc)}</pre>
763
+ </div>
764
+ </section>
765
+
766
+ <section>
767
+ <h2>Modules</h2>
768
+ <table>
769
+ <thead><tr><th>Name</th><th>Prefix</th><th>Provides</th><th>Requires</th></tr></thead>
770
+ <tbody>
771
+ ${modules.map(
772
+ (m) => ` <tr>
773
+ <td><strong>${escapeHtml(m.name)}</strong></td>
774
+ <td>${m.prefix ? `<code>${escapeHtml(m.prefix)}</code>` : '<span class="empty">\u2014</span>'}</td>
775
+ <td>${m.provides.length === 0 ? '<span class="empty">\u2014</span>' : m.provides.map((p) => `<span class="pill provides">${escapeHtml(p)}</span>`).join("")}</td>
776
+ <td>${m.requires.length === 0 ? '<span class="empty">\u2014</span>' : m.requires.map((p) => `<span class="pill requires">${escapeHtml(p)}</span>`).join("")}</td>
777
+ </tr>`
778
+ ).join("\n")}
779
+ </tbody>
780
+ </table>
781
+ </section>
782
+
783
+ <section>
784
+ <h2>Routes</h2>
785
+ ${routes.length === 0 ? '<p class="empty">No routes mounted.</p>' : `<table>
786
+ <thead><tr><th>Method</th><th>Path</th><th>Module</th></tr></thead>
787
+ <tbody>
788
+ ${routes.map(
789
+ (r) => ` <tr>
790
+ <td class="method" data-m="${escapeHtml(r.method)}">${escapeHtml(r.method)}</td>
791
+ <td><code>${escapeHtml(r.path)}</code></td>
792
+ <td>${escapeHtml(r.module)}</td>
793
+ </tr>`
794
+ ).join("\n")}
795
+ </tbody>
796
+ </table>`}
797
+ </section>
798
+ </main>
799
+ <footer>
800
+ Generated by <code>inspectModules()</code> from <code>@katajs/core</code>
801
+ </footer>
802
+ <script type="module">
803
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
804
+ mermaid.initialize({
805
+ startOnLoad: true,
806
+ securityLevel: 'loose',
807
+ theme: 'dark',
808
+ themeVariables: {
809
+ darkMode: true,
810
+ background: '#161922',
811
+ primaryColor: '#1d2230',
812
+ primaryTextColor: '#e8ebf2',
813
+ primaryBorderColor: '#2a3041',
814
+ lineColor: '#7aa2ff',
815
+ secondaryColor: '#57c7b9',
816
+ tertiaryColor: '#1d2230',
817
+ },
818
+ });
819
+ </script>
820
+ </body>
821
+ </html>
822
+ `;
823
+ }
824
+ function escapeHtml(s) {
825
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
826
+ }
827
+ export {
828
+ AppError,
829
+ ValidationError,
830
+ createApp,
831
+ defineConsumer,
832
+ defineMiddleware,
833
+ defineModule,
834
+ errorMapper,
835
+ inspectModules,
836
+ validate
837
+ };
838
+ //# sourceMappingURL=index.js.map