@c9up/ream 0.1.24 → 0.1.26

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 (172) hide show
  1. package/dist/ConfigLoader.d.ts +46 -5
  2. package/dist/ConfigLoader.d.ts.map +1 -1
  3. package/dist/ConfigLoader.js +60 -9
  4. package/dist/ConfigLoader.js.map +1 -1
  5. package/dist/HttpKernel.d.ts.map +1 -1
  6. package/dist/HttpKernel.js +46 -6
  7. package/dist/HttpKernel.js.map +1 -1
  8. package/dist/Ignitor.d.ts.map +1 -1
  9. package/dist/Ignitor.js +21 -3
  10. package/dist/Ignitor.js.map +1 -1
  11. package/dist/container/Container.d.ts +23 -3
  12. package/dist/container/Container.d.ts.map +1 -1
  13. package/dist/container/Container.js +139 -60
  14. package/dist/container/Container.js.map +1 -1
  15. package/dist/env/Env.d.ts +29 -4
  16. package/dist/env/Env.d.ts.map +1 -1
  17. package/dist/env/Env.js +27 -4
  18. package/dist/env/Env.js.map +1 -1
  19. package/dist/env/Secret.d.ts +11 -0
  20. package/dist/env/Secret.d.ts.map +1 -0
  21. package/dist/env/Secret.js +36 -0
  22. package/dist/env/Secret.js.map +1 -0
  23. package/dist/env/interpolate.d.ts +26 -0
  24. package/dist/env/interpolate.d.ts.map +1 -0
  25. package/dist/env/interpolate.js +48 -0
  26. package/dist/env/interpolate.js.map +1 -0
  27. package/dist/env/loadEnvFiles.d.ts.map +1 -1
  28. package/dist/env/loadEnvFiles.js +14 -4
  29. package/dist/env/loadEnvFiles.js.map +1 -1
  30. package/dist/env/schema.d.ts +23 -6
  31. package/dist/env/schema.d.ts.map +1 -1
  32. package/dist/env/schema.js +67 -33
  33. package/dist/env/schema.js.map +1 -1
  34. package/dist/events/Emitter.d.ts +1 -1
  35. package/dist/events/Emitter.d.ts.map +1 -1
  36. package/dist/events/Emitter.js +1 -1
  37. package/dist/events/Emitter.js.map +1 -1
  38. package/dist/events/EventsProvider.d.ts +2 -2
  39. package/dist/events/EventsProvider.d.ts.map +1 -1
  40. package/dist/events/EventsProvider.js +3 -3
  41. package/dist/events/EventsProvider.js.map +1 -1
  42. package/dist/graphql/GraphQLEngine.d.ts +1 -1
  43. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  44. package/dist/graphql/GraphQLEngine.js +2 -2
  45. package/dist/graphql/GraphQLEngine.js.map +1 -1
  46. package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
  47. package/dist/graphql/GraphQLProvider.js +7 -3
  48. package/dist/graphql/GraphQLProvider.js.map +1 -1
  49. package/dist/http/Exception.d.ts +63 -1
  50. package/dist/http/Exception.d.ts.map +1 -1
  51. package/dist/http/Exception.js +185 -48
  52. package/dist/http/Exception.js.map +1 -1
  53. package/dist/http/HttpContext.d.ts +56 -9
  54. package/dist/http/HttpContext.d.ts.map +1 -1
  55. package/dist/http/HttpContext.js +56 -15
  56. package/dist/http/HttpContext.js.map +1 -1
  57. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  58. package/dist/http/RedirectBuilder.js +4 -1
  59. package/dist/http/RedirectBuilder.js.map +1 -1
  60. package/dist/http/Request.d.ts +141 -10
  61. package/dist/http/Request.d.ts.map +1 -1
  62. package/dist/http/Request.js +244 -34
  63. package/dist/http/Request.js.map +1 -1
  64. package/dist/http/Response.d.ts +176 -9
  65. package/dist/http/Response.d.ts.map +1 -1
  66. package/dist/http/Response.js +431 -19
  67. package/dist/http/Response.js.map +1 -1
  68. package/dist/index.d.ts +5 -4
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/middleware/Pipeline.js +1 -1
  73. package/dist/middleware/Pipeline.js.map +1 -1
  74. package/dist/openapi/OpenApiProvider.js +4 -4
  75. package/dist/openapi/OpenApiProvider.js.map +1 -1
  76. package/dist/router/Router.d.ts +223 -31
  77. package/dist/router/Router.d.ts.map +1 -1
  78. package/dist/router/Router.js +520 -102
  79. package/dist/router/Router.js.map +1 -1
  80. package/dist/rpc/RpcProvider.d.ts +1 -0
  81. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  82. package/dist/rpc/RpcProvider.js +7 -3
  83. package/dist/rpc/RpcProvider.js.map +1 -1
  84. package/dist/rpc/RpcRouter.d.ts +2 -2
  85. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  86. package/dist/rpc/RpcRouter.js +5 -4
  87. package/dist/rpc/RpcRouter.js.map +1 -1
  88. package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
  89. package/dist/scheduler/ScheduleProvider.js +7 -15
  90. package/dist/scheduler/ScheduleProvider.js.map +1 -1
  91. package/dist/server/Server.d.ts +17 -4
  92. package/dist/server/Server.d.ts.map +1 -1
  93. package/dist/server/Server.js +41 -7
  94. package/dist/server/Server.js.map +1 -1
  95. package/dist/session/SessionMiddleware.js +3 -3
  96. package/dist/session/SessionMiddleware.js.map +1 -1
  97. package/dist/storage/StaticProvider.js +1 -1
  98. package/dist/storage/StaticProvider.js.map +1 -1
  99. package/dist/testing/RequestBuilder.d.ts +60 -0
  100. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  101. package/dist/testing/RequestBuilder.js +180 -1
  102. package/dist/testing/RequestBuilder.js.map +1 -1
  103. package/dist/testing/TestClient.d.ts +22 -1
  104. package/dist/testing/TestClient.d.ts.map +1 -1
  105. package/dist/testing/TestClient.js +40 -0
  106. package/dist/testing/TestClient.js.map +1 -1
  107. package/dist/utils/Macroable.d.ts +30 -0
  108. package/dist/utils/Macroable.d.ts.map +1 -0
  109. package/dist/utils/Macroable.js +43 -0
  110. package/dist/utils/Macroable.js.map +1 -0
  111. package/dist/utils/inflect.d.ts +17 -0
  112. package/dist/utils/inflect.d.ts.map +1 -0
  113. package/dist/utils/inflect.js +32 -0
  114. package/dist/utils/inflect.js.map +1 -0
  115. package/dist/utils/objectPath.d.ts +57 -0
  116. package/dist/utils/objectPath.d.ts.map +1 -0
  117. package/dist/utils/objectPath.js +141 -0
  118. package/dist/utils/objectPath.js.map +1 -0
  119. package/events.darwin-arm64.node +0 -0
  120. package/events.darwin-x64.node +0 -0
  121. package/events.linux-arm64-gnu.node +0 -0
  122. package/events.linux-x64-gnu.node +0 -0
  123. package/events.win32-x64-msvc.node +0 -0
  124. package/index.darwin-arm64.node +0 -0
  125. package/index.darwin-x64.node +0 -0
  126. package/index.linux-arm64-gnu.node +0 -0
  127. package/index.linux-x64-gnu.node +0 -0
  128. package/index.win32-x64-msvc.node +0 -0
  129. package/package.json +6 -1
  130. package/scheduler.darwin-arm64.node +0 -0
  131. package/scheduler.darwin-x64.node +0 -0
  132. package/scheduler.linux-arm64-gnu.node +0 -0
  133. package/scheduler.linux-x64-gnu.node +0 -0
  134. package/scheduler.win32-x64-msvc.node +0 -0
  135. package/src/ConfigLoader.ts +72 -9
  136. package/src/HttpKernel.ts +53 -15
  137. package/src/Ignitor.ts +24 -3
  138. package/src/container/Container.ts +145 -61
  139. package/src/env/Env.ts +49 -5
  140. package/src/env/Secret.ts +41 -0
  141. package/src/env/interpolate.ts +54 -0
  142. package/src/env/loadEnvFiles.ts +12 -4
  143. package/src/env/schema.ts +109 -35
  144. package/src/events/Emitter.ts +2 -2
  145. package/src/events/EventsProvider.ts +5 -5
  146. package/src/graphql/GraphQLEngine.ts +4 -4
  147. package/src/graphql/GraphQLProvider.ts +7 -3
  148. package/src/http/Exception.ts +214 -37
  149. package/src/http/HttpContext.ts +93 -20
  150. package/src/http/RedirectBuilder.ts +4 -1
  151. package/src/http/Request.ts +264 -34
  152. package/src/http/Response.ts +504 -30
  153. package/src/index.ts +11 -2
  154. package/src/middleware/Pipeline.ts +1 -1
  155. package/src/openapi/OpenApiProvider.ts +4 -4
  156. package/src/router/Router.ts +631 -113
  157. package/src/rpc/RpcProvider.ts +6 -2
  158. package/src/rpc/RpcRouter.ts +7 -6
  159. package/src/scheduler/ScheduleProvider.ts +7 -15
  160. package/src/server/Server.ts +49 -10
  161. package/src/session/SessionMiddleware.ts +4 -4
  162. package/src/storage/StaticProvider.ts +1 -1
  163. package/src/testing/RequestBuilder.ts +218 -1
  164. package/src/testing/TestClient.ts +57 -1
  165. package/src/utils/Macroable.ts +54 -0
  166. package/src/utils/inflect.ts +29 -0
  167. package/src/utils/objectPath.ts +142 -0
  168. package/dist/decorators/Lazy.d.ts +0 -19
  169. package/dist/decorators/Lazy.d.ts.map +0 -1
  170. package/dist/decorators/Lazy.js +0 -53
  171. package/dist/decorators/Lazy.js.map +0 -1
  172. package/src/decorators/Lazy.ts +0 -57
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  import 'reflect-metadata'
13
- import { createLazyProxy, getLazyParams } from '../decorators/Lazy.js'
14
13
  import { getInjectTokens, getServiceMetadata, getServiceRegistry } from '../decorators/Service.js'
