@c9up/ream 0.2.4 → 0.2.6

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 (112) hide show
  1. package/README.md +3 -9
  2. package/dist/Application.d.ts +85 -5
  3. package/dist/Application.d.ts.map +1 -1
  4. package/dist/Application.js +171 -21
  5. package/dist/Application.js.map +1 -1
  6. package/dist/ErrorBoundary.d.ts +1 -7
  7. package/dist/ErrorBoundary.d.ts.map +1 -1
  8. package/dist/ErrorBoundary.js +31 -31
  9. package/dist/ErrorBoundary.js.map +1 -1
  10. package/dist/HttpKernel.js +2 -2
  11. package/dist/HttpKernel.js.map +1 -1
  12. package/dist/Ignitor.d.ts +18 -49
  13. package/dist/Ignitor.d.ts.map +1 -1
  14. package/dist/Ignitor.js +211 -173
  15. package/dist/Ignitor.js.map +1 -1
  16. package/dist/env/Env.d.ts +6 -0
  17. package/dist/env/Env.d.ts.map +1 -1
  18. package/dist/env/Env.js +6 -0
  19. package/dist/env/Env.js.map +1 -1
  20. package/dist/events/Emitter.d.ts +0 -6
  21. package/dist/events/Emitter.d.ts.map +1 -1
  22. package/dist/events/Emitter.js +42 -42
  23. package/dist/events/Emitter.js.map +1 -1
  24. package/dist/events/native.d.ts +13 -11
  25. package/dist/events/native.d.ts.map +1 -1
  26. package/dist/events/native.js.map +1 -1
  27. package/dist/helpers/compose.d.ts +30 -0
  28. package/dist/helpers/compose.d.ts.map +1 -0
  29. package/dist/helpers/compose.js +27 -0
  30. package/dist/helpers/compose.js.map +1 -0
  31. package/dist/helpers/index.d.ts +1 -0
  32. package/dist/helpers/index.d.ts.map +1 -1
  33. package/dist/helpers/index.js +1 -0
  34. package/dist/helpers/index.js.map +1 -1
  35. package/dist/helpers/is.d.ts +69 -16
  36. package/dist/helpers/is.d.ts.map +1 -1
  37. package/dist/helpers/is.js +79 -16
  38. package/dist/helpers/is.js.map +1 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/middleware/Pipeline.d.ts +9 -3
  44. package/dist/middleware/Pipeline.d.ts.map +1 -1
  45. package/dist/middleware/Pipeline.js +10 -4
  46. package/dist/middleware/Pipeline.js.map +1 -1
  47. package/dist/native/generated.d.ts +243 -0
  48. package/dist/native/generated.d.ts.map +1 -0
  49. package/dist/native/generated.js +7 -0
  50. package/dist/native/generated.js.map +1 -0
  51. package/dist/rpc/RpcRouter.js +2 -2
  52. package/dist/rpc/RpcRouter.js.map +1 -1
  53. package/dist/services/app.d.ts +6 -0
  54. package/dist/services/app.d.ts.map +1 -1
  55. package/dist/services/app.js +8 -0
  56. package/dist/services/app.js.map +1 -1
  57. package/dist/services/config.d.ts +15 -0
  58. package/dist/services/config.d.ts.map +1 -0
  59. package/dist/services/config.js +18 -0
  60. package/dist/services/config.js.map +1 -0
  61. package/dist/services/encryption.d.ts +23 -0
  62. package/dist/services/encryption.d.ts.map +1 -0
  63. package/dist/services/encryption.js +33 -0
  64. package/dist/services/encryption.js.map +1 -0
  65. package/dist/services/urlBuilder.d.ts +21 -0
  66. package/dist/services/urlBuilder.d.ts.map +1 -0
  67. package/dist/services/urlBuilder.js +34 -0
  68. package/dist/services/urlBuilder.js.map +1 -0
  69. package/dist/session/drivers/CookieDriver.d.ts +1 -1
  70. package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
  71. package/dist/session/drivers/CookieDriver.js +4 -4
  72. package/dist/session/drivers/CookieDriver.js.map +1 -1
  73. package/dist/testing/ApiResponse.d.ts +44 -44
  74. package/dist/testing/ApiResponse.d.ts.map +1 -1
  75. package/dist/testing/ApiResponse.js +48 -48
  76. package/dist/testing/ApiResponse.js.map +1 -1
  77. package/dist/testing/RequestBuilder.d.ts +39 -39
  78. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  79. package/dist/testing/RequestBuilder.js +49 -49
  80. package/dist/testing/RequestBuilder.js.map +1 -1
  81. package/dist/testing/TestClient.d.ts +5 -5
  82. package/dist/testing/TestClient.d.ts.map +1 -1
  83. package/dist/testing/TestClient.js +7 -7
  84. package/dist/testing/TestClient.js.map +1 -1
  85. package/events.win32-x64-msvc.node +0 -0
  86. package/index.win32-x64-msvc.node +0 -0
  87. package/package.json +15 -2
  88. package/scheduler.win32-x64-msvc.node +0 -0
  89. package/scripts/build-napi-types.mjs +68 -0
  90. package/scripts/generate-napi-types.mjs +171 -0
  91. package/src/Application.ts +221 -21
  92. package/src/ErrorBoundary.ts +31 -31
  93. package/src/HttpKernel.ts +2 -2
  94. package/src/Ignitor.ts +228 -176
  95. package/src/env/Env.ts +6 -0
  96. package/src/events/Emitter.ts +42 -42
  97. package/src/events/native.ts +21 -14
  98. package/src/helpers/compose.ts +84 -0
  99. package/src/helpers/index.ts +1 -0
  100. package/src/helpers/is.ts +87 -16
  101. package/src/index.ts +3 -3
  102. package/src/middleware/Pipeline.ts +10 -4
  103. package/src/native/generated.ts +291 -0
  104. package/src/rpc/RpcRouter.ts +2 -2
  105. package/src/services/app.ts +9 -0
  106. package/src/services/config.ts +24 -0
  107. package/src/services/encryption.ts +42 -0
  108. package/src/services/urlBuilder.ts +41 -0
  109. package/src/session/drivers/CookieDriver.ts +4 -4
  110. package/src/testing/ApiResponse.ts +52 -52
  111. package/src/testing/RequestBuilder.ts +55 -55
  112. package/src/testing/TestClient.ts +8 -8
