@c9up/ream 0.2.26 → 0.2.28

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 (170) hide show
  1. package/README.md +1 -0
  2. package/dist/HttpKernel.d.ts +5 -1
  3. package/dist/HttpKernel.d.ts.map +1 -1
  4. package/dist/HttpKernel.js +6 -6
  5. package/dist/HttpKernel.js.map +1 -1
  6. package/dist/Ignitor.d.ts +3 -1
  7. package/dist/Ignitor.d.ts.map +1 -1
  8. package/dist/Ignitor.js +94 -14
  9. package/dist/Ignitor.js.map +1 -1
  10. package/dist/console/prompts.d.ts.map +1 -1
  11. package/dist/console/prompts.js +34 -46
  12. package/dist/console/prompts.js.map +1 -1
  13. package/dist/console/selection.d.ts +54 -0
  14. package/dist/console/selection.d.ts.map +1 -0
  15. package/dist/console/selection.js +235 -0
  16. package/dist/console/selection.js.map +1 -0
  17. package/dist/dev/fullReload.d.ts +35 -10
  18. package/dist/dev/fullReload.d.ts.map +1 -1
  19. package/dist/dev/fullReload.js +55 -14
  20. package/dist/dev/fullReload.js.map +1 -1
  21. package/dist/dev/hotWatcher.d.ts +19 -3
  22. package/dist/dev/hotWatcher.d.ts.map +1 -1
  23. package/dist/dev/hotWatcher.js +168 -18
  24. package/dist/dev/hotWatcher.js.map +1 -1
  25. package/dist/dev/readyBanner.d.ts +45 -0
  26. package/dist/dev/readyBanner.d.ts.map +1 -0
  27. package/dist/dev/readyBanner.js +72 -0
  28. package/dist/dev/readyBanner.js.map +1 -0
  29. package/dist/dev/shortcuts.d.ts +41 -0
  30. package/dist/dev/shortcuts.d.ts.map +1 -0
  31. package/dist/dev/shortcuts.js +142 -0
  32. package/dist/dev/shortcuts.js.map +1 -0
  33. package/dist/errors/prettyPrintError.d.ts.map +1 -1
  34. package/dist/errors/prettyPrintError.js +5 -8
  35. package/dist/errors/prettyPrintError.js.map +1 -1
  36. package/dist/errors/renderError.d.ts +46 -0
  37. package/dist/errors/renderError.d.ts.map +1 -0
  38. package/dist/errors/renderError.js +197 -0
  39. package/dist/errors/renderError.js.map +1 -0
  40. package/dist/hot.js +12 -2
  41. package/dist/hot.js.map +1 -1
  42. package/dist/index.d.ts +4 -3
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +3 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/native/generated.d.ts +34 -0
  47. package/dist/native/generated.d.ts.map +1 -1
  48. package/dist/profiler/MemoryProfileStore.d.ts +23 -0
  49. package/dist/profiler/MemoryProfileStore.d.ts.map +1 -0
  50. package/dist/profiler/MemoryProfileStore.js +70 -0
  51. package/dist/profiler/MemoryProfileStore.js.map +1 -0
  52. package/dist/profiler/Profiler.d.ts +56 -0
  53. package/dist/profiler/Profiler.d.ts.map +1 -0
  54. package/dist/profiler/Profiler.js +162 -0
  55. package/dist/profiler/Profiler.js.map +1 -0
  56. package/dist/profiler/collectors.d.ts +105 -0
  57. package/dist/profiler/collectors.d.ts.map +1 -0
  58. package/dist/profiler/collectors.js +164 -0
  59. package/dist/profiler/collectors.js.map +1 -0
  60. package/dist/profiler/endpoint.d.ts +27 -0
  61. package/dist/profiler/endpoint.d.ts.map +1 -0
  62. package/dist/profiler/endpoint.js +79 -0
  63. package/dist/profiler/endpoint.js.map +1 -0
  64. package/dist/profiler/index.d.ts +26 -0
  65. package/dist/profiler/index.d.ts.map +1 -0
  66. package/dist/profiler/index.js +41 -0
  67. package/dist/profiler/index.js.map +1 -0
  68. package/dist/profiler/middleware.d.ts +17 -0
  69. package/dist/profiler/middleware.d.ts.map +1 -0
  70. package/dist/profiler/middleware.js +79 -0
  71. package/dist/profiler/middleware.js.map +1 -0
  72. package/dist/profiler/render.d.ts +23 -0
  73. package/dist/profiler/render.d.ts.map +1 -0
  74. package/dist/profiler/render.js +141 -0
  75. package/dist/profiler/render.js.map +1 -0
  76. package/dist/profiler/toolbar.d.ts +34 -0
  77. package/dist/profiler/toolbar.d.ts.map +1 -0
  78. package/dist/profiler/toolbar.js +104 -0
  79. package/dist/profiler/toolbar.js.map +1 -0
  80. package/dist/profiler/types.d.ts +70 -0
  81. package/dist/profiler/types.d.ts.map +1 -0
  82. package/dist/profiler/types.js +11 -0
  83. package/dist/profiler/types.js.map +1 -0
  84. package/dist/router/Router.d.ts +11 -0
  85. package/dist/router/Router.d.ts.map +1 -1
  86. package/dist/router/Router.js +16 -0
  87. package/dist/router/Router.js.map +1 -1
  88. package/dist/server/Server.d.ts +37 -0
  89. package/dist/server/Server.d.ts.map +1 -1
  90. package/dist/server/Server.js +30 -0
  91. package/dist/server/Server.js.map +1 -1
  92. package/dist/session/Session.d.ts +33 -0
  93. package/dist/session/Session.d.ts.map +1 -1
  94. package/dist/session/Session.js +31 -2
  95. package/dist/session/Session.js.map +1 -1
  96. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  97. package/dist/session/SessionMiddleware.js +24 -4
  98. package/dist/session/SessionMiddleware.js.map +1 -1
  99. package/dist/session/drivers/CookieDriver.d.ts +12 -3
  100. package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
  101. package/dist/session/drivers/CookieDriver.js +20 -6
  102. package/dist/session/drivers/CookieDriver.js.map +1 -1
  103. package/dist/session/drivers/DatabaseDriver.d.ts +2 -0
  104. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  105. package/dist/session/drivers/DatabaseDriver.js +6 -0
  106. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  107. package/dist/session/drivers/FileDriver.d.ts +8 -0
  108. package/dist/session/drivers/FileDriver.d.ts.map +1 -1
  109. package/dist/session/drivers/FileDriver.js +85 -11
  110. package/dist/session/drivers/FileDriver.js.map +1 -1
  111. package/dist/session/drivers/MemoryDriver.d.ts +2 -0
  112. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  113. package/dist/session/drivers/MemoryDriver.js +9 -0
  114. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  115. package/dist/session/drivers/RedisDriver.d.ts +2 -0
  116. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  117. package/dist/session/drivers/RedisDriver.js +6 -0
  118. package/dist/session/drivers/RedisDriver.js.map +1 -1
  119. package/dist/session/errors.d.ts +10 -0
  120. package/dist/session/errors.d.ts.map +1 -1
  121. package/dist/session/errors.js +12 -0
  122. package/dist/session/errors.js.map +1 -1
  123. package/events.darwin-arm64.node +0 -0
  124. package/events.darwin-x64.node +0 -0
  125. package/events.linux-arm64-gnu.node +0 -0
  126. package/events.linux-x64-gnu.node +0 -0
  127. package/events.win32-x64-msvc.node +0 -0
  128. package/index.darwin-arm64.node +0 -0
  129. package/index.darwin-x64.node +0 -0
  130. package/index.linux-arm64-gnu.node +0 -0
  131. package/index.linux-x64-gnu.node +0 -0
  132. package/index.win32-x64-msvc.node +0 -0
  133. package/package.json +1 -1
  134. package/scheduler.darwin-arm64.node +0 -0
  135. package/scheduler.darwin-x64.node +0 -0
  136. package/scheduler.linux-arm64-gnu.node +0 -0
  137. package/scheduler.linux-x64-gnu.node +0 -0
  138. package/scheduler.win32-x64-msvc.node +0 -0
  139. package/src/HttpKernel.ts +12 -8
  140. package/src/Ignitor.ts +109 -15
  141. package/src/console/prompts.ts +45 -64
  142. package/src/console/selection.ts +274 -0
  143. package/src/dev/fullReload.ts +57 -15
  144. package/src/dev/hotWatcher.ts +165 -21
  145. package/src/dev/readyBanner.ts +84 -0
  146. package/src/dev/shortcuts.ts +158 -0
  147. package/src/errors/prettyPrintError.ts +5 -5
  148. package/src/errors/renderError.ts +230 -0
  149. package/src/hot.ts +12 -2
  150. package/src/index.ts +11 -1
  151. package/src/native/generated.ts +38 -0
  152. package/src/profiler/MemoryProfileStore.ts +75 -0
  153. package/src/profiler/Profiler.ts +182 -0
  154. package/src/profiler/collectors.ts +230 -0
  155. package/src/profiler/endpoint.ts +98 -0
  156. package/src/profiler/index.ts +56 -0
  157. package/src/profiler/middleware.ts +83 -0
  158. package/src/profiler/render.ts +173 -0
  159. package/src/profiler/toolbar.ts +106 -0
  160. package/src/profiler/types.ts +75 -0
  161. package/src/router/Router.ts +16 -0
  162. package/src/server/Server.ts +49 -0
  163. package/src/session/Session.ts +56 -2
  164. package/src/session/SessionMiddleware.ts +23 -4
  165. package/src/session/drivers/CookieDriver.ts +21 -6
  166. package/src/session/drivers/DatabaseDriver.ts +10 -0
  167. package/src/session/drivers/FileDriver.ts +91 -11
  168. package/src/session/drivers/MemoryDriver.ts +9 -0
  169. package/src/session/drivers/RedisDriver.ts +7 -0
  170. package/src/session/errors.ts +17 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The keys the dev server answers to.