15
14
  import { didYouMean } from '../errors/FuzzyMatcher.js'
16
15
  import { ReamError } from '../errors/ReamError.js'
@@ -34,6 +33,10 @@ export class Container {
34
33
  * pointing at the same object after the test ends.
35
34
  */
36
35
  #singletonBackup: Map<string, unknown> = new Map()
36
+ /** Alias key → the token it forwards to (AdonisJS `container.alias`). */
37
+ #aliases: Map<string, ServiceToken> = new Map()
38
+ /** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
39
+ #resolvingHooks: Map<string, Array<(value: unknown) => void | Promise<void>>> = new Map()
37
40
  #resolutionStack: string[] = []
38
41
  #resolutionSet: Set<string> = new Set()
39
42
 
@@ -58,6 +61,33 @@ export class Container {
58
61
  this.#bindings.set(key, { token, factory: () => value, scope: 'singleton', dependencies: [] })
59
62
  }
60
63
 
64
+ /**
65
+ * Register an alias — resolving `alias` forwards to `target` (AdonisJS
66
+ * `container.alias('db', Database)`). The target may be a class, string, or
67
+ * symbol that itself resolves to a binding or an auto-constructable class.
68
+ */
69
+ alias(alias: ServiceToken, target: ServiceToken): void {
70
+ this.#aliases.set(this.#tokenToKey(alias), target)
71
+ }
72
+
73
+ /**
74
+ * Register a callback that runs each time `token` is constructed (AdonisJS
75
+ * `container.resolving`) — for lazy init (`db.connect()`) or decorating the
76
+ * resolved instance. Runs after construction of a binding/auto-constructed
77
+ * class; NOT for raw `bindValue` values or an already-cached singleton.
78
+ */
79
+ resolving(token: ServiceToken, callback: (value: unknown) => void | Promise<void>): void {
80
+ const key = this.#tokenToKey(token)
81
+ const hooks = this.#resolvingHooks.get(key)
82
+ if (hooks) hooks.push(callback)
83
+ else this.#resolvingHooks.set(key, [callback])
84
+ }
85
+
86
+ async #runResolvingHooks(key: string, value: unknown): Promise<void> {
87
+ const hooks = this.#resolvingHooks.get(key)
88
+ if (hooks) for (const hook of hooks) await hook(value)
89
+ }
90
+
61
91
  // ─── Testing ──────────────────────────────────────────────