package/src/Ignitor.ts CHANGED
@@ -41,12 +41,21 @@ import { CookieSigner } from './security/CookieSigner.js'
41
41
  import { SignedUrl } from './security/SignedUrl.js'
42
42
  import { Server } from './server/Server.js'
43
43
  import { clearApp, setApp } from './services/app.js'
44
+ import { clearEncryption, setEncryption } from './services/encryption.js'
44
45
  import { clearRouter, setRouter } from './services/router.js'
45
46
  import { clearServer, setServer } from './services/server.js'
46
47
  import { registerSessionTemplateTags } from './session/templateTags.js'
47
48
 
48
49
  /** Application environment. */
49
- export type AppEnvironment = 'web' | 'console' | 'test' | 'unknown'
50
+ /**
51
+ * Re-exported from `Application`, which is where it is declared: the
52
+ * environment belongs to the application (a provider reads
53
+ * `app.getEnvironment()`), and the Ignitor is only where the entry point
54
+ * declares it. Two declarations would be two types that happen to look alike.
55
+ */
56
+ export type { AppEnvironment } from './Application.js'
57
+
58
+ import type { AppEnvironment } from './Application.js'
50
59
 
51
60
  /**
52
61
  * Reamrc config — like AdonisJS adonisrc.ts with defineConfig().
@@ -94,7 +103,7 @@ export interface TestSuiteConfig {
94
103
  name: string
95
104
  /**
96
105
  * The suite's files: a glob, several, or a callback returning their URLs
97
- * (Japa `TestFiles`).
106
+ * (helix `TestFiles`).
98
107
  */
99
108
  files: string | string[] | (() => URL[] | Promise<URL[]>)
100
109
  /** Per-test timeout for this suite, in ms. */
@@ -102,7 +111,7 @@ export interface TestSuiteConfig {
102
111
  /** Extra attempts on failure for this suite. */
103
112
  retries?: number
104
113
  /**
105
- * Configure the suite before it runs (Japa `TestSuite.configure`). Receives
114
+ * Configure the suite before it runs (helix `TestSuite.configure`). Receives
106
115
  * the same handle as the bootstrap's `configureSuite`, and runs after it.
107
116
  *
108
117
  * Costs an import of the rc file in every worker, since a function cannot
@@ -128,7 +137,7 @@ export interface TestSuiteHandle {
128
137
  /**
129
138
  * The `tests` block of the rc file — AdonisJS `adonisrc.ts` `tests`, field for
130
139
  * field. The runner that consumes it is helix; ream reads the file, exactly as
131
- * `@adonisjs/core` reads adonisrc and hands the suites to Japa.
140
+ * `@adonisjs/core` reads adonisrc and hands the suites to helix.
132
141
  */
133
142
  export interface TestsConfig {
134
143
  /** Named suites. `ream test` with no argument runs them all, in order. */
@@ -144,8 +153,34 @@ export interface TestsConfig {
144
153
  bootstrap?: string
145
154
  }
146
155
 
156
+ /**
157
+ * Every option the `tests` block accepts.
158
+ *
159
+ * Kept beside the interface because it is the same list: TypeScript's
160
+ * excess-property check only fires on an object literal, so a key that reached
161
+ * `defineConfig` through a variable or a spread is invisible to it.
162
+ */
163
+ const TESTS_OPTIONS = ['bootstrap', 'forceExit', 'suites', 'timeout'] as const
164
+
147
165
  /** defineConfig helper — like AdonisJS defineConfig(). */
148
166
  export function defineConfig(config: ReamrcConfig): ReamrcConfig {
167
+ // A key nobody reads is worse than a rejected one: an option that was
168
+ // renamed, removed between versions, or simply mistyped sits in the rc file
169
+ // doing nothing, and the run behaves as if it had never been written. Say so
170
+ // instead, and list what the block does accept — which is also the answer to
171
+ // "then where did my option go".
172
+ const unknown = Object.keys(config.tests ?? {}).filter(
173
+ (key) => !TESTS_OPTIONS.includes(key as (typeof TESTS_OPTIONS)[number]),
174
+ )
175
+ if (unknown.length > 0) {
176
+ throw new ReamError(
177
+ 'E_REAMRC_UNKNOWN_OPTION',
178
+ `\`tests\` has no option named ${unknown.map((k) => `\`${k}\``).join(', ')}.`,
179
+ {
180
+ hint: `The \`tests\` block accepts: ${TESTS_OPTIONS.join(', ')}. An option removed between versions is gone from the rc file too — the runner is configured in the bootstrap module now.`,
181
+ },
182
+ )
183
+ }
149
184
  return config
150
185
  }
151
186
 
