@c9up/ream 0.2.7 → 0.2.8

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.
Files changed (40) hide show
  1. package/dist/HttpKernel.d.ts.map +1 -1
  2. package/dist/HttpKernel.js +10 -0
  3. package/dist/HttpKernel.js.map +1 -1
  4. package/dist/debug.d.ts +22 -0
  5. package/dist/debug.d.ts.map +1 -0
  6. package/dist/debug.js +23 -0
  7. package/dist/debug.js.map +1 -0
  8. package/dist/dumper/Dumper.d.ts +80 -0
  9. package/dist/dumper/Dumper.d.ts.map +1 -0
  10. package/dist/dumper/Dumper.js +129 -0
  11. package/dist/dumper/Dumper.js.map +1 -0
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/middleware/Pipeline.d.ts.map +1 -1
  15. package/dist/middleware/Pipeline.js +7 -0
  16. package/dist/middleware/Pipeline.js.map +1 -1
  17. package/dist/repl/Repl.d.ts +88 -0
  18. package/dist/repl/Repl.d.ts.map +1 -0
  19. package/dist/repl/Repl.js +123 -0
  20. package/dist/repl/Repl.js.map +1 -0
  21. package/dist/services/dumper.d.ts +25 -0
  22. package/dist/services/dumper.d.ts.map +1 -0
  23. package/dist/services/dumper.js +27 -0
  24. package/dist/services/dumper.js.map +1 -0
  25. package/dist/services/repl.d.ts +19 -0
  26. package/dist/services/repl.d.ts.map +1 -0
  27. package/dist/services/repl.js +19 -0
  28. package/dist/services/repl.js.map +1 -0
  29. package/events.win32-x64-msvc.node +0 -0
  30. package/index.win32-x64-msvc.node +0 -0
  31. package/package.json +17 -1
  32. package/scheduler.win32-x64-msvc.node +0 -0
  33. package/src/HttpKernel.ts +27 -0
  34. package/src/debug.ts +25 -0
  35. package/src/dumper/Dumper.ts +164 -0
  36. package/src/index.ts +1 -1
  37. package/src/middleware/Pipeline.ts +7 -0
  38. package/src/repl/Repl.ts +162 -0
  39. package/src/services/dumper.ts +30 -0
  40. package/src/services/repl.ts +21 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The application REPL — AdonisJS's `Repl`, in shape and in method names.