62
92
 
63
93
  /**
@@ -115,18 +145,27 @@ export class Container {
115
145
  * 4. Auto-construct if class has @inject() or @Service()
116
146
  * 5. Auto-construct any class (plain `new Class()`) as fallback
117
147
  */
118
- make<T>(token: ServiceToken): T {
119
- return this.resolve<T>(token)
148
+ make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
149
+ return this.resolve<T>(token, runtimeValues)
120
150
  }
121
151
 
122
- /** Alias for make() — backward compatible. */
123
- resolve<T>(token: ServiceToken): T {
152
+ /**
153
+ * Resolve/construct a class or binding (AdonisJS `container.make` — async, so
154
+ * factories, providers and `resolving` hooks may be async). `runtimeValues`
155
+ * fill the resolved class's constructor slots by index (`make(Class, [req,
156
+ * res])`); only the `undefined` slots are container-resolved, top-level only.
157
+ */
158
+ async resolve<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
159
+ // Dereference an alias to its target before anything else.
160
+ const aliasTarget = this.#aliases.get(this.#tokenToKey(token))
161
+ if (aliasTarget !== undefined) return this.resolve<T>(aliasTarget, runtimeValues)
162
+
124
163
  const key = this.#tokenToKey(token)
125
164
 
126
165
  if (this.#resolutionSet.has(key)) {
127
166
  const cycle = [...this.#resolutionStack, key].join(' → ')
128
167
  throw new ReamError('CIRCULAR_DEPENDENCY', `Circular dependency detected: ${cycle}`, {
129
- hint: 'Use @Lazy() on one of the constructor parameters to break the cycle.',
168
+ hint: 'Break the cycle by resolving one dependency lazily inside a method (`await container.make(Dep)`) instead of injecting it in the constructor.',
130
169
  context: { chain: cycle },
131
170
  })
132
171
  }
@@ -134,7 +173,7 @@ export class Container {
134
173
  this.#resolutionSet.add(key)
135
174
 
136
175
  try {
137
- return this.#resolveInner<T>(key, token)
176
+ return await this.#resolveInner<T>(key, token, runtimeValues)
138
177
  } finally {
139
178
  this.#resolutionStack.pop()
140
179
  this.#resolutionSet.delete(key)
@@ -159,13 +198,17 @@ export class Container {
159
198
  Reflect.getMetadata('design:paramtypes', target.prototype, method) ?? []
160
199
 
161
200
  const len = Math.max(paramTypes.length, runtimeValues?.length ?? 0)
162
- const args = Array.from({ length: len }, (_, index) => {
163
- // Runtime values take precedence (and fill slots beyond paramTypes)
164
- if (runtimeValues && index < runtimeValues.length) return runtimeValues[index]
201
+ // Sequential resolution keeps the shared cycle-detection stack consistent.
202
+ const args: unknown[] = []
203
+ for (let index = 0; index < len; index += 1) {
204
+ // Runtime values take precedence (and fill slots beyond paramTypes).
205
+ if (runtimeValues && index < runtimeValues.length) {
206
+ args.push(runtimeValues[index])
207
+ continue
208
+ }
165
209
  const type = paramTypes[index]
166
- if (isClassConstructor(type)) return this.resolve(type)
167
- return undefined
168
- })
210
+ args.push(isInjectableClass(type) ? await this.resolve(type) : undefined)
211
+ }
169
212
 
170
213
  const member: unknown = instance[method]
171
214
  if (!isCallable(member)) {
@@ -183,10 +226,16 @@ export class Container {
183
226
  this.#bindings.has(key) ||
184
227
  this.#overrides.has(key) ||
185
228
  this.#singletons.has(key) ||
229
+ this.#aliases.has(key) ||
186
230
  (typeof token === 'function' && getServiceMetadata(token) !== undefined)
187
231
  )
188
232
  }
189
233
 
234
+ /** True only when EVERY token is registered/resolvable (AdonisJS `hasAllBindings`). */
235
+ hasAllBindings(tokens: ServiceToken[]): boolean {
236
+ return tokens.every((token) => this.has(token))
237
+ }
238
+
190
239
  get size(): number {
191
240
  return this.#bindings.size
192
241
  }
@@ -209,34 +258,35 @@ export class Container {
209
258
 
210
259
  // ─── Internal resolution ──────────────────────────────────
211
260
 
212
- #resolveInner<T>(key: string, token: ServiceToken): T {
213
- // 1. Check swaps (test overrides)
261
+ async #resolveInner<T>(key: string, token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
262
+ // 1. Check swaps (test overrides) — a swap factory may be async.
214
263
  if (this.#overrides.has(key)) {
215
- // biome-ignore lint/suspicious/noExplicitAny: IoC container factory returns unknown; caller brands via T
216
- return this.#overrides.get(key)?.() as any as T
264
+ const swapped = await this.#overrides.get(key)?.()
265
+ return swapped as T
217
266
  }
218
267
 
219
268
  // 2. Check cached singletons
220
269
  if (this.#singletons.has(key)) {
221
- // biome-ignore lint/suspicious/noExplicitAny: IoC singleton stored as unknown; caller brands via T
222
- return this.#singletons.get(key) as any as T
270
+ return this.#singletons.get(key) as T
223
271
  }
224
272
 
225
- // 3. Check explicit bindings
273
+ // 3. Check explicit bindings — the factory may be async.
226
274
  const binding = this.#bindings.get(key)
227
275
  if (binding) {
228
- const instance = binding.factory ? binding.factory() : undefined
276
+ const instance = binding.factory ? await binding.factory() : undefined
229
277
  if (binding.scope === 'singleton') {
230
278
  this.#singletons.set(key, instance)
231
279
  }
232
- // biome-ignore lint/suspicious/noExplicitAny: IoC binding factory returns unknown; caller brands via T
233
- return instance as any as T
280
+ await this.#runResolvingHooks(key, instance)
281
+ // Boundary cast from `unknown` — the caller brands the resolved type via T.
282
+ return instance as T
234
283
  }
235
284
 
236
285
  // 4. Auto-construct if it's a class
237
286
  if (typeof token === 'function') {
238
- // biome-ignore lint/suspicious/noExplicitAny: IoC auto-construct returns unknown; caller brands via T
239
- return this.#autoConstruct(token) as any as T
287
+ const instance = (await this.#autoConstruct(token, runtimeValues)) as T
288
+ await this.#runResolvingHooks(key, instance)
289
+ return instance
240
290
  }
241
291
 
242
292
  // 5. Not found
@@ -259,7 +309,10 @@ export class Container {
259
309
  * 2. Reflect.getMetadata('design:paramtypes') — decorator metadata (requires SWC/tsc)
260
310
  * 3. No params → plain `new Class()`
261
311
  */
262
- #autoConstruct(target: new (...args: unknown[]) => unknown): unknown {
312
+ async #autoConstruct(
313
+ target: new (...args: unknown[]) => unknown,
314
+ runtimeValues?: unknown[],
315
+ ): Promise<unknown> {
263
316
  const metadata = getServiceMetadata(target)
264
317
  const scope = metadata?.scope ?? 'transient'
265
318
  const key = metadata?.as ?? target.name
@@ -268,10 +321,12 @@ export class Container {
268
321
  return this.#singletons.get(key)
269
322
  }
270
323
 
324
+ // A runtime value at this index (from `make(Class, [req, res])`) wins over
325
+ // container resolution for that constructor slot.
326
+ const runtimeAt = (index: number): unknown => runtimeValues?.[index]
327
+
271
328
  // 1. Check static containerInjections (AdonisJS Fold-compatible, works
272
- // without emitDecoratorMetadata). The `in` operator narrows the optional
273
- // field to `unknown`, then `readContainerInjectionDeps` validates the
274
- // shape at runtime — no type-level cast required.
329
+ // without emitDecoratorMetadata).
275
330
  const explicitDeps = readContainerInjectionDeps(target)
276
331
 
277
332
  // 2. Fallback to reflect-metadata
@@ -279,7 +334,6 @@ export class Container {
279
334
  explicitDeps ?? Reflect.getMetadata('design:paramtypes', target) ?? []
280
335
 
281
336
  const injectTokens = getInjectTokens(target)
282
- const lazyIndices = getLazyParams(target)
283
337
 
284
338
  // No `design:paramtypes` (a dev transpiler may not emit decorator metadata —
285
339
  // esbuild / tsx don't). Recover what we can, and NEVER construct with
@@ -287,22 +341,34 @@ export class Container {
287
341
  if (paramTypes.length === 0) {
288
342
  // (a) `@Inject(token)` records its tokens INDEPENDENTLY of decorator
289
343
  // metadata, so even without `design:paramtypes` we can resolve the
290
- // constructor from that map alone.
344
+ // constructor from that map alone. Deps resolve SEQUENTIALLY so the
345
+ // shared cycle-detection stack stays consistent.
291
346
  if (injectTokens.size > 0) {
292
- const maxIndex = Math.max(...injectTokens.keys())
293
- const deps = Array.from({ length: maxIndex + 1 }, (_value, index) => {
294
- const depToken = injectTokens.get(index)
295
- if (!depToken) return undefined
296
- if (lazyIndices.includes(index)) {
297
- return createLazyProxy(() => this.resolve<object>(depToken))
347
+ const maxIndex = Math.max(...injectTokens.keys(), (runtimeValues?.length ?? 0) - 1)
348
+ const deps: unknown[] = []
349
+ for (let index = 0; index <= maxIndex; index += 1) {
350
+ const rt = runtimeAt(index)
351
+ if (rt !== undefined) {
352
+ deps.push(rt)
353
+ continue
298
354
  }
299
- return this.resolve(depToken)
300
- })
355
+ const depToken = injectTokens.get(index)
356
+ deps.push(depToken ? await this.resolve(depToken) : undefined)
357
+ }
358
+ const instance = new target(...deps)
359
+ if (scope === 'singleton') this.#singletons.set(key, instance)
360
+ return instance
361
+ }
362
+ // (b) No @Inject/metadata, but the caller supplied runtime values — build
363
+ // from those (the `make(Controller, [req, res])` pattern with a plain
364
+ // constructor). Slots beyond the runtime values stay undefined.
365
+ if (target.length > 0 && (runtimeValues?.length ?? 0) > 0) {
366
+ const deps = Array.from({ length: target.length }, (_value, index) => runtimeAt(index))
301
367
  const instance = new target(...deps)
302
368
  if (scope === 'singleton') this.#singletons.set(key, instance)
303
369
  return instance
304
370
  }
305
- // (b) The constructor declares parameters but we have no way to resolve
371
+ // (c) The constructor declares parameters but we have no way to resolve
306
372
  // them (no metadata, no @Inject, no containerInjections). Fail LOUDLY
307
373
  // instead of `new target()` with undefined deps.
308
374
  if (target.length > 0) {
@@ -314,29 +380,29 @@ export class Container {
314
380
  },
315
381
  )
316
382
  }
317
- // (c) Genuine zero-argument class.
383
+ // (d) Genuine zero-argument class.
318
384
  const instance = new target()
319
385
  if (scope === 'singleton') this.#singletons.set(key, instance)
320
386
  return instance
321
387
  }
322
388
 
323
- const deps = paramTypes.map((type, index) => {
389
+ // Resolve constructor deps SEQUENTIALLY (the cycle-detection stack is
390
+ // instance-shared, so parallel Promise.all would interleave and corrupt it).
391
+ const deps: unknown[] = []
392
+ for (let index = 0; index < paramTypes.length; index += 1) {
393
+ const rt = runtimeAt(index)
394
+ if (rt !== undefined) {
395
+ deps.push(rt)
396
+ continue
397
+ }
324
398
  const namedToken = injectTokens.get(index)
325
- // After the typeof check, type is a constructor function and valid as ServiceToken
399
+ const type = paramTypes[index]
400
+ // A native primitive constructor (String/Number/Boolean/…) is NOT
401
+ // injectable — leave the slot undefined so a constructor default can fill it.
326
402
  const depToken: ServiceToken | undefined =
327
- namedToken ??
328
- // biome-ignore lint/suspicious/noExplicitAny: reflect-metadata returns unknown; constructor functions are valid ServiceTokens
329
- (typeof type === 'function' && type !== Object ? (type as any as ServiceToken) : undefined)
330
-
331
- if (!depToken) return undefined
332
-
333
- if (lazyIndices.includes(index)) {
334
- // biome-ignore lint/suspicious/noExplicitAny: lazy proxy target must be object; IoC resolution returns unknown
335
- return createLazyProxy(() => this.resolve(depToken) as any as object)
336
- }
337
-
338
- return this.resolve(depToken)
339
- })
403
+ namedToken ?? (isInjectableClass(type) ? type : undefined)
404
+ deps.push(depToken ? await this.resolve(depToken) : undefined)
405
+ }
340
406
 
341
407
  const instance = new target(...deps)
342
408
  if (scope === 'singleton') this.#singletons.set(key, instance)
@@ -378,11 +444,29 @@ export class Container {
378
444
  }
379
445
  }
380
446
 
381
- // `design:paramtypes` returns `unknown[]` per our annotation; we narrow each
382
- // entry before treating it as a class token. `Object` slips through because
383
- // emitDecoratorMetadata uses it for `any`/`unknown` parameters.
384
- function isClassConstructor(value: unknown): value is new (...args: unknown[]) => unknown {
385
- return typeof value === 'function' && value !== Object
447
+ /**
448
+ * Native constructors emitDecoratorMetadata uses for primitive / `any` /
449
+ * `unknown` parameters. They are NOT injectable — resolving them would try to
450
+ * auto-construct `String`/`Number`/… (or silently inject `undefined`), so we
451
+ * skip them and let a constructor default fill the slot instead.
452
+ */
453
+ const NON_INJECTABLE_CONSTRUCTORS = new Set<unknown>([
454
+ Object,
455
+ String,
456
+ Number,
457
+ Boolean,
458
+ Array,
459
+ Function,
460
+ Symbol,
461
+ Date,
462
+ RegExp,
463
+ Promise,
464
+ Error,
465
+ ])
466
+
467
+ /** True for a real, injectable class token (excludes native primitive constructors). */
468
+ function isInjectableClass(value: unknown): value is new (...args: unknown[]) => unknown {
469
+ return typeof value === 'function' && !NON_INJECTABLE_CONSTRUCTORS.has(value)
386
470
  }
387
471
 
388
472
  function isCallable(value: unknown): value is (...args: unknown[]) => unknown {
package/src/env/Env.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import {
2
+ defineIdentifier,
3
+ defineIdentifierIfMissing,
4
+ type IdentifierResolver,
5
+ removeIdentifier,
6
+ } from './interpolate.js'
1
7
  import { loadEnvFiles } from './loadEnvFiles.js'
2
8
  import { type SchemaNode, schema } from './schema.js'
3
9
 
@@ -45,6 +51,21 @@ export class Env<Values extends Record<string, unknown>> {
45
51
  /** The validation schema surface — `Env.schema.string()`, `.number()`, … */
46
52
  static readonly schema = schema
47
53
 
54
+ /** Register an interpolation identifier (AdonisJS `Env.defineIdentifier`). */
55
+ static defineIdentifier(name: string, resolver: IdentifierResolver): void {
56
+ defineIdentifier(name, resolver)
57
+ }
58
+
59
+ /** Register an identifier only if the name is free (AdonisJS `defineIdentifierIfMissing`). */
60
+ static defineIdentifierIfMissing(name: string, resolver: IdentifierResolver): void {
61
+ defineIdentifierIfMissing(name, resolver)
62
+ }
63
+
64
+ /** Remove a registered interpolation identifier (AdonisJS `Env.removeIdentifier`). */
65
+ static removeIdentifier(name: string): void {
66
+ removeIdentifier(name)
67
+ }
68
+
48
69
  readonly #values: Values
49
70
 
50
71
  private constructor(values: Values) {
@@ -80,12 +101,35 @@ export class Env<Values extends Record<string, unknown>> {
80
101
  return new Env(collected as EnvRecord<S>)
81
102
  }
82
103
 
83
- /** Read a validated variable. Required keys are always present; optional keys may be `undefined`. */
104
+ /**
105
+ * Read a variable. Validated keys are typed; for any other key the raw
106
+ * `process.env` value is returned as a fallback (AdonisJS `env.get`
107
+ * semantics). Required keys are always present; optional keys may be
108
+ * `undefined`. The second argument is the default when nothing is found.
109
+ */
84
110
  get<K extends keyof Values>(key: K): Values[K]
85
111
  get<K extends keyof Values>(key: K, fallback: Values[K]): Values[K]
86
- get<K extends keyof Values>(key: K, fallback?: Values[K]): Values[K] {
87
- const value = this.#values[key]
88
- if (value === undefined && fallback !== undefined) return fallback
89
- return value
112
+ get(key: string): string | undefined
113
+ get(key: string, fallback: string): string
114
+ get(key: string, fallback?: unknown): unknown {
115
+ const cached = this.#values[key]
116
+ if (cached !== undefined) return cached
117
+ // Fallback to raw process.env for keys outside the validated schema.
118
+ const fromEnv = process.env[key]
119
+ if (fromEnv !== undefined && fromEnv !== '') return fromEnv
120
+ return fallback
121
+ }
122
+
123
+ /**
124
+ * Set/override a variable at runtime (AdonisJS `env.set`). Updates the cache
125
+ * AND `process.env`. The value is NOT re-validated — pass the correct type.
126
+ */
127
+ set<K extends keyof Values>(key: K, value: Values[K]): void
128
+ set(key: string, value: string): void
129
+ set(key: string, value: unknown): void {
130
+ // Reflect.set writes to the generic-typed cache without a cast (a direct
131
+ // `this.#values[key] = value` is rejected: a generic type is read-only-indexed).
132
+ Reflect.set(this.#values, key, value)
133
+ process.env[key] = String(value)
90
134
  }
91
135
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Secret — wraps a sensitive value so it never leaks through logs, JSON, or
3
+ * `util.inspect`. Mirrors `@poppinss/utils`'s `Secret` (zero-dep here). Read the
4
+ * underlying value explicitly with {@link release}.
5
+ *
6
+ * const key = env.get('APP_KEY') // Secret<string> when declared via schema.secret
7
+ * key.toString() // '[redacted]'
8
+ * JSON.stringify({ key }) // '{"key":"[redacted]"}'
9
+ * key.release() // the real value — only when you deliberately need it
10
+ */
11
+ const REDACTED = '[redacted]'
12
+
13
+ export class Secret<T> {
14
+ readonly #value: T
15
+ readonly #keyword: string
16
+
17
+ constructor(value: T, redactedKeyword: string = REDACTED) {
18
+ this.#value = value
19
+ this.#keyword = redactedKeyword
20
+ }
21
+
22
+ /** Redacted placeholder (so `JSON.stringify` never emits the secret). */
23
+ toJSON(): string {
24
+ return this.#keyword
25
+ }
26
+
27
+ /** Redacted placeholder for string coercion + template literals. */
28
+ toString(): string {
29
+ return this.#keyword
30
+ }
31
+
32
+ /** Redacted placeholder for `console.log` / `util.inspect`. */
33
+ [Symbol.for('nodejs.util.inspect.custom')](): string {
34
+ return this.#keyword
35
+ }
36
+
37
+ /** Release the underlying value — the one deliberate way to read the secret. */
38
+ release(): T {
39
+ return this.#value
40
+ }
41
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Env value interpolation — AdonisJS-style variable substitution + identifiers,
3
+ * applied by {@link loadEnvFiles} as each `.env` value is loaded.
4
+ *
5
+ * - `$VAR` / `${VAR}` are replaced with the value of `VAR` (looked up against
6
+ * the values already loaded + `process.env`). A missing reference expands to
7
+ * an empty string. `\$` is a literal dollar sign (escape).
8
+ * - `identifier:rest` — when `identifier` was registered via
9
+ * {@link defineIdentifier}, the registered callback transforms `rest`
10
+ * (e.g. a `base64:` decoder). Unregistered prefixes (like `postgres://…`)
11
+ * are left untouched.
12
+ */
13
+
14
+ /** Transform the remainder of an `identifier:rest` value into its final form. */
15
+ export type IdentifierResolver = (value: string) => string
16
+
17
+ const identifiers = new Map<string, IdentifierResolver>()
18
+
19
+ /** Register an identifier resolver (AdonisJS `Env.defineIdentifier`). */
20
+ export function defineIdentifier(name: string, resolver: IdentifierResolver): void {
21
+ identifiers.set(name, resolver)
22
+ }
23
+
24
+ /** Register an identifier resolver only if the name is free (AdonisJS `defineIdentifierIfMissing`). */
25
+ export function defineIdentifierIfMissing(name: string, resolver: IdentifierResolver): void {
26
+ if (!identifiers.has(name)) identifiers.set(name, resolver)
27
+ }
28
+
29
+ /** Remove a registered identifier (AdonisJS `Env.removeIdentifier`). */
30
+ export function removeIdentifier(name: string): void {
31
+ identifiers.delete(name)
32
+ }
33
+
34
+ const REFERENCE = /(\\)?\$(?:\{([^}]+)\}|([A-Za-z_][A-Za-z0-9_]*))/g
35
+
36
+ /**
37
+ * Expand `$VAR` / `${VAR}` references and apply an identifier prefix. `lookup`
38
+ * resolves a referenced variable name to its current value (or undefined).
39
+ */
40
+ export function interpolate(value: string, lookup: (name: string) => string | undefined): string {
41
+ const substituted = value.replace(REFERENCE, (full, escaped, braced, bare) => {
42
+ // `\$FOO` → literal `$FOO` (drop only the escaping backslash).
43
+ if (escaped) return full.slice(1)
44
+ const name = braced ?? bare
45
+ return lookup(name) ?? ''
46
+ })
47
+
48
+ const prefixed = /^([A-Za-z_][A-Za-z0-9_]*):([\s\S]*)$/.exec(substituted)
49
+ if (prefixed) {
50
+ const resolver = identifiers.get(prefixed[1])
51
+ if (resolver) return resolver(prefixed[2])
52
+ }
53
+ return substituted
54
+ }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { parseEnv } from 'node:util'
4
+ import { interpolate } from './interpolate.js'
4
5
 
5
6
  /**
6
7
  * Load `.env` files into `process.env` — the shared primitive behind both the
@@ -31,10 +32,17 @@ export function loadEnvFiles(appRoot: URL, options: { skipEnvLocal?: boolean } =
31
32
  } catch {
32
33
  continue // file absent — nothing to load
33
34
  }
34
- for (const [key, value] of Object.entries(parseEnv(contents))) {
35
- if (typeof value === 'string' && process.env[key] === undefined) {
36
- process.env[key] = value
37
- }
35
+ const parsed = parseEnv(contents)
36
+ for (const [key, value] of Object.entries(parsed)) {
37
+ if (typeof value !== 'string' || process.env[key] !== undefined) continue
38
+ // Interpolate `$VAR`/`${VAR}` against process.env then this file's own
39
+ // (already-loaded) values, and apply any `identifier:` resolver.
40
+ process.env[key] = interpolate(value, (name) => {
41
+ const fromProcess = process.env[name]
42
+ if (fromProcess !== undefined) return fromProcess
43
+ const fromFile = parsed[name]
44
+ return typeof fromFile === 'string' ? fromFile : undefined
45
+ })
38
46
  }
39
47
  }
40
48
  }