@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
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/ream",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Ream — Rust-powered Node.js application framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
Binary file
Binary file
Binary file
Binary file
Binary file
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,10 +40,18 @@ 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'
44
- import { Server } from './server/Server.js'
54
+ import { type GrpcHandler, Server } from './server/Server.js'
45
55
  import { clearApp, setApp } from './services/app.js'
46
56
  import { clearEncryption, setEncryption } from './services/encryption.js'
47
57
  import { clearLogger, setLogger } from './services/logger.js'
@@ -258,6 +268,8 @@ export function defineConfig(config: ReamrcConfig): ReamrcConfig {
258
268
  /** Minimal interface for the HTTP server (NAPI or mock). */
259
269
  export interface HyperServerLike {
260
270
  onRequest(callback: (request: HttpKernelRequest) => Promise<HttpKernelResponse>): void
271
+ /** Optional — present on the real `HyperServer`; required once `server.grpc()` is used. */
272
+ onGrpc?(callback: GrpcHandler): void
261
273
  /**
262
274
  * Optional — present on the real `HyperServer` (Rust-backed), absent on
263
275
  * trivial test mocks. Ignitor only calls it when a shield config is set.
@@ -345,6 +357,13 @@ export class Ignitor {
345
357
  #hotReloadCleanup?: () => void
346
358
  #shutdownHandle?: ShutdownHandle
347
359
  #host?: string
360
+ /**
361
+ * Stops listening for the dev-server keys. Only ever set under `ream dev`.
362
+ *
363
+ * Removed on shutdown, or a terminal left in raw mode would swallow the
364
+ * next command you type after the server exits.
365
+ */
366
+ #removeShortcuts?: () => void
348
367
  #console?: Console
349
368
 
350
369
  // Inline configuration (for simple use or testing)
@@ -812,6 +831,10 @@ export class Ignitor {
812
831
  // Boot the Server (resolves lazy error handler etc.)
813
832
  await this.#server.boot()
814
833
 
834
+ // Before the kernel: the kernel's server middleware and its dev script
835
+ // both read it.
836
+ await this.#startProfiler()
837
+
815
838
  // Start HTTP server if this process serves one
816
839
  if (this.#servesHttp() && this.#config.serverFactory) {
817
840
  // Build the HttpKernel with server middleware + router middleware.
@@ -830,7 +853,7 @@ export class Ignitor {
830
853
  ...this.#server.getServerMiddleware(),
831
854
  ],
832
855
  routerMiddleware: this.#router.getRouterMiddleware(),
833
- devReloadScript: this.#devReloadScript(),
856
+ devScript: this.#devScript(),
834
857
  onError: (error, ctx) => {
835
858
  this.#errorBoundary.serviceError('HttpKernel', error, ctx.id)
836
859
  },
@@ -882,6 +905,17 @@ export class Ignitor {
882
905
  this.#config.host ?? process.env.HOST ?? (this.#app.inProduction ? '0.0.0.0' : 'localhost')
883
906
  this.#_httpServer = this.#config.serverFactory(availablePort, this.#host)
884
907
  this.#_httpServer.onRequest(kernel)
908
+ const grpc = this.#server.getGrpcHandler()
909
+ if (grpc) {
910
+ if (typeof this.#_httpServer.onGrpc !== 'function') {
911
+ throw new ReamError(
912
+ 'E_IGNITOR_GRPC_UNSUPPORTED',
913
+ 'A gRPC handler is registered, but this server cannot serve gRPC.',
914
+ { hint: 'Use the native HyperServer, or a test server that implements onGrpc().' },
915
+ )
916
+ }
917
+ this.#_httpServer.onGrpc(grpc)
918
+ }
885
919
  // Pre-resolve client IPs in Rust from the trusted-proxy CIDRs before
886
920
  // listen. Security filtering itself lives in @c9up/blackhole.
887
921
  const trustedProxies = this.#server.getTrustedProxies()
@@ -992,6 +1026,23 @@ export class Ignitor {
992
1026
  // balancer at a process that cannot answer.
993
1027
  this.#app.markReady()
994
1028
  this.#phase = 'ready'
1029
+
1030
+ // The address, once there is one to print. Upstream's dev server prints
1031
+ // this sticker when the child reports ready; here the child prints it,
1032
+ // because a port found by scanning past a taken 3000 is known only to the
1033
+ // process that bound it.
1034
+ if (inDevServer() && this.#host !== undefined && this.#_httpServer !== undefined) {
1035
+ const port = await this.#_httpServer.port()
1036
+ printReadyBanner({
1037
+ host: this.#host,
1038
+ port,
1039
+ mode: 'HMR',
1040
+ bootMs: performance.now(),
1041
+ })
1042
+ // The keys upstream's dev server answers to. Installed here for the same
1043
+ // reason the sticker is printed here: `o` needs the address.
1044
+ this.#removeShortcuts = installShortcuts({ url: devServerUrl(this.#host, port) })
1045
+ }
995
1046
  }
996
1047
 
997
1048
  /**
@@ -1154,6 +1205,11 @@ export class Ignitor {
1154
1205
  async stop(): Promise<void> {
1155
1206
  const errors: unknown[] = []
1156
1207
 
1208
+ // First: a terminal left in raw mode swallows the next command you type
1209
+ // after the server exits, and that outlives the process.
1210
+ this.#removeShortcuts?.()
1211
+ this.#removeShortcuts = undefined
1212
+
1157
1213
  // Release the consoleApp locator first — ownership-guarded, so a second Ignitor
1158
1214
  // having rebound it is left alone.
1159
1215
  if (this.#console !== undefined) {
@@ -1296,7 +1352,7 @@ export class Ignitor {
1296
1352
  this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
1297
1353
  serverMiddleware: this.#server.getServerMiddleware(),
1298
1354
  routerMiddleware: this.#router.getRouterMiddleware(),
1299
- devReloadScript: this.#devReloadScript(),
1355
+ devScript: this.#devScript(),
1300
1356
  })
1301
1357
  return this.#kernel
1302
1358
  }
@@ -1308,29 +1364,67 @@ export class Ignitor {
1308
1364
  }
1309
1365
 
1310
1366
  /**
1311
- * The reload script, in development only.
1367
+ * What development appends to an HTML page: the reload client, and the
1368
+ * debug toolbar once a profile exists for the request.
1312
1369
  *
1313
1370
  * 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.
1371
+ * reload script is just as useful without it, since a restart is exactly
1372
+ * what it detects. In production this is `undefined` and the response is
1373
+ * untouched.
1374
+ *
1375
+ * The token is read back off the response rather than passed along a side
1376
+ * channel — the profiling middleware has already stamped it there, which is
1377
+ * also where a client outside the page reads it (Symfony's `X-Debug-Token`).
1316
1378
  */
1317
- #devReloadScript(): ((nonce?: string) => string) | undefined {
1318
- return this.isDevMode() ? hmrClientScript : undefined
1379
+ #devScript(): ((ctx: HttpContext) => string) | undefined {
1380
+ if (!this.isDevMode()) return undefined
1381
+ return (ctx) => {
1382
+ const nonce = ctx.response.nonce
1383
+ const reload = hmrClientScript(nonce)
1384
+ const token = ctx.response.getHeader(PROFILER_TOKEN_HEADER)
1385
+ return token === undefined ? reload : reload + toolbarScript(token, nonce)
1386
+ }
1319
1387
  }
1320
1388
 
1389
+ /**
1390
+ * The profiler, in development only.
1391
+ *
1392
+ * It needs the application's emitter — `db:query` is where the database
1393
+ * panel comes from — so it is built here rather than in a field initialiser,
1394
+ * once the container has one. An application without an event bus still gets
1395
+ * every other panel.
1396
+ */
1397
+ async #startProfiler(): Promise<void> {
1398
+ if (!this.isDevMode() || this.#profiler !== undefined) return
1399
+ const events = this.#app.container.has('events')
1400
+ ? await this.#app.container.resolve<Emitter>('events')
1401
+ : { on: () => undefined }
1402
+ this.#profiler = createProfiler(events)
1403
+ }
1404
+
1405
+ #profiler?: Profiler
1406
+
1321
1407
  /**
1322
1408
  * The server middleware the framework itself contributes, ahead of the
1323
1409
  * application's.
1324
1410
  *
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.
1411
+ * Development only, and the order is the point:
1412
+ *
1413
+ * 1. the profiler's own pages, which must not appear in their own history;
1414
+ * 2. the hot-reload stream, which answers first because it arrives once a
1415
+ * second per open tab and its content depends on nothing — no user, no
1416
+ * session, no body. Behind an application's middleware it paid for the
1417
+ * whole stack each time, which for anything that resolves a user from a
1418
+ * cookie meant a `SELECT` per second for a string holding a process id
1419
+ * and a counter. It is also open for as long as the tab is, so profiling
1420
+ * it would leave one profile unfinished forever;
1421
+ * 3. everything else, wrapped in a profile.
1331
1422
  */
1332
1423
  #frameworkServerMiddleware(): MiddlewareFunction[] {
1333
- return this.isDevMode() ? [hmrEndpoint()] : []
1424
+ if (!this.isDevMode()) return []
1425
+ const profiler = this.#profiler
1426
+ if (profiler === undefined) return [hmrEndpoint()]
1427
+ return [profilerEndpoint(profiler), hmrEndpoint(), profileRequest(profiler)]
1334
1428
  }
1335
1429
 
1336
1430
  getPhase(): string {
@@ -12,6 +12,7 @@
12
12
  import { stdin, stdout } from 'node:process'
13
13
  import { clearLine, createInterface, cursorTo } from 'node:readline'
14
14
  import { ReamError } from '../errors/ReamError.js'
15
+ import { runSelection } from './selection.js'
15
16
  import { colourise } from './ui.js'
16
17
 
17
18
  /** A selectable option — a bare string, or a value with its display text. */
@@ -246,31 +247,18 @@ export class Prompt {
246
247
  return this.#applyResult(picked[0], options)
247
248
  }
248
249
 
249
- const limit = options.limit ?? 10
250
- for (;;) {
251
- const typed = await this.readLine(this.#label(message, options, '(type to filter)'))
252
- const matches = choices.filter((choice) =>
253
- nameOf(choice).toLowerCase().includes(typed.toLowerCase()),
254
- )
255
-
256
- if (matches.length === 0) {
257
- stdout.write(`${colourise('No match.', 'yellow')}\n`)
258
- continue
259
- }
260
-
261
- const first = matches[0]
262
- if (matches.length === 1 && first !== undefined) {
263
- return this.#finish(nameOf(first), options)
264
- }
265
-
266
- const picked = await this.#select(
267
- message,
268
- matches.slice(0, limit),
269
- { ...options, name: undefined },
270
- false,
271
- )
272
- return picked[0]
273
- }
250
+ // One list that narrows as you type, rather than a filter question
251
+ // followed by a list: upstream's autocomplete is a single prompt, and
252
+ // answering twice for one value is a different thing wearing its name.
253
+ const picked = await this.#select(
254
+ message,
255
+ choices,
256
+ { ...options, name: undefined },
257
+ false,
258
+ true,
259
+ options.limit ?? 10,
260
+ )
261
+ return picked[0]
274
262
  }
275
263
 
276
264
  // ─── internals ──────────────────────────────────────────────
@@ -299,6 +287,8 @@ export class Prompt {
299
287
  choices: readonly PromptChoice[],
300
288
  options: SelectPromptOptions | MultiplePromptOptions,
301
289
  many: boolean,
290
+ filter?: boolean,
291
+ limit?: number,
302
292
  ): Promise<unknown[]> {
303
293
  if (choices.length === 0) {
304
294
  throw new ReamError('E_CONSOLE_EMPTY_CHOICES', `"${message}" was asked with no options.`)
@@ -313,50 +303,41 @@ export class Prompt {
313
303
  return many ? picked : picked.map((value) => this.#applyResult(value, options))
314
304
  }
315
305
 
316
- const defaultLabel =
317
- options.default === undefined
318
- ? undefined
319
- : [options.default]
320
- .flat()
321
- .map((index) => {
322
- const choice = choices[index]
323
- return choice === undefined ? String(index) : displayOf(choice)
324
- })
325
- .join(', ')
326
-
306
+ // Arrow keys, as upstream's prompts navigate. The list redraws in place
307
+ // until the user answers; a cancel is no answer, not an empty one.
327
308
  for (;;) {
328
- stdout.write(`${this.#label(message, { ...options, default: defaultLabel }).trimEnd()}\n`)
329
- choices.forEach((choice, index) => {
330
- const hint = typeof choice === 'string' ? undefined : choice.hint
331
- stdout.write(
332
- ` ${colourise(String(index + 1), 'cyan')}) ${displayOf(choice)}` +
333
- `${hint === undefined ? '' : colourise(` — ${hint}`, 'dim')}\n`,
334
- )
309
+ const outcome = await runSelection({
310
+ title: this.#label(message, { ...options, default: undefined }).trimEnd(),
311
+ items: choices.map((choice) => {
312
+ const hint = typeof choice === 'string' ? undefined : choice.hint
313
+ return hint === undefined
314
+ ? { label: displayOf(choice) }
315
+ : { label: displayOf(choice), hint }
316
+ }),
317
+ many,
318
+ ...(options.default === undefined
319
+ ? {}
320
+ : {
321
+ selected: [options.default]
322
+ .flat()
323
+ .filter(
324
+ (index): index is number =>
325
+ Number.isInteger(index) && index >= 0 && index < choices.length,
326
+ ),
327
+ }),
328
+ ...(filter === true ? { filter: true } : {}),
329
+ ...(limit === undefined ? {} : { limit }),
335
330
  })
336
331
 
337
- const answer = await this.readLine(
338
- many ? `Select (e.g. 1,3) from 1-${choices.length}: ` : `Select (1-${choices.length}): `,
339
- )
340
-
341
- // An empty answer takes the default — which for a selection prompt is an
342
- // index, as Adonis spells it. Without this the default was shown in the
343
- // label and then rejected as an invalid selection.
344
- const indexes =
345
- answer === '' && options.default !== undefined
346
- ? [options.default]
347
- .flat()
348
- .filter((index) => Number.isInteger(index) && index >= 0 && index < choices.length)
349
- : answer
350
- .split(',')
351
- .map((part) => Number(part.trim()) - 1)
352
- .filter((index) => Number.isInteger(index) && index >= 0 && index < choices.length)
353
-
354
- if (indexes.length === 0 || (!many && indexes.length !== 1)) {
355
- stdout.write(`${colourise('Invalid selection.', 'yellow')}\n`)
356
- continue
332
+ if (outcome.kind === 'cancelled') {
333
+ throw new ReamError('E_CONSOLE_PROMPT_CANCELLED', `"${message}" was cancelled.`, {
334
+ hint: 'Answer the prompt, or pass the value as a flag.',
335
+ })
357
336
  }
358
337
 
359
- const picked = indexes.map((index) => nameOf(choices[index] as PromptChoice))
338
+ // A multiple with nothing ticked is an answer of none, which is only
339
+ // valid if the command says so — `validate` is where that is decided.
340
+ const picked = outcome.indexes.map((index) => nameOf(choices[index] as PromptChoice))
360
341
  const problem = await this.#selectionProblem(picked, options, many)
361
342
  if (problem !== undefined) {
362
343
  stdout.write(`${colourise(problem, 'yellow')}\n`)