@@ -202,31 +237,36 @@ export interface IgnitorConfig {
202
237
  * Lifecycle: register → boot → start → ready → shutdown
203
238
  */
204
239
  export class Ignitor {
205
- private app: Application
206
- private router: Router
207
- private server: Server
208
- private middleware: MiddlewareRegistry
240
+ #app: Application
241
+ #router: Router
242
+ #server: Server
243
+ #middleware: MiddlewareRegistry
209
244
  /** Where each data package registers its migration runner. See `MigrationRegistry`. */
210
- private migrations: MigrationRegistry
211
- private errorBoundary: ErrorBoundary
212
- private _httpServer?: HyperServerLike
213
- private config: IgnitorConfig
214
- private appRoot?: URL
215
- private environment: AppEnvironment = 'unknown'
216
- private reamrc?: ReamrcConfig
217
- private providers: ProviderContract[] = []
218
- private errorListeners: Array<(event: ErrorEvent) => void> = []
219
- private phase: 'created' | 'registered' | 'booted' | 'started' | 'ready' | 'shutdown' = 'created'
220
- private hotReloadCleanup?: () => void
245
+ #migrations: MigrationRegistry
246
+ #errorBoundary: ErrorBoundary
247
+ /**
248
+ * The APP_KEY-backed signer, when there is one. Held so `stop()` can release
249
+ * the `services/encryption` locator against the exact instance it installed
250
+ * — an unguarded clear would tear down a newer Ignitor's binding.
251
+ */
252
+ #signer?: CookieSigner
253
+ #_httpServer?: HyperServerLike
254
+ #config: IgnitorConfig
255
+ #appRoot?: URL
256
+ #reamrc?: ReamrcConfig
257
+ #providers: ProviderContract[] = []
258
+ #errorListeners: Array<(event: ErrorEvent) => void> = []
259
+ #phase: 'created' | 'registered' | 'booted' | 'started' | 'ready' | 'shutdown' = 'created'
260
+ #hotReloadCleanup?: () => void
221
261
  #shutdownHandle?: ShutdownHandle
222
262
  #host?: string
223
263
  #console?: Console
224
264
 
225
265
  // Inline configuration (for simple use or testing)
226
- private inlineRoutes?: (router: Router) => void
227
- private inlineMiddleware: MiddlewareFunction[] = []
228
- private inlineNamedMiddleware: Array<[string, MiddlewareFunction]> = []
229
- private inlineProviderFactories: Array<(app: Application) => ProviderContract> = []
266
+ #inlineRoutes?: (router: Router) => void
267
+ #inlineMiddleware: MiddlewareFunction[] = []
268
+ #inlineNamedMiddleware: Array<[string, MiddlewareFunction]> = []
269
+ #inlineProviderFactories: Array<(app: Application) => ProviderContract> = []
230
270
 
231
271
  /**
232
272
  * Create the Ignitor.
@@ -239,47 +279,47 @@ export class Ignitor {
239
279
  */
240
280
  constructor(appRootOrConfig?: URL | IgnitorConfig, config?: IgnitorConfig) {
241
281
  if (appRootOrConfig instanceof URL) {
242
- this.appRoot = appRootOrConfig
243
- this.config = config ?? {}
282
+ this.#appRoot = appRootOrConfig
283
+ this.#config = config ?? {}
244
284
  } else {
245
- this.config = appRootOrConfig ?? {}
285
+ this.#config = appRootOrConfig ?? {}
246
286
  }
247
287
 
248
- this.app = new Application()
249
- this.router = new Router()
250
- this.server = new Server(this.router)
251
- this.middleware = new MiddlewareRegistry()
252
- this.migrations = new MigrationRegistry()
253
- this.errorBoundary = new ErrorBoundary((event) => this.handleError(event), this.isDevMode())
288
+ this.#app = new Application()
289
+ this.#router = new Router()
290
+ this.#server = new Server(this.#router)
291
+ this.#middleware = new MiddlewareRegistry()
292
+ this.#migrations = new MigrationRegistry()
293
+ this.#errorBoundary = new ErrorBoundary((event) => this.#handleError(event), this.isDevMode())
254
294
 
255
295
  // Register framework services in container
256
- this.app.container.singleton('router', () => this.router)
257
- this.app.container.singleton('server', () => this.server)
258
- this.app.container.singleton('middleware', () => this.middleware)
296
+ this.#app.container.singleton('router', () => this.#router)
297
+ this.#app.container.singleton('server', () => this.#server)
298
+ this.#app.container.singleton('middleware', () => this.#middleware)
259
299
  // Always bound, even with no data package: an EMPTY registry is what lets
260
300
  // the CLI tell "this app has no migrations" from "this app predates the
261
301
  // registry" and say something useful in each case.
262
- this.app.container.singleton('migrations', () => this.migrations)
263
- this.app.container.singleton('app', () => this.app)
302
+ this.#app.container.singleton('migrations', () => this.#migrations)
303
+ this.#app.container.singleton('app', () => this.#app)
264
304
  // AdonisJS binds the config store too, so code that only needs settings can
265
305
  // resolve `config` instead of taking the whole application.
266
- this.app.container.singleton('config', () => this.app.config)
306
+ this.#app.container.singleton('config', () => this.#app.config)
267
307
  // The HttpContext CLASS, so an integration provider can extend it the way
268
308
  // AdonisJS does — `HttpContext.getter('view', …)` is how the view layer
269
309
  // attaches `ctx.view`. Exported from the barrel too, but a provider that
270
310
  // must stay agnostic reaches it through the container instead of importing
271
311
  // ream at runtime.
272
- this.app.container.singleton('HttpContext', () => HttpContext)
312
+ this.#app.container.singleton('HttpContext', () => HttpContext)
273
313
  // `appRoot` is the URL passed to `new Ignitor(new URL('../', import.meta.url))`.
274
314
  // Providers resolve it through the container so they can interpret
275
315
  // relative paths in config files (e.g. `pages.root: './resources/pages'`)
276
316
  // against the project root — same convention `modules.path` uses.
277
- if (this.appRoot) {
278
- const root = this.appRoot
279
- this.app.container.singleton('appRoot', () => root)
317
+ if (this.#appRoot) {
318
+ const root = this.#appRoot
319
+ this.#app.container.singleton('appRoot', () => root)
280
320
  // Also expose it on the Application for the AdonisJS-style path helpers
281
321
  // (`app.makePath`, `app.configPath`, `app.migrationsPath`, …).
282
- this.app.setAppRoot(root)
322
+ this.#app.setAppRoot(root)
283
323
  }
284
324
 
285
325
  // Encryption / cookie-signing service (AdonisJS `APP_KEY` idiom). Registered
@@ -294,18 +334,20 @@ export class Ignitor {
294
334
  )
295
335
  }
296
336
  const signer = new CookieSigner(appKey)
297
- this.app.container.singleton('encryption', () => signer)
337
+ this.#app.container.singleton('encryption', () => signer)
338
+ this.#signer = signer
339
+ setEncryption(signer)
298
340
  // Signed-URL helper (same APP_KEY): the router signs via makeSignedUrl,
299
341
  // HttpContext hands it to the request so hasValidSignature() can verify.
300
342
  const signedUrl = new SignedUrl({ secret: appKey })
301
- this.app.container.singleton('signedUrl', () => signedUrl)
302
- this.router.setSignedUrl(signedUrl)
343
+ this.#app.container.singleton('signedUrl', () => signedUrl)
344
+ this.#router.setSignedUrl(signedUrl)
303
345
  }
304
346
 
305
347
  // Set service singletons so route/kernel files can import them
306
- setApp(this.app)
307
- setRouter(this.router)
308
- setServer(this.server)
348
+ setApp(this.#app)
349
+ setRouter(this.#router)
350
+ setServer(this.#server)
309
351
  }
310
352
 
311
353
  // ─── Configuration ────────────────────────────────────────
@@ -315,28 +357,34 @@ export class Ignitor {
315
357
  * Like AdonisJS: .tap((app) => { app.booting(...) })
316
358
  */
317
359
  tap(callback: (app: Application) => void): this {
318
- callback(this.app)
360
+ callback(this.#app)
319
361
  return this
320
362
  }
321
363
 
322
- /** Set the application environment. */
364
+ /**
365
+ * Set the application environment.
366
+ *
367
+ * Stored on the Application, which is where a provider reads it
368
+ * (`app.getEnvironment()`, the AdonisJS shape). The Ignitor keeps the setter
369
+ * because that is where the entry point declares it.
370
+ */
323
371
  setEnvironment(env: AppEnvironment): this {
324
- this.environment = env
372
+ this.#app.setEnvironment(env)
325
373
  return this
326
374
  }
327
375
 
328
376
  getEnvironment(): AppEnvironment {
329
- return this.environment
377
+ return this.#app.getEnvironment()
330
378
  }
331
379
 
332
380
  /**
333
381
  * Load the reamrc config (equivalent to adonisrc.ts).
334
382
  */
335
383
  useRcFile(reamrc: ReamrcConfig): this {
336
- this.reamrc = reamrc
384
+ this.#reamrc = reamrc
337
385
  // Hand the app its directory overrides now rather than at boot: a path
338
386
  // helper called from a provider's register() must already see them.
339
- this.app.rcContents(reamrc)
387
+ this.#app.rcContents(reamrc)
340
388
  return this
341
389
  }
342
390
 
@@ -344,37 +392,37 @@ export class Ignitor {
344
392
 
345
393
  /** Define routes inline (simple mode). */
346
394
  routes(callback: (router: Router) => void): this {
347
- this.inlineRoutes = callback
395
+ this.#inlineRoutes = callback
348
396
  return this
349
397
  }
350
398
 
351
399
  /** Add global middleware inline. */
352
400
  use(mw: MiddlewareFunction): this {
353
- this.inlineMiddleware.push(mw)
401
+ this.#inlineMiddleware.push(mw)
354
402
  return this
355
403
  }
356
404
 
357
405
  /** Register a named middleware inline. */
358
406
  named(name: string, mw: MiddlewareFunction): this {
359
- this.inlineNamedMiddleware.push([name, mw])
407
+ this.#inlineNamedMiddleware.push([name, mw])
360
408
  return this
361
409
  }
362
410
 
363
411
  /** Register a provider inline (for testing or simple apps). */
364
412
  provider(factory: (app: Application) => ProviderContract): this {
365
- this.inlineProviderFactories.push(factory)
413
+ this.#inlineProviderFactories.push(factory)
366
414
  return this
367
415
  }
368
416
 
369
417
  /** Listen for error events. */
370
418
  onError(listener: (event: ErrorEvent) => void): this {
371
- this.errorListeners.push(listener)
419
+ this.#errorListeners.push(listener)
372
420
  return this
373
421
  }
374
422
 
375
423
  /** Set a config value. */
376
424
  configure(key: string, value: unknown): this {
377
- this.app.config.set(key, value)
425
+ this.#app.config.set(key, value)
378
426
  return this
379
427
  }
380
428
 
@@ -382,19 +430,19 @@ export class Ignitor {
382
430
 
383
431
  /** Configure for HTTP server mode. */
384
432
  httpServer(): this {
385
- this.environment = 'web'
433
+ this.#app.setEnvironment('web')
386
434
  return this
387
435
  }
388
436
 
389
437
  /** Configure for CLI/console mode. Returns a ConsoleKernel for dispatching commands. */
390
438
  console(): ConsoleKernel {
391
- this.environment = 'console'
439
+ this.#app.setEnvironment('console')
392
440
  return new ConsoleKernel(this)
393
441
  }
394
442
 
395
443
  /** Configure for test mode. */
396
444
  testMode(): this {
397
- this.environment = 'test'
445
+ this.#app.setEnvironment('test')
398
446
  return this
399
447
  }
400
448
 
@@ -403,25 +451,25 @@ export class Ignitor {
403
451
  async start(): Promise<Ignitor> {
404
452
  // Double-start guard: a second start() would re-run phaseRegister and
405
453
  // instantiate + register every reamrc provider a second time.
406
- if (this.phase !== 'created') {
454
+ if (this.#phase !== 'created') {
407
455
  throw new ReamError(
408
456
  'IGNITOR_ALREADY_STARTED',
409
- `start() called while in phase '${this.phase}' — an Ignitor boots once`,
457
+ `start() called while in phase '${this.#phase}' — an Ignitor boots once`,
410
458
  { hint: 'Create a new Ignitor instance instead of restarting this one.' },
411
459
  )
412
460
  }
413
461
  this.#loadEnvironmentFiles()
414
462
  try {
415
- await this.phaseRegister()
416
- await this.phaseBoot()
417
- await this.phaseStart()
463
+ await this.#phaseRegister()
464
+ await this.#phaseBoot()
465
+ await this.#phaseStart()
418
466
  // Console documents its `consoleApp` service as available once the application has
419
467
  // booted, so the locator is installed here rather than on first use —
420
468
  // `import consoleApp from '@c9up/ream/services/console'` must not throw in a running
421
469
  // app. Only the façade is built (three small modules); loading the
422
470
  // commands themselves stays lazy, inside `consoleApp.boot()`.
423
471
  await this.consoleApp()
424
- await this.phaseReady()
472
+ await this.#phaseReady()
425
473
  } catch (err) {
426
474
  // A throw mid-boot (e.g. a provider ready() failing AFTER the HTTP port
427
475
  // is bound) must release everything the partial boot opened — port,
@@ -447,8 +495,8 @@ export class Ignitor {
447
495
  * `process.env` win, so the shell / CI always overrides the files.
448
496
  */
449
497
  #loadEnvironmentFiles(): void {
450
- if (!this.appRoot) return
451
- loadEnvFiles(this.appRoot, { skipEnvLocal: this.environment === 'test' })
498
+ if (!this.#appRoot) return
499
+ loadEnvFiles(this.#appRoot, { skipEnvLocal: this.#app.getEnvironment() === 'test' })
452
500
  }
453
501
 
454
502
  /**
@@ -458,10 +506,10 @@ export class Ignitor {
458
506
  * The engine is duck-typed out of the container so ream carries no dependency
459
507
  * on it, and an app with no template layer simply skips this.
460
508
  */
461
- private async registerSessionTemplateTags(): Promise<void> {
509
+ async #registerSessionTemplateTags(): Promise<void> {
462
510
  for (const token of ['inker', 'view']) {
463
- if (!this.app.container.has(token)) continue
464
- const binding: unknown = await this.app.container.resolve(token)
511
+ if (!this.#app.container.has(token)) continue
512
+ const binding: unknown = await this.#app.container.resolve(token)
465
513
  for (const candidate of [binding, Reflect.get(Object(binding), '_templates')]) {
466
514
  const register = Reflect.get(Object(candidate), 'registerTag')
467
515
  if (typeof register !== 'function') continue
@@ -475,106 +523,106 @@ export class Ignitor {
475
523
  }
476
524
  }
477
525
 
478
- private async phaseRegister(): Promise<void> {
526
+ async #phaseRegister(): Promise<void> {
479
527
  // Auto-load config/*.ts files into app.config
480
- await this.autoloadConfig()
528
+ await this.#autoloadConfig()
481
529
 
482
530
  // Load providers from reamrc
483
- if (this.reamrc?.providers) {
484
- for (const providerEntry of this.reamrc.providers) {
531
+ if (this.#reamrc?.providers) {
532
+ for (const providerEntry of this.#reamrc.providers) {
485
533
  const providerImport =
486
534
  typeof providerEntry === 'function' ? providerEntry : providerEntry.file
487
535
  const env = typeof providerEntry === 'function' ? undefined : providerEntry.environment
488
536
 
489
537
  // Skip providers not matching current environment
490
- if (env && !env.includes(this.environment)) continue
538
+ if (env && !env.includes(this.#app.getEnvironment())) continue
491
539
 
492
540
  const mod = await providerImport()
493
541
  const ProviderClass = mod.default
494
- const instance = new ProviderClass(this.app)
495
- this.providers.push(instance)
496
- this.app.register(instance)
542
+ const instance = new ProviderClass(this.#app)
543
+ this.#providers.push(instance)
544
+ this.#app.register(instance)
497
545
  }
498
546
  }
499
547
 
500
548
  // Register inline providers
501
- for (const factory of this.inlineProviderFactories) {
502
- const instance = factory(this.app)
503
- this.providers.push(instance)
504
- this.app.register(instance)
549
+ for (const factory of this.#inlineProviderFactories) {
550
+ const instance = factory(this.#app)
551
+ this.#providers.push(instance)
552
+ this.#app.register(instance)
505
553
  }
506
554
 
507
- this.phase = 'registered'
555
+ this.#phase = 'registered'
508
556
  }
509
557
 
510
- private async phaseBoot(): Promise<void> {
511
- await this.app.boot()
512
- this.phase = 'booted'
558
+ async #phaseBoot(): Promise<void> {
559
+ await this.#app.boot()
560
+ this.#phase = 'booted'
513
561
  }
514
562
 
515
- private async phaseStart(): Promise<void> {
563
+ async #phaseStart(): Promise<void> {
516
564
  // Import preload files (routes.ts, kernel.ts, etc.)
517
- if (this.reamrc?.preloads) {
518
- for (const preloadEntry of this.reamrc.preloads) {
565
+ if (this.#reamrc?.preloads) {
566
+ for (const preloadEntry of this.#reamrc.preloads) {
519
567
  const preloadImport = typeof preloadEntry === 'function' ? preloadEntry : preloadEntry.file
520
568
  const env =
521
569
  typeof preloadEntry === 'function'
522
570
  ? undefined
523
571
  : (preloadEntry as { environment?: string[] }).environment
524
572
 
525
- if (env && !env.includes(this.environment)) continue
573
+ if (env && !env.includes(this.#app.getEnvironment())) continue
526
574
  await preloadImport()
527
575
  }
528
576
  }
529
577
 
530
578
  // Auto-load module files (routes.ts, etc.) from modules directory
531
- await this.autoloadModules()
579
+ await this.#autoloadModules()
532
580
 
533
581
  // Apply inline configuration
534
- for (const mw of this.inlineMiddleware) {
535
- this.middleware.use(mw)
582
+ for (const mw of this.#inlineMiddleware) {
583
+ this.#middleware.use(mw)
536
584
  }
537
- for (const [name, mw] of this.inlineNamedMiddleware) {
538
- this.middleware.register(name, mw)
585
+ for (const [name, mw] of this.#inlineNamedMiddleware) {
586
+ this.#middleware.register(name, mw)
539
587
  }
540
- if (this.inlineRoutes) {
541
- this.inlineRoutes(this.router)
588
+ if (this.#inlineRoutes) {
589
+ this.#inlineRoutes(this.#router)
542
590
  }
543
591
 
544
592
  // Call start() on providers
545
- for (const provider of this.providers) {
593
+ for (const provider of this.#providers) {
546
594
  await callProviderPhase(provider, 'start')
547
595
  }
548
596
 
549
- this.phase = 'started'
597
+ this.#phase = 'started'
550
598
  }
551
599
 
552
- private async phaseReady(): Promise<void> {
600
+ async #phaseReady(): Promise<void> {
553
601
  // Boot the Server (resolves lazy error handler etc.)
554
- await this.server.boot()
602
+ await this.#server.boot()
555
603
 
556
604
  // Start HTTP server if in web mode
557
- if (this.environment === 'web' && this.config.serverFactory) {
605
+ if (this.#app.getEnvironment() === 'web' && this.#config.serverFactory) {
558
606
  // Build the HttpKernel with server middleware + router middleware.
559
607
  // The streaming backend is resolved lazily — the HyperServer is
560
608
  // created a few lines down, after the kernel — so the kernel
561
- // closes over the `this._httpServer` slot and reads it on every
609
+ // closes over the `this.#_httpServer` slot and reads it on every
562
610
  // request through the factory.
563
611
  const kernel = createHttpKernel({
564
- router: this.router,
565
- middleware: this.middleware,
566
- container: this.app.container,
612
+ router: this.#router,
613
+ middleware: this.#middleware,
614
+ container: this.#app.container,
567
615
  exceptionHandler:
568
- this.server.getErrorHandler() ?? new ExceptionHandler(!this.app.inProduction),
569
- serverMiddleware: this.server.getServerMiddleware(),
570
- routerMiddleware: this.router.getRouterMiddleware(),
616
+ this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
617
+ serverMiddleware: this.#server.getServerMiddleware(),
618
+ routerMiddleware: this.#router.getRouterMiddleware(),
571
619
  onError: (error, ctx) => {
572
- this.errorBoundary.serviceError('HttpKernel', error, ctx.id)
620
+ this.#errorBoundary.serviceError('HttpKernel', error, ctx.id)
573
621
  },
574
622
  // AdonisJS `config/app.ts` → `http: { allowMethodSpoofing: true }`.
575
- allowMethodSpoofing: this.app.config.get<boolean>('app.http.allowMethodSpoofing') === true,
623
+ allowMethodSpoofing: this.#app.config.get<boolean>('app.http.allowMethodSpoofing') === true,
576
624
  streamBackend: () => {
577
- const server = this._httpServer
625
+ const server = this.#_httpServer
578
626
  if (
579
627
  server &&
580
628
  typeof server.registerStream === 'function' &&
@@ -596,28 +644,28 @@ export class Ignitor {
596
644
  },
597
645
  })
598
646
 
599
- const desiredPort = this.config.port ?? 3000
647
+ const desiredPort = this.#config.port ?? 3000
600
648
  // Port-scan fallback (+1..+19) is a DEV convenience only. In production
601
649
  // a silent drift to :3001 while the LB targets :3000 is an outage —
602
650
  // bind the configured port and let EADDRINUSE fail loudly. The probe
603
651
  // also has an inherent TOCTOU; acceptable in dev, not in prod.
604
- const availablePort = this.app.inProduction
652
+ const availablePort = this.#app.inProduction
605
653
  ? desiredPort
606
654
  : await findAvailablePort(desiredPort)
607
655
  this.#host =
608
- this.config.host ?? process.env.HOST ?? (this.app.inProduction ? '0.0.0.0' : 'localhost')
609
- this._httpServer = this.config.serverFactory(availablePort, this.#host)
610
- this._httpServer.onRequest(kernel)
656
+ this.#config.host ?? process.env.HOST ?? (this.#app.inProduction ? '0.0.0.0' : 'localhost')
657
+ this.#_httpServer = this.#config.serverFactory(availablePort, this.#host)
658
+ this.#_httpServer.onRequest(kernel)
611
659
  // Pre-resolve client IPs in Rust from the trusted-proxy CIDRs before
612
660
  // listen. Security filtering itself lives in @c9up/blackhole.
613
- const trustedProxies = this.server.getTrustedProxies()
661
+ const trustedProxies = this.#server.getTrustedProxies()
614
662
  if (
615
663
  trustedProxies.length > 0 &&
616
- typeof this._httpServer.configureTrustedProxies === 'function'
664
+ typeof this.#_httpServer.configureTrustedProxies === 'function'
617
665
  ) {
618
- this._httpServer.configureTrustedProxies([...trustedProxies])
666
+ this.#_httpServer.configureTrustedProxies([...trustedProxies])
619
667
  }
620
- await this._httpServer.listen()
668
+ await this.#_httpServer.listen()
621
669
 
622
670
  // Wire OS-signal graceful shutdown. Without it the process never closes
623
671
  // the HTTP server on SIGTERM/SIGINT, so live keep-alive / SSE sockets keep
@@ -625,7 +673,7 @@ export class Ignitor {
625
673
  // force-kill, and an orchestrator's rolling deploy drops in-flight work.
626
674
  // onShutdown = this.stop(), which closes the port (aborting connections),
627
675
  // shuts providers down (DB pools), and releases the locators.
628
- if (this.config.gracefulShutdown !== false) {
676
+ if (this.#config.gracefulShutdown !== false) {
629
677
  this.#shutdownHandle = installGracefulShutdown({
630
678
  onShutdown: () => this.stop(),
631
679
  logger: {
@@ -634,7 +682,7 @@ export class Ignitor {
634
682
  },
635
683
  })
636
684
  }
637
- } else if (this.environment === 'web' && !this.config.serverFactory) {
685
+ } else if (this.#app.getEnvironment() === 'web' && !this.#config.serverFactory) {
638
686
  throw new ReamError(
639
687
  'IGNITOR_NO_SERVER_FACTORY',
640
688
  'httpServer() requires a serverFactory in config',
@@ -645,23 +693,23 @@ export class Ignitor {
645
693
  }
646
694
 
647
695
  // Install error boundary
648
- this.errorBoundary.install()
696
+ this.#errorBoundary.install()
649
697
 
650
698
  // Publish the flash-message tags to the template engine, if one is
651
699
  // installed. After boot(), so the engine's own provider has bound it;
652
700
  // before ready(), so a provider that pre-renders finds the tags in place.
653
- await this.registerSessionTemplateTags()
701
+ await this.#registerSessionTemplateTags()
654
702
 
655
703
  // Call ready() on providers
656
- for (const provider of this.providers) {
704
+ for (const provider of this.#providers) {
657
705
  await callProviderPhase(provider, 'ready')
658
706
  }
659
707
 
660
708
  // Core domain event: the app finished booting (all providers ready).
661
709
  // Emitted once through the bus when events are wired — zero hot-path cost.
662
- if (this.app.container.has('events')) {
663
- const bus = await this.app.container.resolve<Emitter>('events')
664
- bus.emit('app:ready', { environment: this.environment })
710
+ if (this.#app.container.has('events')) {
711
+ const bus = await this.#app.container.resolve<Emitter>('events')
712
+ bus.emit('app:ready', { environment: this.#app.getEnvironment() })
665
713
  }
666
714
 
667
715
  // Dev-mode change watcher. IMPORTANT: this does NOT attempt an in-process
@@ -676,11 +724,11 @@ export class Ignitor {
676
724
  // the Ignitor without a supervisor. A future true HMR needs a loader-hook
677
725
  // (hot-hook style) that can invalidate the ESM cache — plug it in here.
678
726
  if (this.isDevMode()) {
679
- const watchDirs = this.config.watchDirs ?? ['app', 'start']
680
- this.hotReloadCleanup = startHotReload({
727
+ const watchDirs = this.#config.watchDirs ?? ['app', 'start']
728
+ this.#hotReloadCleanup = startHotReload({
681
729
  watchDirs,
682
730
  onReload: () => {
683
- this.handleError({
731
+ this.#handleError({
684
732
  type: 'system.info',
685
733
  source: 'HotReload',
686
734
  message:
@@ -691,7 +739,7 @@ export class Ignitor {
691
739
  },
692
740
  logger: {
693
741
  info: (msg) =>
694
- this.handleError({
742
+ this.#handleError({
695
743
  type: 'system.info',
696
744
  source: 'HotReload',
697
745
  message: msg,
@@ -702,7 +750,7 @@ export class Ignitor {
702
750
  })
703
751
  }
704
752
 
705
- this.phase = 'ready'
753
+ this.#phase = 'ready'
706
754
  }
707
755
 
708
756
  /**
@@ -710,13 +758,13 @@ export class Ignitor {
710
758
  * Each file's default export is stored under its filename (without extension).
711
759
  * e.g. config/database.ts → app.config.get('database')
712
760
  */
713
- private async autoloadConfig(): Promise<void> {
761
+ async #autoloadConfig(): Promise<void> {
714
762
  const { readdirSync, existsSync } = await import('node:fs')
715
763
  const { join, basename } = await import('node:path')
716
764
  const { fileURLToPath, pathToFileURL } = await import('node:url')
717
765
 
718
- const configDir = this.appRoot
719
- ? join(fileURLToPath(this.appRoot), 'config')
766
+ const configDir = this.#appRoot
767
+ ? join(fileURLToPath(this.#appRoot), 'config')
720
768
  : join(process.cwd(), 'config')
721
769
 
722
770
  if (!existsSync(configDir)) return
@@ -739,7 +787,7 @@ export class Ignitor {
739
787
  // is what AdonisJS does (`fsImportAll`). Spread rather than stored as-is:
740
788
  // an ESM namespace is sealed and null-prototype, so a later merge into
741
789
  // this entry would fail on it.
742
- this.app.config.set(key, 'default' in mod ? mod.default : { ...mod })
790
+ this.#app.config.set(key, 'default' in mod ? mod.default : { ...mod })
743
791
  }
744
792
  }
745
793
 
@@ -747,8 +795,8 @@ export class Ignitor {
747
795
  * Auto-load module files (routes.ts, etc.) from the modules directory.
748
796
  * Scans modules.path for subdirectories and imports matching files.
749
797
  */
750
- private async autoloadModules(): Promise<void> {
751
- const modulesConfig = this.reamrc?.modules
798
+ async #autoloadModules(): Promise<void> {
799
+ const modulesConfig = this.#reamrc?.modules
752
800
  if (!modulesConfig?.path) return
753
801
 
754
802
  const { readdirSync, existsSync } = await import('node:fs')
@@ -757,8 +805,8 @@ export class Ignitor {
757
805
  const { pathToFileURL } = await import('node:url')
758
806
 
759
807
  // Resolve modules path relative to app root or cwd
760
- const basePath = this.appRoot
761
- ? join(fileURLToPath(this.appRoot), modulesConfig.path)
808
+ const basePath = this.#appRoot
809
+ ? join(fileURLToPath(this.#appRoot), modulesConfig.path)
762
810
  : resolve(modulesConfig.path)
763
811
 
764
812
  if (!existsSync(basePath)) return
@@ -819,22 +867,26 @@ export class Ignitor {
819
867
  this.#shutdownHandle = undefined
820
868
  })
821
869
  await attempt(() => {
822
- if (this.hotReloadCleanup) this.hotReloadCleanup()
870
+ if (this.#hotReloadCleanup) this.#hotReloadCleanup()
823
871
  })
824
872
  await attempt(async () => {
825
- if (this._httpServer) await this._httpServer.close()
873
+ if (this.#_httpServer) await this.#_httpServer.close()
826
874
  })
827
- await attempt(() => this.errorBoundary.uninstall())
828
- await attempt(() => this.app.shutdown())
875
+ await attempt(() => this.#errorBoundary.uninstall())
876
+ await attempt(() => this.#app.shutdown())
829
877
 
830
878
  // Release the process-wide service locators (ownership-guarded): a stopped
831
879
  // app must not keep its container/router reachable through the module
832
880
  // singletons, and must not clobber a newer Ignitor's bindings.
833
- clearApp(this.app)
834
- clearRouter(this.router)
835
- clearServer(this.server)
836
-
837
- this.phase = 'shutdown'
881
+ clearApp(this.#app)
882
+ clearRouter(this.#router)
883
+ clearServer(this.#server)
884
+ // `signedUrl` is what the router was handed; the signer behind
885
+ // `services/encryption` is the same object, so releasing it here keeps the
886
+ // locator from outliving the app that built it.
887
+ if (this.#signer) clearEncryption(this.#signer)
888
+
889
+ this.#phase = 'shutdown'
838
890
  if (errors.length === 1) throw errors[0]
839
891
  if (errors.length > 1) {
840
892
  throw new AggregateError(errors, 'Ignitor.stop() completed with errors')
@@ -844,7 +896,7 @@ export class Ignitor {
844
896
  // ─── Accessors ────────────────────────────────────────────
845
897
 
846
898
  async port(): Promise<number> {
847
- return this._httpServer ? this._httpServer.port() : 0
899
+ return this.#_httpServer ? this.#_httpServer.port() : 0
848
900
  }
849
901
 
850
902
  /**
@@ -858,7 +910,7 @@ export class Ignitor {
858
910
  }
859
911
 
860
912
  getApp(): Application {
861
- return this.app
913
+ return this.#app
862
914
  }
863
915
 
864
916
  /**
@@ -866,12 +918,12 @@ export class Ignitor {
866
918
  * Undefined for inline/test ignitors created without one.
867
919
  */
868
920
  getAppRoot(): URL | undefined {
869
- return this.appRoot
921
+ return this.#appRoot
870
922
  }
871
923
 
872
924
  /** Has the application already been started? */
873
925
  isStarted(): boolean {
874
- return this.phase === 'started' || this.phase === 'ready'
926
+ return this.#phase === 'started' || this.#phase === 'ready'
875
927
  }
876
928
 
877
929
  /**
@@ -892,26 +944,26 @@ export class Ignitor {
892
944
 
893
945
  this.#console = consoleApp
894
946
  setConsole(consoleApp)
895
- this.app.container.singleton('console', () => consoleApp)
947
+ this.#app.container.singleton('console', () => consoleApp)
896
948
  return consoleApp
897
949
  }
898
950
 
899
951
  /** The loaded rc file, if `useRcFile()` was called. */
900
952
  getRcFile(): ReamrcConfig | undefined {
901
- return this.reamrc
953
+ return this.#reamrc
902
954
  }
903
955
 
904
956
  /** The custom module importer, when the app provided one. */
905
957
  getImporter(): ((filePath: string) => Promise<unknown>) | undefined {
906
- return this.config.importer
958
+ return this.#config.importer
907
959
  }
908
960
 
909
961
  getRouter(): Router {
910
- return this.router
962
+ return this.#router
911
963
  }
912
964
 
913
965
  getServer(): Server {
914
- return this.server
966
+ return this.#server
915
967
  }
916
968
 
917
969
  /**
@@ -920,13 +972,13 @@ export class Ignitor {
920
972
  */
921
973
  getKernel(): (request: HttpKernelRequest) => Promise<HttpKernelResponse> {
922
974
  this.#kernel ??= createHttpKernel({
923
- router: this.router,
924
- middleware: this.middleware,
925
- container: this.app.container,
975
+ router: this.#router,
976
+ middleware: this.#middleware,
977
+ container: this.#app.container,
926
978
  exceptionHandler:
927
- this.server.getErrorHandler() ?? new ExceptionHandler(!this.app.inProduction),
928
- serverMiddleware: this.server.getServerMiddleware(),
929
- routerMiddleware: this.router.getRouterMiddleware(),
979
+ this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
980
+ serverMiddleware: this.#server.getServerMiddleware(),
981
+ routerMiddleware: this.#router.getRouterMiddleware(),
930
982
  })
931
983
  return this.#kernel
932
984
  }
@@ -934,15 +986,15 @@ export class Ignitor {
934
986
  #kernel?: (request: HttpKernelRequest) => Promise<HttpKernelResponse>
935
987
 
936
988
  isDevMode(): boolean {
937
- return this.app.inDev
989
+ return this.#app.inDev
938
990
  }
939
991
 
940
992
  getPhase(): string {
941
- return this.phase
993
+ return this.#phase
942
994
  }
943
995
 
944
- private handleError(event: ErrorEvent): void {
945
- for (const listener of this.errorListeners) {
996
+ #handleError(event: ErrorEvent): void {
997
+ for (const listener of this.#errorListeners) {
946
998
  try {
947
999
  listener(event)
948
1000
  } catch {
@@ -957,7 +1009,7 @@ export class Ignitor {
957
1009
  * shutdown logger so drain progress / timeouts are observable.
958
1010
  */
959
1011
  #emitSystemInfo(source: string, message: string, kind: 'info' | 'error'): void {
960
- this.handleError({
1012
+ this.#handleError({
961
1013
  type: kind === 'error' ? 'system.error' : 'system.info',
962
1014
  source,
963
1015
  message,