3
+ *
4
+ * import repl from '@c9up/ream/services/repl'
5
+ * repl.addMethod('loadModels', async () => { … }, {
6
+ * description: 'Load every entity onto the prompt',
7
+ * })
8
+ * repl.start({ app, router })
9
+ *
10
+ * Built on `node:repl`, which is the REPL — this adds what makes it an
11
+ * APPLICATION repl: a context seeded with the booted services, helpers a
12
+ * project registers itself, and `.ls` to list them, because a prompt full of
13
+ * undiscoverable helpers is a prompt nobody uses.
14
+ */
15
+ import { start as startNodeRepl } from 'node:repl';
16
+ export class Repl {
17
+ #methods = new Map();
18
+ #readyCallbacks = [];
19
+ #compiler;
20
+ #server;
21
+ #prompt;
22
+ #write;
23
+ constructor(options = {}) {
24
+ this.#prompt = options.prompt ?? '> ';
25
+ this.#write =
26
+ options.write ??
27
+ ((message) => {
28
+ process.stdout.write(message);
29
+ });
30
+ }
31
+ /**
32
+ * Print a message around the prompt.
33
+ *
34
+ * Newline-wrapped: written mid-session the text would otherwise land on the
35
+ * line the user is typing.
36
+ */
37
+ notify(message) {
38
+ this.#write(`\n${message}\n`);
39
+ }
40
+ /** Run `callback` once the prompt is up. Chainable. */
41
+ ready(callback) {
42
+ this.#readyCallbacks.push(callback);
43
+ return this;
44
+ }
45
+ /**
46
+ * Register a helper, available at the prompt by name. Chainable.
47
+ *
48
+ * `width` is the name's display width, recorded here so `.ls` can align a
49
+ * column without measuring the set twice.
50
+ */
51
+ addMethod(name, handler, options = {}) {
52
+ this.#methods.set(name, { handler, options: { ...options, width: name.length } });
53
+ return this;
54
+ }
55
+ /** Every registered helper, keyed by name — AdonisJS `getMethods`. */
56
+ getMethods() {
57
+ return Object.fromEntries(this.#methods);
58
+ }
59
+ /** Use `compiler` for the code typed at the prompt. Chainable. */
60
+ useCompiler(compiler) {
61
+ this.#compiler = compiler;
62
+ return this;
63
+ }
64
+ /** The compiler in use, if any. */
65
+ get compiler() {
66
+ return this.#compiler;
67
+ }
68
+ /** The underlying `node:repl` server, once started. */
69
+ get server() {
70
+ return this.#server;
71
+ }
72
+ /**
73
+ * List the registered helpers, one per line, names aligned.
74
+ *
75
+ * Exposed rather than private because it IS the `.ls` command, and a caller
76
+ * driving the repl programmatically (a test, a custom command) needs the
77
+ * same listing without a live prompt.
78
+ */
79
+ describeMethods() {
80
+ if (this.#methods.size === 0)
81
+ return 'No methods registered';
82
+ const widest = Math.max(...[...this.#methods.values()].map((m) => m.options.width));
83
+ return [...this.#methods]
84
+ .map(([name, method]) => {
85
+ const description = method.options.description ?? '';
86
+ const usage = method.options.usage ? ` (${method.options.usage})` : '';
87
+ return `${name.padEnd(widest)} ${description}${usage}`.trimEnd();
88
+ })
89
+ .join('\n');
90
+ }
91
+ /**
92
+ * Start the prompt, seeding it with `context`.
93
+ *
94
+ * Registered helpers are bound to this repl before the prompt appears — a
95
+ * helper that only exists after the first keystroke is a helper the user
96
+ * finds missing.
97
+ */
98
+ start(context = {}) {
99
+ const server = startNodeRepl({ prompt: this.#prompt, useGlobal: false });
100
+ this.#server = server;
101
+ for (const [key, value] of Object.entries(context)) {
102
+ Object.defineProperty(server.context, key, { value, configurable: true, enumerable: true });
103
+ }
104
+ for (const [name, method] of this.#methods) {
105
+ Object.defineProperty(server.context, name, {
106
+ value: (...args) => method.handler(this, ...args),
107
+ configurable: true,
108
+ enumerable: true,
109
+ });
110
+ }
111
+ server.defineCommand('ls', {
112
+ help: 'List the helpers available at this prompt',
113
+ action: () => {
114
+ this.notify(this.describeMethods());
115
+ server.displayPrompt();
116
+ },
117
+ });
118
+ for (const callback of this.#readyCallbacks)
119
+ callback(this);
120
+ return this;
121
+ }
122
+ }
123
+ //# sourceMappingURL=Repl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Repl.js","sourceRoot":"","sources":["../../src/repl/Repl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Dumper singleton service.
3
+ *
4
+ * import { dd } from '@c9up/ream/services/dumper'
5
+ * dd(user)
6
+ *
7
+ * import dumper from '@c9up/ream/services/dumper'
8
+ * dumper.configureAnsiOutput({ depth: 8 })
9
+ *
10
+ * Unlike the other accessors this one holds a real instance rather than a
11
+ * proxy: the dumper depends on nothing the boot phase builds, and something
12
+ * you reach for mid-debug should not require a booted application.
13
+ */
14
+ import { Dumper } from '../dumper/Dumper.js';
15
+ declare const dumper: Dumper;
16
+ /**
17
+ * Dump and die — AdonisJS's `dd`.
18
+ *
19
+ * The trace index is 2, not the default 1: the frame above is this function,
20
+ * so the caller of `dd` is one further out. Without it, every dump would
21
+ * report this file as its source.
22
+ */
23
+ export declare const dd: (value: unknown) => void;
24
+ export default dumper;
25
+ //# sourceMappingURL=dumper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dumper.d.ts","sourceRoot":"","sources":["../../src/services/dumper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAE5C,QAAA,MAAM,MAAM,QAAe,CAAA;AAE3B;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,GAAI,OAAO,OAAO,KAAG,IAEnC,CAAA;AAED,eAAe,MAAM,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Dumper singleton service.
3
+ *
4
+ * import { dd } from '@c9up/ream/services/dumper'
5
+ * dd(user)
6
+ *
7
+ * import dumper from '@c9up/ream/services/dumper'
8
+ * dumper.configureAnsiOutput({ depth: 8 })
9
+ *
10
+ * Unlike the other accessors this one holds a real instance rather than a
11
+ * proxy: the dumper depends on nothing the boot phase builds, and something
12
+ * you reach for mid-debug should not require a booted application.
13
+ */
14
+ import { Dumper } from '../dumper/Dumper.js';
15
+ const dumper = new Dumper();
16
+ /**
17
+ * Dump and die — AdonisJS's `dd`.
18
+ *
19
+ * The trace index is 2, not the default 1: the frame above is this function,
20
+ * so the caller of `dd` is one further out. Without it, every dump would
21
+ * report this file as its source.
22
+ */
23
+ export const dd = (value) => {
24
+ dumper.dd(value, 2);
25
+ };
26
+ export default dumper;
27
+ //# sourceMappingURL=dumper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dumper.js","sourceRoot":"","sources":["../../src/services/dumper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAE5C,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA;AAE3B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAc,EAAQ,EAAE;IACzC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;AACrB,CAAC,CAAA;AAED,eAAe,MAAM,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * REPL singleton service.
3
+ *
4
+ * // start/repl.ts, imported as a preload for the `repl` environment
5
+ * import repl from '@c9up/ream/services/repl'
6
+ * import { UserEntity } from '#app/entities/user'
7
+ *
8
+ * repl.addMethod('users', () => UserEntity, {
9
+ * description: 'The user entity, for querying at the prompt',
10
+ * })
11
+ *
12
+ * Holds a real instance rather than a proxy: helpers are registered BEFORE the
13
+ * prompt starts — that is the point of registering them — so the object has to
14
+ * exist before anything boots it.
15
+ */
16
+ import { Repl } from '../repl/Repl.js';
17
+ declare const repl: Repl;
18
+ export default repl;
19
+ //# sourceMappingURL=repl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../src/services/repl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAEtC,QAAA,MAAM,IAAI,MAAa,CAAA;AAEvB,eAAe,IAAI,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * REPL singleton service.
3
+ *
4
+ * // start/repl.ts, imported as a preload for the `repl` environment
5
+ * import repl from '@c9up/ream/services/repl'
6
+ * import { UserEntity } from '#app/entities/user'
7
+ *
8
+ * repl.addMethod('users', () => UserEntity, {
9
+ * description: 'The user entity, for querying at the prompt',
10
+ * })
11
+ *
12
+ * Holds a real instance rather than a proxy: helpers are registered BEFORE the
13
+ * prompt starts — that is the point of registering them — so the object has to
14
+ * exist before anything boots it.
15
+ */
16
+ import { Repl } from '../repl/Repl.js';
17
+ const repl = new Repl();
18
+ export default repl;
19
+ //# sourceMappingURL=repl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repl.js","sourceRoot":"","sources":["../../src/services/repl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAEtC,MAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAA;AAEvB,eAAe,IAAI,CAAA"}
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/ream",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Ream — Rust-powered Node.js application framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,6 +39,22 @@
39
39
  "types": "./dist/services/config.d.ts",
40
40
  "import": "./dist/services/config.js"
41
41
  },
42
+ "./services/dumper": {
43
+ "types": "./dist/services/dumper.d.ts",
44
+ "import": "./dist/services/dumper.js"
45
+ },
46
+ "./dumper": {
47
+ "types": "./dist/dumper/Dumper.d.ts",
48
+ "import": "./dist/dumper/Dumper.js"
49
+ },
50
+ "./services/repl": {
51
+ "types": "./dist/services/repl.d.ts",
52
+ "import": "./dist/services/repl.js"
53
+ },
54
+ "./repl": {
55
+ "types": "./dist/repl/Repl.d.ts",
56
+ "import": "./dist/repl/Repl.js"
57
+ },
42
58
  "./services/encryption": {
43
59
  "types": "./dist/services/encryption.d.ts",
44
60
  "import": "./dist/services/encryption.js"
Binary file
package/src/HttpKernel.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { Container } from './container/Container.js'
14
+ import { debugHttp } from './debug.js'
14
15
  import type { Emitter } from './events/Emitter.js'
15
16
  import { E_ROUTE_NOT_FOUND, ExceptionHandler } from './http/Exception.js'
16
17
  import { type ChildLoggerSource, HttpContext } from './http/HttpContext.js'
@@ -157,6 +158,14 @@ export function createHttpKernel(
157
158
  // only correct key; the previous PascalCase fallback was dead code.
158
159
  const host = reqData.headers.host
159
160
  const match = config.router.match(reqData.method, reqData.path, host)
161
+ if (debugHttp.enabled) {
162
+ debugHttp(
163
+ '%s %s -> %s',
164
+ reqData.method,
165
+ reqData.path,
166
+ match ? (match.route.name ?? match.route.path) : 'no match',
167
+ )
168
+ }
160
169
  const routeInfo = match
161
170
  ? {
162
171
  pattern: match.route.path,
@@ -327,6 +336,15 @@ export function createHttpKernel(
327
336
  path: reqData.path,
328
337
  status: ctx.response.getStatus(),
329
338
  })
339
+ if (debugHttp.enabled) {
340
+ debugHttp(
341
+ '%s %s <- %d (%s)',
342
+ reqData.method,
343
+ reqData.path,
344
+ ctx.response.getStatus(),
345
+ correlationId,
346
+ )
347
+ }
330
348
  const serialized = await serializeResponse(ctx)
331
349
  // The body is built; anything registered with `response.onFinish()` runs
332
350
  // now — after the answer is ready, before we hand it back.
@@ -374,6 +392,15 @@ export function createHttpKernel(
374
392
  path: reqData.path,
375
393
  status: ctx.response.getStatus(),
376
394
  })
395
+ if (debugHttp.enabled) {
396
+ debugHttp(
397
+ '%s %s <- %d (%s)',
398
+ reqData.method,
399
+ reqData.path,
400
+ ctx.response.getStatus(),
401
+ correlationId,
402
+ )
403
+ }
377
404
  const serialized = await serializeResponse(ctx)
378
405
  // The body is built; anything registered with `response.onFinish()` runs
379
406
  // now — after the answer is ready, before we hand it back.
package/src/debug.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Debug channels, off unless `NODE_DEBUG` names them — the shape AdonisJS
3
+ * uses (`NODE_DEBUG=adonisjs:http`).
4
+ *
5
+ * NODE_DEBUG=ream:http ream serve
6
+ * NODE_DEBUG=ream:* ream serve
7
+ *
8
+ * These are for the framework's own tracing: which route a request matched,
9
+ * which middleware ran, what status came back. They are NOT request logging —
10
+ * a production request log belongs to the app's logger (`ctx.logger`), which
11
+ * is correlation-scoped and structured. Neither framework prints one by
12
+ * default, and this is the switch for when you need to see the plumbing.
13
+ *
14
+ * `debuglog` returns a no-op when the channel is off, so an inactive probe
15
+ * costs a call. Guard with `.enabled` anywhere the ARGUMENTS would cost
16
+ * something to build — the per-request probes below all do.
17
+ */
18
+
19
+ import { debuglog } from 'node:util'
20
+
21
+ /** Routing, middleware and response tracing. AdonisJS `adonisjs:http`. */
22
+ export const debugHttp = debuglog('ream:http')
23
+
24
+ /** Boot: providers, preloads, container bindings. AdonisJS `adonisjs:core`. */
25
+ export const debugCore = debuglog('ream:core')
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Value dumping — AdonisJS's `Dumper`, in shape and in method names.
3
+ *
4
+ * import { dd } from '@c9up/ream/services/dumper'
5
+ * dd(user) // prints and stops the process
6
+ *
7
+ * import dumper from '@c9up/ream/services/dumper'
8
+ * response.send(dumper.dumpToHtml(user, { cspNonce: response.nonce }))
9
+ *
10
+ * The rendering is `node:util`'s `inspect`, not a dependency. It already knows
11
+ * about depth limits, circular references, getters, Maps, Sets and typed
12
+ * arrays, and it colours for a terminal — which is the whole of the ANSI side.
13
+ * The HTML side escapes that same output rather than building a second
14
+ * formatter, so the two can never describe a value differently.
15
+ */
16
+
17
+ import { inspect } from 'node:util'
18
+ import { escapeHTML } from '../helpers/string.js'
19
+
20
+ /** Where a dump was written, so the output can say so. */
21
+ export interface DumpSource {
22
+ location: string
23
+ line: number
24
+ }
25
+
26
+ /** Knobs for the terminal rendering — AdonisJS `ConsoleDumpConfig`. */
27
+ export interface ConsoleDumpConfig {
28
+ /** How deep to walk before printing `[Object]`. Default 4. */
29
+ depth?: number
30
+ /** Colour the output. Default: whether stdout is a TTY. */
31
+ colors?: boolean
32
+ /** Show non-enumerable properties. Default false. */
33
+ showHidden?: boolean
34
+ }
35
+
36
+ /** Knobs for the HTML rendering — AdonisJS `HTMLDumpConfig`. */
37
+ export interface HTMLDumpConfig {
38
+ /** How deep to walk. Default 4. */
39
+ depth?: number
40
+ /** Show non-enumerable properties. Default false. */
41
+ showHidden?: boolean
42
+ /** Style overrides for the emitted `<pre>`, as CSS declarations. */
43
+ styles?: Record<string, string>
44
+ }
45
+
46
+ const DEFAULT_STYLES: Record<string, string> = {
47
+ 'background-color': '#0b0b0d',
48
+ color: '#e6e6e6',
49
+ padding: '1rem',
50
+ 'border-radius': '6px',
51
+ 'font-family': 'ui-monospace, SFMono-Regular, Menlo, monospace',
52
+ 'font-size': '13px',
53
+ 'line-height': '1.5',
54
+ 'overflow-x': 'auto',
55
+ }
56
+
57
+ export class Dumper {
58
+ #html: HTMLDumpConfig = {}
59
+ #ansi: ConsoleDumpConfig = {}
60
+
61
+ /** Configure the HTML rendering. Chainable, as in AdonisJS. */
62
+ configureHtmlOutput(config: HTMLDumpConfig): this {
63
+ this.#html = { ...this.#html, ...config }
64
+ return this
65
+ }
66
+
67
+ /** Configure the terminal rendering. Chainable, as in AdonisJS. */
68
+ configureAnsiOutput(config: ConsoleDumpConfig): this {
69
+ this.#ansi = { ...this.#ansi, ...config }
70
+ return this
71
+ }
72
+
73
+ /**
74
+ * The `<style>` a page needs before its first HTML dump.
75
+ *
76
+ * Takes a CSP nonce because a page that sets a nonce-based policy rejects an
77
+ * unattributed inline `<style>` — and a dump landing in a page that silently
78
+ * drops its styling is worse than no dump. `response.nonce` is what to pass.
79
+ */
80
+ getHeadElements(cspNonce?: string): string {
81
+ const nonce = cspNonce ? ` nonce="${escapeHTML(cspNonce)}"` : ''
82
+ const styles = { ...DEFAULT_STYLES, ...this.#html.styles }
83
+ const declarations = Object.entries(styles)
84
+ .map(([property, value]) => ` ${property}: ${value};`)
85
+ .join('\n')
86
+ return `<style${nonce}>\n.ream-dump {\n${declarations}\n}\n</style>`
87
+ }
88
+
89
+ /** Render `value` for a terminal. */
90
+ dumpToAnsi(value: unknown, options?: { title?: string; source?: DumpSource }): string {
91
+ const body = inspect(value, {
92
+ depth: this.#ansi.depth ?? 4,
93
+ colors: this.#ansi.colors ?? process.stdout.isTTY === true,
94
+ showHidden: this.#ansi.showHidden ?? false,
95
+ // Long strings and arrays are the reason someone is dumping; truncating
96
+ // them defeats the call.
97
+ maxStringLength: null,
98
+ maxArrayLength: null,
99
+ })
100
+ return [header(options), body].filter(Boolean).join('\n')
101
+ }
102
+
103
+ /**
104
+ * Render `value` as HTML.
105
+ *
106
+ * The inspected output is escaped before it reaches the page: a dumped value
107
+ * is by definition data someone else may control, and a dump is something
108
+ * you reach for while debugging — exactly when a script tag in a field name
109
+ * would be least expected.
110
+ */
111
+ dumpToHtml(
112
+ value: unknown,
113
+ options?: { cspNonce?: string; title?: string; source?: DumpSource },
114
+ ): string {
115
+ const body = inspect(value, {
116
+ depth: this.#html.depth ?? 4,
117
+ colors: false,
118
+ showHidden: this.#html.showHidden ?? false,
119
+ maxStringLength: null,
120
+ maxArrayLength: null,
121
+ })
122
+ const heading = header(options)
123
+ const label = heading ? `<div class="ream-dump-header">${escapeHTML(heading)}</div>` : ''
124
+ return `${this.getHeadElements(options?.cspNonce)}\n${label}<pre class="ream-dump">${escapeHTML(body)}</pre>`
125
+ }
126
+
127
+ /**
128
+ * Dump and die — print to stderr and stop the process.
129
+ *
130
+ * `traceSourceIndex` selects which stack frame to report as the dump site:
131
+ * 1 is the caller of `dd`, which is right when `dd` is called directly, and
132
+ * a wrapper passes 2.
133
+ */
134
+ dd(value: unknown, traceSourceIndex = 1): void {
135
+ process.stderr.write(`${this.dumpToAnsi(value, { source: callSite(traceSourceIndex) })}\n`)
136
+ process.exit(1)
137
+ }
138
+ }
139
+
140
+ /** `title` and `source` as one line, or nothing when neither is given. */
141
+ function header(options?: { title?: string; source?: DumpSource }): string {
142
+ const parts: string[] = []
143
+ if (options?.title) parts.push(options.title)
144
+ if (options?.source) parts.push(`${options.source.location}:${options.source.line}`)
145
+ return parts.join(' — ')
146
+ }
147
+
148
+ /**
149
+ * Where `dd` was called, read from a thrown stack.
150
+ *
151
+ * Best-effort: a bundled or minified frame may not parse, and a dump that
152
+ * cannot name its own line is still a useful dump — so a miss returns nothing
153
+ * rather than throwing on the debugging path.
154
+ */
155
+ function callSite(index: number): DumpSource | undefined {
156
+ const frames = new Error().stack?.split('\n').slice(1)
157
+ const frame = frames?.[index]
158
+ if (!frame) return undefined
159
+ const match = /\(?(.+):(\d+):\d+\)?$/.exec(frame.trim())
160
+ const location = match?.[1]
161
+ const line = match?.[2]
162
+ if (!location || !line) return undefined
163
+ return { location, line: Number(line) }
164
+ }
package/src/index.ts CHANGED
@@ -371,4 +371,4 @@ export {
371
371
  StatsTracker,
372
372
  } from './scheduler/index.js'
373
373
 
374
- export const VERSION = '0.2.7'
374
+ export const VERSION = '0.2.8'
@@ -11,6 +11,7 @@
11
11
  * 5. Handler
12
12
  */
13
13
 
14
+ import { debugHttp } from '../debug.js'
14
15
  import { ReamError } from '../errors/ReamError.js'
15
16
  import { E_FORBIDDEN, E_UNAUTHORIZED, E_VALIDATION_ERROR } from '../http/Exception.js'
16
17
  import type { HttpContext } from '../http/HttpContext.js'
@@ -196,6 +197,12 @@ function composeMiddleware(middleware: MiddlewareFunction[]): MiddlewareFunction
196
197
  const fn = i < middleware.length ? middleware[i] : finalNext
197
198
  if (!fn) return
198
199
 
200
+ if (debugHttp.enabled && i < middleware.length) {
201
+ // `fn.name` is what a named middleware function or a bound class
202
+ // method carries; an inline arrow has none, and its position is the
203
+ // only thing that identifies it.
204
+ debugHttp('middleware %d/%d %s', i + 1, middleware.length, fn.name || '(anonymous)')
205
+ }
199
206
  await fn(ctx, () => dispatch(i + 1))
200
207
  }
201
208