@c9up/ream 0.2.25 → 0.2.27

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 (178) hide show
  1. package/README.md +2 -0
  2. package/dist/Codemods.d.ts +12 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +114 -23
  5. package/dist/Codemods.js.map +1 -1
  6. package/dist/HttpKernel.d.ts +5 -1
  7. package/dist/HttpKernel.d.ts.map +1 -1
  8. package/dist/HttpKernel.js +6 -6
  9. package/dist/HttpKernel.js.map +1 -1
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +87 -14
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/console/Kernel.d.ts.map +1 -1
  14. package/dist/console/Kernel.js +20 -1
  15. package/dist/console/Kernel.js.map +1 -1
  16. package/dist/console/cliui.d.ts +9 -245
  17. package/dist/console/cliui.d.ts.map +1 -1
  18. package/dist/console/cliui.js +9 -583
  19. package/dist/console/cliui.js.map +1 -1
  20. package/dist/console/make/commands.d.ts +19 -0
  21. package/dist/console/make/commands.d.ts.map +1 -0
  22. package/dist/console/make/commands.js +252 -0
  23. package/dist/console/make/commands.js.map +1 -0
  24. package/dist/console/make/eject.d.ts +28 -0
  25. package/dist/console/make/eject.d.ts.map +1 -0
  26. package/dist/console/make/eject.js +161 -0
  27. package/dist/console/make/eject.js.map +1 -0
  28. package/dist/console/make/flush.d.ts +51 -0
  29. package/dist/console/make/flush.d.ts.map +1 -0
  30. package/dist/console/make/flush.js +93 -0
  31. package/dist/console/make/flush.js.map +1 -0
  32. package/dist/console/make/generators.d.ts +36 -0
  33. package/dist/console/make/generators.d.ts.map +1 -0
  34. package/dist/console/make/generators.js +148 -0
  35. package/dist/console/make/generators.js.map +1 -0
  36. package/dist/console/make/naming.d.ts +32 -0
  37. package/dist/console/make/naming.d.ts.map +1 -0
  38. package/dist/console/make/naming.js +82 -0
  39. package/dist/console/make/naming.js.map +1 -0
  40. package/dist/console/make/resolve.d.ts +46 -0
  41. package/dist/console/make/resolve.d.ts.map +1 -0
  42. package/dist/console/make/resolve.js +113 -0
  43. package/dist/console/make/resolve.js.map +1 -0
  44. package/dist/console/prompts.d.ts.map +1 -1
  45. package/dist/console/prompts.js +34 -46
  46. package/dist/console/prompts.js.map +1 -1
  47. package/dist/console/selection.d.ts +54 -0
  48. package/dist/console/selection.d.ts.map +1 -0
  49. package/dist/console/selection.js +235 -0
  50. package/dist/console/selection.js.map +1 -0
  51. package/dist/console/ui.d.ts +9 -15
  52. package/dist/console/ui.d.ts.map +1 -1
  53. package/dist/console/ui.js +11 -24
  54. package/dist/console/ui.js.map +1 -1
  55. package/dist/dev/fullReload.d.ts +35 -10
  56. package/dist/dev/fullReload.d.ts.map +1 -1
  57. package/dist/dev/fullReload.js +55 -14
  58. package/dist/dev/fullReload.js.map +1 -1
  59. package/dist/dev/hotWatcher.d.ts +19 -3
  60. package/dist/dev/hotWatcher.d.ts.map +1 -1
  61. package/dist/dev/hotWatcher.js +168 -18
  62. package/dist/dev/hotWatcher.js.map +1 -1
  63. package/dist/dev/readyBanner.d.ts +45 -0
  64. package/dist/dev/readyBanner.d.ts.map +1 -0
  65. package/dist/dev/readyBanner.js +72 -0
  66. package/dist/dev/readyBanner.js.map +1 -0
  67. package/dist/dev/shortcuts.d.ts +41 -0
  68. package/dist/dev/shortcuts.d.ts.map +1 -0
  69. package/dist/dev/shortcuts.js +142 -0
  70. package/dist/dev/shortcuts.js.map +1 -0
  71. package/dist/errors/prettyPrintError.d.ts.map +1 -1
  72. package/dist/errors/prettyPrintError.js +5 -8
  73. package/dist/errors/prettyPrintError.js.map +1 -1
  74. package/dist/errors/renderError.d.ts +46 -0
  75. package/dist/errors/renderError.d.ts.map +1 -0
  76. package/dist/errors/renderError.js +197 -0
  77. package/dist/errors/renderError.js.map +1 -0
  78. package/dist/hot.js +12 -2
  79. package/dist/hot.js.map +1 -1
  80. package/dist/index.d.ts +4 -3
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +5 -2
  83. package/dist/index.js.map +1 -1
  84. package/dist/profiler/MemoryProfileStore.d.ts +23 -0
  85. package/dist/profiler/MemoryProfileStore.d.ts.map +1 -0
  86. package/dist/profiler/MemoryProfileStore.js +70 -0
  87. package/dist/profiler/MemoryProfileStore.js.map +1 -0
  88. package/dist/profiler/Profiler.d.ts +56 -0
  89. package/dist/profiler/Profiler.d.ts.map +1 -0
  90. package/dist/profiler/Profiler.js +162 -0
  91. package/dist/profiler/Profiler.js.map +1 -0
  92. package/dist/profiler/collectors.d.ts +105 -0
  93. package/dist/profiler/collectors.d.ts.map +1 -0
  94. package/dist/profiler/collectors.js +164 -0
  95. package/dist/profiler/collectors.js.map +1 -0
  96. package/dist/profiler/endpoint.d.ts +27 -0
  97. package/dist/profiler/endpoint.d.ts.map +1 -0
  98. package/dist/profiler/endpoint.js +79 -0
  99. package/dist/profiler/endpoint.js.map +1 -0
  100. package/dist/profiler/index.d.ts +26 -0
  101. package/dist/profiler/index.d.ts.map +1 -0
  102. package/dist/profiler/index.js +41 -0
  103. package/dist/profiler/index.js.map +1 -0
  104. package/dist/profiler/middleware.d.ts +17 -0
  105. package/dist/profiler/middleware.d.ts.map +1 -0
  106. package/dist/profiler/middleware.js +79 -0
  107. package/dist/profiler/middleware.js.map +1 -0
  108. package/dist/profiler/render.d.ts +23 -0
  109. package/dist/profiler/render.d.ts.map +1 -0
  110. package/dist/profiler/render.js +141 -0
  111. package/dist/profiler/render.js.map +1 -0
  112. package/dist/profiler/toolbar.d.ts +34 -0
  113. package/dist/profiler/toolbar.d.ts.map +1 -0
  114. package/dist/profiler/toolbar.js +104 -0
  115. package/dist/profiler/toolbar.js.map +1 -0
  116. package/dist/profiler/types.d.ts +70 -0
  117. package/dist/profiler/types.d.ts.map +1 -0
  118. package/dist/profiler/types.js +11 -0
  119. package/dist/profiler/types.js.map +1 -0
  120. package/dist/router/Router.d.ts +11 -0
  121. package/dist/router/Router.d.ts.map +1 -1
  122. package/dist/router/Router.js +16 -0
  123. package/dist/router/Router.js.map +1 -1
  124. package/dist/stubs/index.d.ts +11 -0
  125. package/dist/stubs/index.d.ts.map +1 -0
  126. package/dist/stubs/index.js +12 -0
  127. package/dist/stubs/index.js.map +1 -0
  128. package/dist/stubs/template.d.ts +63 -0
  129. package/dist/stubs/template.d.ts.map +1 -0
  130. package/dist/stubs/template.js +209 -0
  131. package/dist/stubs/template.js.map +1 -0
  132. package/events.win32-x64-msvc.node +0 -0
  133. package/index.win32-x64-msvc.node +0 -0
  134. package/package.json +7 -1
  135. package/scheduler.win32-x64-msvc.node +0 -0
  136. package/src/Codemods.ts +153 -29
  137. package/src/HttpKernel.ts +12 -8
  138. package/src/Ignitor.ts +95 -14
  139. package/src/console/Kernel.ts +21 -1
  140. package/src/console/cliui.ts +44 -754
  141. package/src/console/make/commands.ts +270 -0
  142. package/src/console/make/eject.ts +163 -0
  143. package/src/console/make/flush.ts +125 -0
  144. package/src/console/make/generators.ts +176 -0
  145. package/src/console/make/naming.ts +85 -0
  146. package/src/console/make/resolve.ts +134 -0
  147. package/src/console/prompts.ts +45 -64
  148. package/src/console/selection.ts +274 -0
  149. package/src/console/ui.ts +12 -24
  150. package/src/dev/fullReload.ts +57 -15
  151. package/src/dev/hotWatcher.ts +165 -21
  152. package/src/dev/readyBanner.ts +84 -0
  153. package/src/dev/shortcuts.ts +158 -0
  154. package/src/errors/prettyPrintError.ts +5 -5
  155. package/src/errors/renderError.ts +230 -0
  156. package/src/hot.ts +12 -2
  157. package/src/index.ts +23 -3
  158. package/src/profiler/MemoryProfileStore.ts +75 -0
  159. package/src/profiler/Profiler.ts +182 -0
  160. package/src/profiler/collectors.ts +230 -0
  161. package/src/profiler/endpoint.ts +98 -0
  162. package/src/profiler/index.ts +56 -0
  163. package/src/profiler/middleware.ts +83 -0
  164. package/src/profiler/render.ts +173 -0
  165. package/src/profiler/toolbar.ts +106 -0
  166. package/src/profiler/types.ts +75 -0
  167. package/src/router/Router.ts +16 -0
  168. package/src/stubs/index.ts +14 -0
  169. package/src/stubs/template.ts +262 -0
  170. package/stubs/make/command.stub +29 -0
  171. package/stubs/make/controller.stub +30 -0
  172. package/stubs/make/entity.stub +11 -0
  173. package/stubs/make/event.stub +13 -0
  174. package/stubs/make/listener.stub +13 -0
  175. package/stubs/make/middleware.stub +16 -0
  176. package/stubs/make/provider.stub +26 -0
  177. package/stubs/make/service.stub +27 -0
  178. package/stubs/make/validator.stub +10 -0