3
+ *
4
+ * Upstream's set, read off `@adonisjs/assembler`'s build: `r` restarts, `c`
5
+ * clears, `o` opens the browser, `h` shows the list, and Ctrl-C or Ctrl-D
6
+ * quits. Same keys, same order, same words.
7
+ *
8
+ * Owned by the application rather than by the `ream dev` parent, which is
9
+ * where upstream owns it — the same split as the ready sticker, and for the
10
+ * same reason: `o` needs the address, and a port found by scanning past a
11
+ * taken 3000 is known only to the process that bound it. Everything else
12
+ * follows from there, because this process already has what each key needs:
13
+ * exiting on the restart code is what makes the parent start it again, and
14
+ * the graceful shutdown is already wired to a signal.
15
+ *
16
+ * NO dependency for opening a browser. Upstream imports `open`; this spawns
17
+ * the platform's opener, which is the one thing that package does on the three
18
+ * platforms a dev server runs on.
19
+ */
20
+
21
+ import { spawn } from 'node:child_process'
22
+ import { lumen } from '@c9up/lumen'
23
+ import { clearScreen } from './fullReload.js'
24
+
25
+ /** The exit code the `ream dev` parent restarts on. */
26
+ export const RESTART_EXIT_CODE = 75
27
+
28
+ interface Shortcut {
29
+ key: string
30
+ description: string
31
+ handler: () => void
32
+ }
33
+
34
+ export interface ShortcutsOptions {
35
+ /** Where the server is listening, for `o`. */
36
+ url?: string
37
+ /** Leave the process the way a restart leaves it. Defaults to exiting. */
38
+ onRestart?: () => void
39
+ /** End the session. Defaults to exiting through the normal shutdown. */
40
+ onQuit?: () => void
41
+ }
42
+
43
+ /** The lines `h` prints, so a test can read them without a terminal. */
44
+ export function helpLines(): string[] {
45
+ return ['Available shortcuts:', ...shortcutsFor({}).map((s) => `· ${s.key}: ${s.description}`)]
46
+ }
47
+
48
+ function shortcutsFor(options: ShortcutsOptions): Shortcut[] {
49
+ const ui = lumen()
50
+ return [
51
+ {
52
+ key: 'r',
53
+ description: 'restart server',
54
+ handler: () => {
55
+ ui.logger.log('')
56
+ ui.logger.info('Manual restart triggered...')
57
+ // Exiting on the restart code IS the restart: the parent loops on it,
58
+ // which is the same path a change outside the boundaries takes.
59
+ if (options.onRestart) options.onRestart()
60
+ else process.exit(RESTART_EXIT_CODE)
61
+ },
62
+ },
63
+ {
64
+ key: 'c',
65
+ description: 'clear console',
66
+ handler: () => {
67
+ clearScreen()
68
+ ui.logger.info('Console cleared')
69
+ },
70
+ },
71
+ {
72
+ key: 'o',
73
+ description: 'open in browser',
74
+ handler: () => {
75
+ if (options.url === undefined) {
76
+ ui.logger.warning('No server address to open')
77
+ return
78
+ }
79
+ ui.logger.log('')
80
+ ui.logger.info(`Opening ${options.url}...`)
81
+ openBrowser(options.url)
82
+ },
83
+ },
84
+ {
85
+ key: 'h',
86
+ description: 'show this help',
87
+ handler: () => {
88
+ ui.logger.log('')
89
+ for (const line of helpLines()) ui.logger.log(line)
90
+ },
91
+ },
92
+ ]
93
+ }
94
+
95
+ /** What to do with one keypress. Exported so the mapping is testable. */
96
+ export function handleKey(key: string, options: ShortcutsOptions = {}): boolean {
97
+ // \u0003 is Ctrl-C and \u0004 is Ctrl-D. Raw mode means no signal is raised
98
+ // for either, so the process would sit there unkillable without this.
99
+ if (key === '\u0003' || key === '\u0004') {
100
+ if (options.onQuit) options.onQuit()
101
+ else process.kill(process.pid, 'SIGINT')
102
+ return true
103
+ }
104
+ const shortcut = shortcutsFor(options).find((entry) => entry.key === key)
105
+ if (shortcut === undefined) return false
106
+ shortcut.handler()
107
+ return true
108
+ }
109
+
110
+ /**
111
+ * Listen for keys. Answers a function that stops listening.
112
+ *
113
+ * A no-op when stdin is not a terminal — `ream dev` piped into a file has no
114
+ * keyboard, and raw mode on a pipe throws.
115
+ */
116
+ export function installShortcuts(options: ShortcutsOptions = {}): () => void {
117
+ if (!process.stdin.isTTY) return () => {}
118
+
119
+ const onData = (data: Buffer | string): void => {
120
+ handleKey(data.toString(), options)
121
+ }
122
+ process.stdin.setRawMode(true)
123
+ process.stdin.on('data', onData)
124
+ // Nothing should stay alive for a keyboard: a server that finished its work
125
+ // must be able to exit with a listener still attached.
126
+ process.stdin.unref()
127
+
128
+ return () => {
129
+ process.stdin.setRawMode(false)
130
+ process.stdin.removeListener('data', onData)
131
+ process.stdin.pause()
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Hand the URL to whatever opens links here.
137
+ *
138
+ * Detached and ignored: an opener that writes to stderr, or outlives the dev
139
+ * server, must not take the terminal or the exit with it.
140
+ */
141
+ function openBrowser(url: string): void {
142
+ const command =
143
+ process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
144
+ try {
145
+ const child = spawn(command, [url], {
146
+ detached: true,
147
+ stdio: 'ignore',
148
+ shell: process.platform === 'win32',
149
+ })
150
+ child.unref()
151
+ child.on('error', () => {
152
+ // No opener on this machine — a headless container, say. The address was
153
+ // printed a moment ago; losing the click is not worth a crash.
154
+ })
155
+ } catch {
156
+ // Same.
157
+ }
158
+ }
@@ -7,14 +7,14 @@
7
7
  */
8
8
 
9
9
  import { ReamError } from './ReamError.js'
10
+ import { renderError } from './renderError.js'
10
11
 
11
12
  export function prettyPrintError(error: unknown): void {
13
+ // A framework error already says what it is, what it was doing and what to
14
+ // try — printing a source excerpt under it would bury that.
12
15
  if (error instanceof ReamError) {
13
16
  console.error(error.toDevString())
14
- } else if (error instanceof Error) {
15
- console.error(`\n ${error.message}\n`)
16
- if (error.stack) console.error(error.stack)
17
- } else {
18
- console.error(error)
17
+ return
19
18
  }
19
+ console.error(renderError(error))
20
20
  }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * An error, rendered for someone reading a terminal.
3
+ *
4
+ * Upstream hands this to youch. Written here instead, for the reason lumen was
5
+ * written rather than a colour package taken: a dependency is code we do not
6
+ * control, shipped into every application that installs the framework.
7
+ *
8
+ * The expensive part of doing this properly is usually reading source maps to
9
+ * get from a transpiled frame back to the line someone wrote. It is not needed
10
+ * here: `ream dev` runs TypeScript through swc-node, which already reports
11
+ * `app/controllers/users_controller.ts:24:11` — measured, not assumed. A build
12
+ * that does not, or a frame whose file cannot be read, falls back to the plain
13
+ * stack rather than showing nothing.
14
+ *
15
+ * NOTHING here may throw. This is what runs when something has already gone
16
+ * wrong, and an error thrown while rendering an error replaces the one you
17
+ * needed to see.
18
+ */
19
+
20
+ import { readFileSync } from 'node:fs'
21
+ import { relative, sep } from 'node:path'
22
+ import { fileURLToPath } from 'node:url'
23
+ import { ansiColors, silentColors, supportsColor } from '@c9up/lumen'
24
+
25
+ /** One line of a stack, taken apart. */
26
+ export interface StackFrame {
27
+ /** The function, when the runtime named one. */
28
+ callee?: string
29
+ file: string
30
+ line: number
31
+ column: number
32
+ /** Where it comes from, which is what decides how much of it is shown. */
33
+ origin: 'application' | 'dependency' | 'runtime'
34
+ }
35
+
36
+ export interface RenderOptions {
37
+ /** Paths are printed relative to this. Defaults to the working directory. */
38
+ cwd?: string
39
+ /** Lines of source shown around the failing one. */
40
+ context?: number
41
+ /** Force colour on or off. Defaults to what the stream takes. */
42
+ colors?: boolean
43
+ }
44
+
45
+ const AT_WITH_CALLEE = /^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/
46
+ const AT_BARE = /^\s*at\s+(.+):(\d+):(\d+)$/
47
+
48
+ /**
49
+ * Take a stack apart.
50
+ *
51
+ * A line that matches neither shape is dropped rather than guessed at: a
52
+ * malformed frame in the middle of a trace is worse than a shorter trace.
53
+ */
54
+ export function parseStack(stack: string | undefined, cwd: string): StackFrame[] {
55
+ if (stack === undefined) return []
56
+ const frames: StackFrame[] = []
57
+
58
+ for (const raw of stack.split('\n').slice(1)) {
59
+ const withCallee = AT_WITH_CALLEE.exec(raw)
60
+ const bare = withCallee === null ? AT_BARE.exec(raw) : null
61
+ const parts = withCallee ?? bare
62
+ if (parts === null) continue
63
+
64
+ const callee = withCallee === null ? undefined : parts[1]
65
+ const location = withCallee === null ? parts[1] : parts[2]
66
+ const line = Number(withCallee === null ? parts[2] : parts[3])
67
+ const column = Number(withCallee === null ? parts[3] : parts[4])
68
+ if (location === undefined || !Number.isInteger(line)) continue
69
+
70
+ const file = toPath(location)
71
+ frames.push({
72
+ ...(callee === undefined ? {} : { callee }),
73
+ file,
74
+ line,
75
+ column: Number.isInteger(column) ? column : 0,
76
+ origin: originOf(file, cwd),
77
+ })
78
+ }
79
+ return frames
80
+ }
81
+
82
+ /** The whole thing: message, the failing line in context, the trace, the cause. */
83
+ export function renderError(error: unknown, options: RenderOptions = {}): string {
84
+ try {
85
+ return render(error, options, new Set())
86
+ } catch {
87
+ // The renderer failed on the error it was asked to show. Whatever it was
88
+ // is still more useful than a second failure.
89
+ return String(error instanceof Error ? (error.stack ?? error.message) : error)
90
+ }
91
+ }
92
+
93
+ function render(error: unknown, options: RenderOptions, seen: Set<unknown>): string {
94
+ const colors = options.colors === undefined ? forStream() : palette(options.colors)
95
+ const cwd = options.cwd ?? process.cwd()
96
+
97
+ if (!(error instanceof Error)) {
98
+ return `\n ${colors.red('Error')} ${String(error)}\n`
99
+ }
100
+ // A cause that points back at something already rendered would recurse
101
+ // forever; errors do get wired in circles.
102
+ if (seen.has(error)) return ''
103
+ seen.add(error)
104
+
105
+ const out: string[] = ['']
106
+ const name = error.name === '' ? 'Error' : error.name
107
+ out.push(` ${colors.bgRed(colors.white(` ${name} `))} ${colors.red(error.message)}`)
108
+
109
+ const frames = parseStack(error.stack, cwd)
110
+ const first = frames.find((frame) => frame.origin === 'application') ?? frames[0]
111
+
112
+ if (first !== undefined) {
113
+ const excerpt = sourceExcerpt(first, options.context ?? 3, colors)
114
+ if (excerpt.length > 0) {
115
+ out.push('')
116
+ out.push(` ${colors.dim(`${relativeTo(first.file, cwd)}:${first.line}:${first.column}`)}`)
117
+ out.push('')
118
+ out.push(...excerpt)
119
+ }
120
+ }
121
+
122
+ const trace = frames.filter((frame) => frame.origin !== 'runtime')
123
+ if (trace.length > 0) {
124
+ out.push('')
125
+ for (const frame of trace.slice(0, 12)) {
126
+ const where = `${relativeTo(frame.file, cwd)}:${frame.line}`
127
+ const callee = frame.callee === undefined ? '' : `${frame.callee} `
128
+ const line = ` at ${callee}${where}`
129
+ // A frame from a dependency is dimmed rather than dropped: it is where
130
+ // the failure passed through, and hiding it makes a trace lie.
131
+ out.push(frame.origin === 'application' ? line : colors.dim(line))
132
+ }
133
+ }
134
+
135
+ const cause = 'cause' in error ? error.cause : undefined
136
+ if (cause !== undefined && cause !== null) {
137
+ const rendered = render(cause, options, seen)
138
+ if (rendered !== '') {
139
+ out.push('')
140
+ out.push(` ${colors.dim('caused by')}`)
141
+ out.push(rendered)
142
+ }
143
+ }
144
+
145
+ out.push('')
146
+ return out.join('\n')
147
+ }
148
+
149
+ /**
150
+ * The failing line, with the ones around it.
151
+ *
152
+ * Empty when the file cannot be read — a frame inside a bundle, a file deleted
153
+ * since the process started, a permission. The trace still prints.
154
+ */
155
+ function sourceExcerpt(
156
+ frame: StackFrame,
157
+ context: number,
158
+ colors: ReturnType<typeof ansiColors>,
159
+ ): string[] {
160
+ let source: string
161
+ try {
162
+ source = readFileSync(frame.file, 'utf8')
163
+ } catch {
164
+ return []
165
+ }
166
+
167
+ const lines = source.split('\n')
168
+ const start = Math.max(1, frame.line - context)
169
+ const end = Math.min(lines.length, frame.line + context)
170
+ const gutter = String(end).length
171
+
172
+ const out: string[] = []
173
+ for (let number = start; number <= end; number++) {
174
+ const text = lines[number - 1] ?? ''
175
+ const label = String(number).padStart(gutter, ' ')
176
+ if (number === frame.line) {
177
+ out.push(` ${colors.red('❯')} ${colors.red(label)} │ ${text}`)
178
+ if (frame.column > 0) {
179
+ // The caret under the column the runtime reported. Tabs are copied
180
+ // across so the caret lands under the character, not beside it.
181
+ const prefix = text.slice(0, frame.column - 1).replace(/[^\t]/g, ' ')
182
+ out.push(` ${' '.repeat(gutter)} │ ${prefix}${colors.red('^')}`)
183
+ }
184
+ } else {
185
+ out.push(` ${colors.dim(label)} ${colors.dim('│')} ${colors.dim(text)}`)
186
+ }
187
+ }
188
+ return out
189
+ }
190
+
191
+ /** `file:///a/b.ts` and `/a/b.ts` both name the same file. */
192
+ function toPath(location: string): string {
193
+ if (!location.startsWith('file:')) return location
194
+ try {
195
+ return fileURLToPath(location)
196
+ } catch {
197
+ return location
198
+ }
199
+ }
200
+
201
+ function originOf(file: string, cwd: string): StackFrame['origin'] {
202
+ if (file.startsWith('node:') || !file.includes(sep)) return 'runtime'
203
+ if (file.includes(`${sep}node_modules${sep}`)) return 'dependency'
204
+ return file.startsWith(cwd) ? 'application' : 'dependency'
205
+ }
206
+
207
+ /**
208
+ * The shortest path that still says where a frame is.
209
+ *
210
+ * A dependency frame is cut back to what comes after the last
211
+ * `node_modules/`, because the part before it is a store layout nobody reads:
212
+ * `@vitest/runner/dist/chunk-artifact.js`, not
213
+ * `…/.pnpm/@vitest+runner@4.1.11/node_modules/@vitest/runner/dist/…`.
214
+ */
215
+ function relativeTo(file: string, cwd: string): string {
216
+ if (file.startsWith('node:')) return file
217
+ const marker = `${sep}node_modules${sep}`
218
+ const last = file.lastIndexOf(marker)
219
+ if (last !== -1) return file.slice(last + marker.length)
220
+ const path = relative(cwd, file)
221
+ return path === '' || path.startsWith('..') ? file : path
222
+ }
223
+
224
+ function forStream(): ReturnType<typeof ansiColors> {
225
+ return supportsColor(process.stderr) ? ansiColors() : silentColors()
226
+ }
227
+
228
+ function palette(on: boolean): ReturnType<typeof ansiColors> {
229
+ return on ? ansiColors() : silentColors()
230
+ }
package/src/hot.ts CHANGED
@@ -28,7 +28,7 @@ import { readFile } from 'node:fs/promises'
28
28
  import { registerHooks } from 'node:module'
29
29
  import { dirname, relative, resolve as resolvePath } from 'node:path'
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url'
31
- import { fullReloadNotice } from './dev/fullReload.js'
31
+ import { clearScreen, fullReloadNotice, invalidatedNotice } from './dev/fullReload.js'
32
32
  import { hotReloadHappened } from './dev/hmr.js'
33
33
  import { readHotHookConfig } from './dev/hotConfig.js'
34
34
  import { globMatcher } from './dev/hotGlob.js'
@@ -184,10 +184,20 @@ watchProject({
184
184
  ...reloadPatterns.map(watchRootOf).filter((dir) => dir !== ''),
185
185
  ],
186
186
  files: [...restartFiles].map((file) => relative(root, file)),
187
+ // A watcher that quietly stops watching is how this went unnoticed for a
188
+ // release: the first save swapped, the rest did nothing at all.
189
+ onWarning: (message) => sayNow(`[ream] hot reload — ${message}`),
187
190
  onChange: (file) => {
188
191
  const decision = graph.decide(file)
192
+ // Silent, as upstream is: a file the running process never imported has
193
+ // nothing to swap and nothing to restart for, and upstream's dev server
194
+ // returns without a word when its own inspection comes back empty.
189
195
  if (decision.kind === 'ignore') return
190
196
  if (decision.kind === 'reload') {
197
+ // Upstream clears the terminal before this line, so the logs that follow
198
+ // the restart are the only ones on screen. `--no-clear` turns it off,
199
+ // which is the flag `ace serve` gives for the same thing.
200
+ clearScreen()
191
201
  sayNow(fullReloadNotice(decision.file, decision.reason, relativeToRoot))
192
202
  // Exiting on a known code is how the Rust parent learns it must restart —
193
203
  // see `EXIT_RESTART` in the CLI's dev module.
@@ -198,6 +208,6 @@ watchProject({
198
208
  // process keeps serving the class it promoted: a stable PID and stale
199
209
  // output, which is exactly how it was first reported.
200
210
  hotReloadHappened()
201
- sayNow(`[ream] hot swap — ${relativeToRoot(file)}`)
211
+ sayNow(invalidatedNotice(file, relativeToRoot))
202
212
  },
203
213
  })
package/src/index.ts CHANGED
@@ -91,6 +91,7 @@ export type {
91
91
  export { RedisDriver as SessionRedisDriver } from './session/drivers/RedisDriver.js'
92
92
  export {
93
93
  E_SESSION_NOT_READY,
94
+ E_SESSION_STORE_NOT_CONDITIONAL,
94
95
  E_SESSION_TAGGING_NOT_SUPPORTED,
95
96
  } from './session/errors.js'
96
97
  export type { ValuePath } from './session/ReadOnlyValuesStore.js'
@@ -220,6 +221,9 @@ export { createHttpKernel } from './HttpKernel.js'
220
221
 
221
222
  export type {
222
223
  ErrorHandlerClass,
224
+ GrpcCall,
225
+ GrpcHandler,
226
+ GrpcReply,
223
227
  LazyImport,
224
228
  MiddlewareClass,
225
229
  MiddlewareEntry,
@@ -249,6 +253,12 @@ export type {
249
253
  CommandOptions,
250
254
  FlagMetaData,
251
255
  } from './console/types.js'
256
+ export {
257
+ parseStack,
258
+ type RenderOptions,
259
+ renderError,
260
+ type StackFrame,
261
+ } from './errors/renderError.js'
252
262
  export type {
253
263
  HyperServerLike,
254
264
  IgnitorConfig,
@@ -399,4 +409,4 @@ export {
399
409
  StatsTracker,
400
410
  } from './scheduler/index.js'
401
411
 
402
- export const VERSION = '0.2.26'
412
+ export const VERSION = '0.2.28'
@@ -23,6 +23,35 @@ export interface NapiResponse {
23
23
  streamId?: string
24
24
  }
25
25
 
26
+ /**
27
+ * One gRPC call, handed to the JS handler registered with `onGrpc`.
28
+ *
29
+ * The message crosses as BYTES — never through the `String` (base64 for
30
+ * binary) body of the HTTP path.
31
+ */
32
+
33
+ export interface NapiGrpcCall {
34
+ /** `/package.Service/Method`. */
35
+ path: string
36
+ /** Request metadata, header names lowercased. */
37
+ metadata: Record<string, string>
38
+ /** The request message, still encoded. */
39
+ message: Uint8Array
40
+ /** Resolved client IP, by the same trusted-proxy rules as `request.ip`. */
41
+ ip: string
42
+ }
43
+
44
+ /**
45
+ * What the gRPC handler answers: a reply `message`, or a status `code`
46
+ * (with its `details`) instead of one.
47
+ */
48
+
49
+ export interface NapiGrpcReply {
50
+ message?: Uint8Array
51
+ code?: number
52
+ details?: string
53
+ }
54
+
26
55
  /**
27
56
  * Configuration for the wire-level shield filter. Mirrors the Rust
28
57
  * `ShieldConfig` shape; the JS layer constructs it from `ShieldMiddleware`
@@ -95,6 +124,15 @@ export declare class HyperServer {
95
124
  * this comment described until the boundary stopped carrying them.
96
125
  */
97
126
  onRequest(callback: (request: Record<string, unknown>) => Promise<NapiResponse>): void
127
+ /**
128
+ * Register the gRPC handler. From `listen()` on, `application/grpc`
129
+ * requests on this port are gRPC calls handed to it; without it they stay
130
+ * HTTP requests. Must be called BEFORE `listen()`.
131
+ *
132
+ * A rejected promise is answered `INTERNAL`, with nothing of the error
133
+ * sent to the client.
134
+ */
135
+ onGrpc(callback: (arg: NapiGrpcCall) => Promise<NapiGrpcReply>): void
98
136
  /**
99
137
  * Configure the trusted-proxy CIDR list used by `request.ip` resolution.
100
138
  * Must be called BEFORE `listen()`. Empty list = legacy permissive mode
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Where profiles live during a dev session.
3
+ *
4
+ * In memory and bounded. Symfony writes its profiles to disk so they survive a
5
+ * restart; here a restart is the common case — every saved file changes it —
6
+ * and a dev server that grows a directory nobody empties is a worse trade than
7
+ * losing the history of the session you just ended.
8
+ *
9
+ * The bound is a count, not a size: a profile holding 400 queries is the one
10
+ * you most want to keep, and dropping it for being big would lose exactly the
11
+ * request worth looking at.
12
+ */
13
+
14
+ import type { Profile, ProfilerCollector, ProfileStore, ProfileSummary } from './types.js'
15
+
16
+ /** How many requests are remembered. Enough to walk back through a flow. */
17
+ const DEFAULT_LIMIT = 50
18
+
19
+ export class MemoryProfileStore implements ProfileStore {
20
+ readonly #profiles = new Map<string, Profile>()
21
+ readonly #limit: number
22
+ readonly #collectors: readonly ProfilerCollector[]
23
+
24
+ constructor(collectors: readonly ProfilerCollector[] = [], limit = DEFAULT_LIMIT) {
25
+ this.#collectors = collectors
26
+ this.#limit = Math.max(1, limit)
27
+ }
28
+
29
+ save(profile: Profile): void {
30
+ this.#profiles.set(profile.token, profile)
31
+ // A Map iterates in insertion order, so the oldest key is the first one.
32
+ while (this.#profiles.size > this.#limit) {
33
+ const oldest = this.#profiles.keys().next()
34
+ if (oldest.done === true) break
35
+ this.#profiles.delete(oldest.value)
36
+ }
37
+ }
38
+
39
+ find(token: string): Profile | undefined {
40
+ return this.#profiles.get(token)
41
+ }
42
+
43
+ /** Newest first, which is the order anyone reading a history wants. */
44
+ recent(limit = 20): ProfileSummary[] {
45
+ const summaries: ProfileSummary[] = []
46
+ for (const profile of [...this.#profiles.values()].reverse()) {
47
+ if (summaries.length >= limit) break
48
+ summaries.push(this.#summarise(profile))
49
+ }
50
+ return summaries
51
+ }
52
+
53
+ clear(): void {
54
+ this.#profiles.clear()
55
+ }
56
+
57
+ /**
58
+ * The headline each collector wants shown.
59
+ *
60
+ * Asked of the collector rather than computed here: what counts as the
61
+ * headline of a panel is the panel's business, and a store that knew would
62
+ * have to be changed every time one is added.
63
+ */
64
+ #summarise(profile: Profile): ProfileSummary {
65
+ const badges: Record<string, string> = {}
66
+ for (const collector of this.#collectors) {
67
+ const data = profile.data[collector.name]
68
+ if (data === undefined || collector.badge === undefined) continue
69
+ const badge = collector.badge(data)
70
+ if (badge !== undefined) badges[collector.name] = badge
71
+ }
72
+ const { data: _data, ...rest } = profile
73
+ return { ...rest, badges }
74
+ }
75
+ }