package/src/Codemods.ts CHANGED
@@ -1,15 +1,77 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
+ import { render as renderTemplate, type StubState } from './stubs/template.js'
4
+
5
+ export type { StubState }
3
6
 
4
7
  export interface Codemods {
5
8
  addProvider(importPath: string): Promise<void>
6
9
  addMetaFile(pattern: string, reloadServer?: boolean): Promise<void>
7
10
  addEnvVars(vars: Record<string, string>): Promise<void>
8
11
  writeFile(filePath: string, content: string, options?: { force?: boolean }): Promise<void>
12
+ makeUsingStub(
13
+ stubsRoot: string,
14
+ stubPath: string,
15
+ state?: StubState,
16
+ options?: { force?: boolean },
17
+ ): Promise<GeneratedStub>
9
18
  registerCommand(importPath: string): Promise<void>
10
19
  registerMiddleware(importPath: string, options?: { tier?: 'server' | 'router' }): Promise<void>
11
20
  }
12
21
 
22
+ /** What a stub may interpolate. Scalars only — a stub is text, not a program. */
23
+
24
+ /** Where a stub landed, and what it wrote there. */
25
+ export interface GeneratedStub {
26
+ /** Relative to the project root. */
27
+ path: string
28
+ contents: string
29
+ }
30
+
31
+ /**
32
+ * A stub, split into the destination it declares and the body it renders.
33
+ *
34
+ * The format is the one `ream-cli`'s generators already read, deliberately:
35
+ * two stub dialects in one framework is a worse tax than the feature is worth.
36
+ *
37
+ * ---
38
+ * to: config/visa.ts
39
+ * ---
40
+ * export default defineConfig({ ... })
41
+ *
42
+ * Adonis declares the same thing with `{{{ exports({ to: … }) }}}`, which is
43
+ * JavaScript we cannot evaluate here — the front matter is that line, written
44
+ * so a text substitution can read it.
45
+ */
46
+ function splitFrontMatter(template: string): { to?: string; body: string } {
47
+ const trimmed = template.replace(/^[\uFEFF\r\n]+/, '')
48
+ if (!trimmed.startsWith('---')) return { body: template }
49
+
50
+ const after = trimmed.slice(3).replace(/^\r?\n/, '')
51
+ const end = after.indexOf('\n---')
52
+ if (end === -1) {
53
+ throw new Error('[configure] stub front matter opened with `---` but never closed')
54
+ }
55
+ const block = after.slice(0, end)
56
+ const body = after.slice(end + 4).replace(/^\r?\n/, '')
57
+
58
+ let to: string | undefined
59
+ for (const rawLine of block.split('\n')) {
60
+ const line = rawLine.trim()
61
+ if (line === '' || line.startsWith('#')) continue
62
+ const separator = line.indexOf(':')
63
+ if (separator === -1) {
64
+ throw new Error(`[configure] unreadable stub front-matter line: \`${line}\``)
65
+ }
66
+ const key = line.slice(0, separator).trim()
67
+ const value = line.slice(separator + 1).trim()
68
+ if (key === 'to') to = value
69
+ // Any other key is ignored rather than refused: a stub written for a
70
+ // later version of this loader must not break the current one.
71
+ }
72
+ return to === undefined ? { body } : { to, body }
73
+ }
74
+
13
75
  type MiddlewareTier = 'server' | 'router'
14
76
 
15
77
  function isMiddlewareTier(value: unknown): value is MiddlewareTier {
@@ -70,6 +132,55 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
70
132
  const force = options?.force ?? false
71
133
  const root = path.resolve(options?.cwd ?? process.cwd())
72
134
 
135
+ /**
136
+ * The one place a file reaches the disk.
137
+ *
138
+ * Shared by `writeFile` and `makeUsingStub` rather than copied, because what
139
+ * it does is not writing — it is refusing to write outside the project, to
140
+ * follow a symlink out of it, or over something that already exists.
141
+ */
142
+ async function writeGuarded(
143
+ filePath: string,
144
+ content: string,
145
+ shouldOverwrite: boolean,
146
+ ): Promise<void> {
147
+ if (path.isAbsolute(filePath)) {
148
+ throw new Error(`[configure] Absolute paths not allowed: ${filePath}`)
149
+ }
150
+ const resolved = path.resolve(root, filePath)
151
+ if (!resolved.startsWith(root + path.sep) && resolved !== root) {
152
+ throw new Error(`[configure] Refusing to write outside project root: ${filePath}`)
153
+ }
154
+
155
+ const shouldForce = shouldOverwrite
156
+ if (fs.existsSync(resolved) && !shouldForce) return
157
+
158
+ const dir = path.dirname(resolved)
159
+ fs.mkdirSync(dir, { recursive: true })
160
+
161
+ const canonRoot = fs.realpathSync(root)
162
+
163
+ if (fs.existsSync(dir)) {
164
+ const canonDir = fs.realpathSync(dir)
165
+ if (!canonDir.startsWith(canonRoot + path.sep) && canonDir !== canonRoot) {
166
+ throw new Error(
167
+ `[configure] Symlink escape detected: ${filePath} — directory resolves outside project root`,
168
+ )
169
+ }
170
+ }
171
+
172
+ if (fs.existsSync(resolved)) {
173
+ const canonFile = fs.realpathSync(resolved)
174
+ if (!canonFile.startsWith(canonRoot + path.sep)) {
175
+ throw new Error(
176
+ `[configure] Symlink escape detected: ${filePath} — file resolves outside project root`,
177
+ )
178
+ }
179
+ }
180
+
181
+ fs.writeFileSync(resolved, content)
182
+ }
183
+
73
184
  return {
74
185
  /**
75
186
  * Insert a provider import into `reamrc.ts` by string manipulation.
@@ -192,41 +303,54 @@ export function createCodemods(options?: { force?: boolean; cwd?: string }): Cod
192
303
  },
193
304
 
194
305
  async writeFile(filePath: string, content: string, opts?: { force?: boolean }): Promise<void> {
195
- if (path.isAbsolute(filePath)) {
196
- throw new Error(`[configure] Absolute paths not allowed: ${filePath}`)
197
- }
198
- const resolved = path.resolve(root, filePath)
199
- if (!resolved.startsWith(root + path.sep) && resolved !== root) {
200
- throw new Error(`[configure] Refusing to write outside project root: ${filePath}`)
201
- }
202
-
203
- const shouldForce = opts?.force ?? force
204
- if (fs.existsSync(resolved) && !shouldForce) return
205
-
206
- const dir = path.dirname(resolved)
207
- fs.mkdirSync(dir, { recursive: true })
306
+ return writeGuarded(filePath, content, opts?.force ?? force)
307
+ },
208
308
 
209
- const canonRoot = fs.realpathSync(root)
309
+ /**
310
+ * Render a stub and write it where the stub says.
311
+ *
312
+ * The application's own copy wins: a stub published to `stubs/<stubPath>`
313
+ * is used instead of the package's, which is what lets someone change the
314
+ * config a package generates without forking it. Adonis calls the same
315
+ * thing "searches in publishTarget first"; `ream-cli` already does it for
316
+ * the `make:` generators, and this is the same rule on the configure side.
317
+ *
318
+ * A package therefore names its stub after itself — `config/visa.stub`,
319
+ * not `config.stub` — or two packages would publish to the same path.
320
+ */
321
+ async makeUsingStub(
322
+ stubsRoot: string,
323
+ stubPath: string,
324
+ state: StubState = {},
325
+ opts?: { force?: boolean },
326
+ ): Promise<GeneratedStub> {
327
+ if (stubPath.includes('..') || path.isAbsolute(stubPath)) {
328
+ throw new Error(`[configure] Unsafe stub path: ${stubPath}`)
329
+ }
210
330
 
211
- if (fs.existsSync(dir)) {
212
- const canonDir = fs.realpathSync(dir)
213
- if (!canonDir.startsWith(canonRoot + path.sep) && canonDir !== canonRoot) {
214
- throw new Error(
215
- `[configure] Symlink escape detected: ${filePath} — directory resolves outside project root`,
216
- )
217
- }
331
+ const published = path.resolve(root, 'stubs', stubPath)
332
+ const shipped = path.resolve(stubsRoot, stubPath)
333
+ const source = fs.existsSync(published) ? published : shipped
334
+ if (!fs.existsSync(source)) {
335
+ throw new Error(`[configure] Stub not found: ${stubPath} (looked in ${stubsRoot})`)
218
336
  }
219
337
 
220
- if (fs.existsSync(resolved)) {
221
- const canonFile = fs.realpathSync(resolved)
222
- if (!canonFile.startsWith(canonRoot + path.sep)) {
223
- throw new Error(
224
- `[configure] Symlink escape detected: ${filePath} — file resolves outside project root`,
225
- )
226
- }
338
+ const raw = fs.readFileSync(source, 'utf8')
339
+ const { to, body } = splitFrontMatter(raw)
340
+ if (to === undefined) {
341
+ // Without it there is nowhere to put the file, and guessing from the
342
+ // stub's own name would put a package's config wherever it was named.
343
+ throw new Error(
344
+ `[configure] Stub ${stubPath} declares no destination — add a \`to:\` front matter line.`,
345
+ )
227
346
  }
228
347
 
229
- fs.writeFileSync(resolved, content)
348
+ // The destination goes through the engine too: `to: config/{{ name }}.ts`
349
+ // is the reason a single stub can serve several outputs.
350
+ const destination = renderTemplate(to, state)
351
+ const contents = renderTemplate(body, state)
352
+ await writeGuarded(destination, contents, opts?.force ?? force)
353
+ return { path: destination, contents }
230
354
  },
231
355
 
232
356
  /**
package/src/HttpKernel.ts CHANGED
@@ -41,8 +41,12 @@ export interface HttpKernelConfig {
41
41
  * handler produced. It lives here rather than in a middleware because this
42
42
  * is the one place every response passes through on its way out, whichever
43
43
  * route or error handler produced it.
44
+ *
45
+ * It is handed the whole context, not just the nonce: the debug toolbar
46
+ * carries the profile token inline, and that token is only on the response
47
+ * by the time the body is serialised.
44
48
  */
45
- devReloadScript?: (nonce?: string) => string
49
+ devScript?: (ctx: HttpContext) => string
46
50
  routerMiddleware?: MiddlewareFunction[]
47
51
  onError?: (error: unknown, ctx: HttpContext) => void
48
52
  debug?: boolean
@@ -375,7 +379,7 @@ export function createHttpKernel(
375
379
  correlationId,
376
380
  )
377
381
  }
378
- const serialized = await serializeResponse(ctx, config.devReloadScript)
382
+ const serialized = await serializeResponse(ctx, config.devScript)
379
383
  // The body is built; anything registered with `response.onFinish()` runs
380
384
  // now — after the answer is ready, before we hand it back.
381
385
  ctx.response.runFinishCallbacks()
@@ -431,7 +435,7 @@ export function createHttpKernel(
431
435
  correlationId,
432
436
  )
433
437
  }
434
- const serialized = await serializeResponse(ctx, config.devReloadScript)
438
+ const serialized = await serializeResponse(ctx, config.devScript)
435
439
  // The body is built; anything registered with `response.onFinish()` runs
436
440
  // now — after the answer is ready, before we hand it back.
437
441
  ctx.response.runFinishCallbacks()
@@ -484,18 +488,18 @@ function readControllerGuardMetadata(
484
488
 
485
489
  async function serializeResponse(
486
490
  ctx: HttpContext,
487
- devReloadScript?: (nonce?: string) => string,
491
+ devScript?: (ctx: HttpContext) => string,
488
492
  ): Promise<HttpKernelResponse> {
489
493
  // The terminal step, as AdonisJS' server calls `response.finish()`: it
490
494
  // stamps the request id and waits for a body still being produced.
491
495
  await ctx.response.finish()
492
496
  const streamId = ctx.response.getStreamId()
493
497
  const headers = ctx.response.getHeaders()
494
- const body = withDevReloadScript(
498
+ const body = withDevScript(
495
499
  ctx.response.getBody(),
496
500
  headers,
497
501
  streamId,
498
- devReloadScript === undefined ? undefined : devReloadScript(ctx.response.nonce),
502
+ devScript === undefined ? undefined : devScript(ctx),
499
503
  )
500
504
  return {
501
505
  status: ctx.response.getStatus(),
@@ -548,14 +552,14 @@ function reconcileRepresentation(
548
552
  }
549
553
 
550
554
  /**
551
- * Append the dev reload script to an HTML body.
555
+ * Append the development script to an HTML body.
552
556
  *
553
557
  * Only HTML, and never a stream: appending to an SSE or a download would
554
558
  * corrupt it, and a page is the only thing that can act on the script anyway.
555
559
  * Before `</body>` when there is one, so the script is inside the document
556
560
  * rather than trailing after it.
557
561
  */
558
- function withDevReloadScript(
562
+ function withDevScript(
559
563
  body: string,
560
564
  headers: Record<string, string>,
561
565
  streamId: string | undefined,
package/src/Ignitor.ts CHANGED
@@ -18,6 +18,8 @@ import type { Console } from './console/Console.js'
18
18
  import type { CommandLoader, Kernel as ConsoleKernelInstance } from './console/Kernel.js'
19
19
  import { type CommandClass, isCommandClass } from './console/types.js'
20
20
  import { hmrClientScript, hmrEndpoint } from './dev/hmr.js'
21
+ import { devServerUrl, inDevServer, printReadyBanner } from './dev/readyBanner.js'
22
+ import { installShortcuts } from './dev/shortcuts.js'
21
23
  import type { DirectoriesNode } from './directories.js'
22
24
  import type { ErrorEvent } from './ErrorBoundary.js'
23
25
  import { ErrorBoundary } from './ErrorBoundary.js'
@@ -38,6 +40,14 @@ import { MiddlewareRegistry } from './middleware/Pipeline.js'
38
40
  import { MigrationRegistry } from './migrations/MigrationRegistry.js'
39
41
  import type { AppContext, ProviderContract } from './Provider.js'
40
42
  import { callProviderPhase } from './Provider.js'
43
+ import {
44
+ createProfiler,
45
+ PROFILER_TOKEN_HEADER,
46
+ type Profiler,
47
+ profileRequest,
48
+ profilerEndpoint,
49
+ toolbarScript,
50
+ } from './profiler/index.js'
41
51
  import { Router } from './router/Router.js'
42
52
  import { CookieSigner } from './security/CookieSigner.js'
43
53
  import { SignedUrl } from './security/SignedUrl.js'
@@ -345,6 +355,13 @@ export class Ignitor {
345
355
  #hotReloadCleanup?: () => void
346
356
  #shutdownHandle?: ShutdownHandle
347
357
  #host?: string
358
+ /**
359
+ * Stops listening for the dev-server keys. Only ever set under `ream dev`.
360
+ *
361
+ * Removed on shutdown, or a terminal left in raw mode would swallow the
362
+ * next command you type after the server exits.
363
+ */
364
+ #removeShortcuts?: () => void
348
365
  #console?: Console
349
366
 
350
367
  // Inline configuration (for simple use or testing)
@@ -812,6 +829,10 @@ export class Ignitor {
812
829
  // Boot the Server (resolves lazy error handler etc.)
813
830
  await this.#server.boot()
814
831
 
832
+ // Before the kernel: the kernel's server middleware and its dev script
833
+ // both read it.
834
+ await this.#startProfiler()
835
+
815
836
  // Start HTTP server if this process serves one
816
837
  if (this.#servesHttp() && this.#config.serverFactory) {
817
838
  // Build the HttpKernel with server middleware + router middleware.
@@ -830,7 +851,7 @@ export class Ignitor {
830
851
  ...this.#server.getServerMiddleware(),
831
852
  ],
832
853
  routerMiddleware: this.#router.getRouterMiddleware(),
833
- devReloadScript: this.#devReloadScript(),
854
+ devScript: this.#devScript(),
834
855
  onError: (error, ctx) => {
835
856
  this.#errorBoundary.serviceError('HttpKernel', error, ctx.id)
836
857
  },
@@ -992,6 +1013,23 @@ export class Ignitor {
992
1013
  // balancer at a process that cannot answer.
993
1014
  this.#app.markReady()
994
1015
  this.#phase = 'ready'
1016
+
1017
+ // The address, once there is one to print. Upstream's dev server prints
1018
+ // this sticker when the child reports ready; here the child prints it,
1019
+ // because a port found by scanning past a taken 3000 is known only to the
1020
+ // process that bound it.
1021
+ if (inDevServer() && this.#host !== undefined && this.#_httpServer !== undefined) {
1022
+ const port = await this.#_httpServer.port()
1023
+ printReadyBanner({
1024
+ host: this.#host,
1025
+ port,
1026
+ mode: 'HMR',
1027
+ bootMs: performance.now(),
1028
+ })
1029
+ // The keys upstream's dev server answers to. Installed here for the same
1030
+ // reason the sticker is printed here: `o` needs the address.
1031
+ this.#removeShortcuts = installShortcuts({ url: devServerUrl(this.#host, port) })
1032
+ }
995
1033
  }
996
1034
 
997
1035
  /**
@@ -1154,6 +1192,11 @@ export class Ignitor {
1154
1192
  async stop(): Promise<void> {
1155
1193
  const errors: unknown[] = []
1156
1194
 
1195
+ // First: a terminal left in raw mode swallows the next command you type
1196
+ // after the server exits, and that outlives the process.
1197
+ this.#removeShortcuts?.()
1198
+ this.#removeShortcuts = undefined
1199
+
1157
1200
  // Release the consoleApp locator first — ownership-guarded, so a second Ignitor
1158
1201
  // having rebound it is left alone.
1159
1202
  if (this.#console !== undefined) {
@@ -1296,7 +1339,7 @@ export class Ignitor {
1296
1339
  this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
1297
1340
  serverMiddleware: this.#server.getServerMiddleware(),
1298
1341
  routerMiddleware: this.#router.getRouterMiddleware(),
1299
- devReloadScript: this.#devReloadScript(),
1342
+ devScript: this.#devScript(),
1300
1343
  })
1301
1344
  return this.#kernel
1302
1345
  }
@@ -1308,29 +1351,67 @@ export class Ignitor {
1308
1351
  }
1309
1352
 
1310
1353
  /**
1311
- * The reload script, in development only.
1354
+ * What development appends to an HTML page: the reload client, and the
1355
+ * debug toolbar once a profile exists for the request.
1312
1356
  *
1313
1357
  * Gated on the environment rather than on `hot-hook` being present: the
1314
- * script is just as useful without it, since a restart is exactly what it
1315
- * detects. In production it is `undefined` and the response is untouched.
1358
+ * reload script is just as useful without it, since a restart is exactly
1359
+ * what it detects. In production this is `undefined` and the response is
1360
+ * untouched.
1361
+ *
1362
+ * The token is read back off the response rather than passed along a side
1363
+ * channel — the profiling middleware has already stamped it there, which is
1364
+ * also where a client outside the page reads it (Symfony's `X-Debug-Token`).
1316
1365
  */
1317
- #devReloadScript(): ((nonce?: string) => string) | undefined {
1318
- return this.isDevMode() ? hmrClientScript : undefined
1366
+ #devScript(): ((ctx: HttpContext) => string) | undefined {
1367
+ if (!this.isDevMode()) return undefined
1368
+ return (ctx) => {
1369
+ const nonce = ctx.response.nonce
1370
+ const reload = hmrClientScript(nonce)
1371
+ const token = ctx.response.getHeader(PROFILER_TOKEN_HEADER)
1372
+ return token === undefined ? reload : reload + toolbarScript(token, nonce)
1373
+ }
1374
+ }
1375
+
1376
+ /**
1377
+ * The profiler, in development only.
1378
+ *
1379
+ * It needs the application's emitter — `db:query` is where the database
1380
+ * panel comes from — so it is built here rather than in a field initialiser,
1381
+ * once the container has one. An application without an event bus still gets
1382
+ * every other panel.
1383
+ */
1384
+ async #startProfiler(): Promise<void> {
1385
+ if (!this.isDevMode() || this.#profiler !== undefined) return
1386
+ const events = this.#app.container.has('events')
1387
+ ? await this.#app.container.resolve<Emitter>('events')
1388
+ : { on: () => undefined }
1389
+ this.#profiler = createProfiler(events)
1319
1390
  }
1320
1391
 
1392
+ #profiler?: Profiler
1393
+
1321
1394
  /**
1322
1395
  * The server middleware the framework itself contributes, ahead of the
1323
1396
  * application's.
1324
1397
  *
1325
- * Only the hot-reload token, and only in development. It answers first
1326
- * because the poll arrives once a second per open tab and its content
1327
- * depends on nothing — no user, no session, no body. Behind an application's
1328
- * middleware it paid for the whole stack each time, which for anything that
1329
- * resolves a user from a cookie meant a `SELECT` per second for a string
1330
- * holding a process id and a counter.
1398
+ * Development only, and the order is the point:
1399
+ *
1400
+ * 1. the profiler's own pages, which must not appear in their own history;
1401
+ * 2. the hot-reload stream, which answers first because it arrives once a
1402
+ * second per open tab and its content depends on nothing — no user, no
1403
+ * session, no body. Behind an application's middleware it paid for the
1404
+ * whole stack each time, which for anything that resolves a user from a
1405
+ * cookie meant a `SELECT` per second for a string holding a process id
1406
+ * and a counter. It is also open for as long as the tab is, so profiling
1407
+ * it would leave one profile unfinished forever;
1408
+ * 3. everything else, wrapped in a profile.
1331
1409
  */
1332
1410
  #frameworkServerMiddleware(): MiddlewareFunction[] {
1333
- return this.isDevMode() ? [hmrEndpoint()] : []
1411
+ if (!this.isDevMode()) return []
1412
+ const profiler = this.#profiler
1413
+ if (profiler === undefined) return [hmrEndpoint()]
1414
+ return [profilerEndpoint(profiler), hmrEndpoint(), profileRequest(profiler)]
1334
1415
  }
1335
1416
 
1336
1417
  getPhase(): string {
@@ -18,6 +18,7 @@ import { buildFlag } from './decorators.js'
18
18
  import { ExceptionHandler } from './ExceptionHandler.js'
19
19
  import HelpCommand from './HelpCommand.js'
20
20
  import ListCommand from './ListCommand.js'
21
+ import { MAKE_COMMANDS } from './make/commands.js'
21
22
  import { assignParsedValues, parseArgv, validateParsed } from './parser.js'
22
23
  import { Prompt } from './prompts.js'
23
24
  import { commandExec } from './tracing.js'
@@ -229,6 +230,14 @@ export class Kernel {
229
230
  this.register(HelpCommand)
230
231
  this.#defaults.add(HelpCommand.commandName)
231
232
 
233
+ // The generators. Registered as defaults, so an application that defines
234
+ // its own `make:controller` replaces this one rather than colliding with
235
+ // it — the same standing an app has over `list` and `help`.
236
+ for (const command of MAKE_COMMANDS) {
237
+ this.register(command)
238
+ this.#defaults.add(command.commandName)
239
+ }
240
+
232
241
  // Console's `--help`, a global flag whose listener runs the help command.
233
242
  this.defineFlag('help', {
234
243
  type: 'boolean',
@@ -460,7 +469,18 @@ export class Kernel {
460
469
  for (const metadata of await loader.getMetaData()) {
461
470
  // Metadata only — the class is imported by `find()`, when someone asks
462
471
  // for the command. `loading` / `loaded` belong there too.
463
- if (!this.#commands.has(metadata.commandName)) {
472
+ //
473
+ // A name the kernel registered ITSELF gives way: `list`, `help` and
474
+ // the generators are defaults, and an application that ships its own
475
+ // `make:controller` must get it. Without this the application declared
476
+ // a command that silently never ran — the same rule `register()`
477
+ // applies, applied on the path an application actually uses.
478
+ const isDefault = this.#defaults.has(metadata.commandName)
479
+ if (!this.#commands.has(metadata.commandName) || isDefault) {
480
+ if (isDefault) {
481
+ this.#commands.delete(metadata.commandName)
482
+ this.#defaults.delete(metadata.commandName)
483
+ }
464
484
  this.#pending.set(metadata.commandName, { metadata, loader })
465
485
  }
466
486
  // The metadata is what a manifest-style loader publishes; its aliases