@c9up/ream 0.1.25 → 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.
- package/dist/ConfigLoader.d.ts +46 -5
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +60 -9
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +46 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +21 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/container/Container.d.ts +23 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +139 -60
- package/dist/container/Container.js.map +1 -1
- package/dist/env/Env.d.ts +29 -4
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +27 -4
- package/dist/env/Env.js.map +1 -1
- package/dist/env/Secret.d.ts +11 -0
- package/dist/env/Secret.d.ts.map +1 -0
- package/dist/env/Secret.js +36 -0
- package/dist/env/Secret.js.map +1 -0
- package/dist/env/interpolate.d.ts +26 -0
- package/dist/env/interpolate.d.ts.map +1 -0
- package/dist/env/interpolate.js +48 -0
- package/dist/env/interpolate.js.map +1 -0
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +14 -4
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/env/schema.d.ts +23 -6
- package/dist/env/schema.d.ts.map +1 -1
- package/dist/env/schema.js +67 -33
- package/dist/env/schema.js.map +1 -1
- package/dist/events/Emitter.d.ts +1 -1
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +1 -1
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsProvider.d.ts +2 -2
- package/dist/events/EventsProvider.d.ts.map +1 -1
- package/dist/events/EventsProvider.js +3 -3
- package/dist/events/EventsProvider.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +2 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/graphql/GraphQLProvider.d.ts.map +1 -1
- package/dist/graphql/GraphQLProvider.js +7 -3
- package/dist/graphql/GraphQLProvider.js.map +1 -1
- package/dist/http/Exception.d.ts +63 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +185 -48
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +53 -7
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +56 -15
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +4 -1
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +141 -10
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +244 -34
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +176 -9
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +404 -16
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.js +1 -1
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/openapi/OpenApiProvider.js +4 -4
- package/dist/openapi/OpenApiProvider.js.map +1 -1
- package/dist/router/Router.d.ts +223 -31
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +520 -102
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +1 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +7 -3
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +2 -2
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +5 -4
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +7 -15
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/server/Server.d.ts +17 -4
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +41 -7
- package/dist/server/Server.js.map +1 -1
- package/dist/session/SessionMiddleware.js +3 -3
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/storage/StaticProvider.js +1 -1
- package/dist/storage/StaticProvider.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +60 -0
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +180 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +22 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +40 -0
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +30 -0
- package/dist/utils/Macroable.d.ts.map +1 -0
- package/dist/utils/Macroable.js +43 -0
- package/dist/utils/Macroable.js.map +1 -0
- package/dist/utils/inflect.d.ts +17 -0
- package/dist/utils/inflect.d.ts.map +1 -0
- package/dist/utils/inflect.js +32 -0
- package/dist/utils/inflect.js.map +1 -0
- package/dist/utils/objectPath.d.ts +57 -0
- package/dist/utils/objectPath.d.ts.map +1 -0
- package/dist/utils/objectPath.js +141 -0
- package/dist/utils/objectPath.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +6 -1
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/ConfigLoader.ts +72 -9
- package/src/HttpKernel.ts +53 -15
- package/src/Ignitor.ts +24 -3
- package/src/container/Container.ts +145 -61
- package/src/env/Env.ts +49 -5
- package/src/env/Secret.ts +41 -0
- package/src/env/interpolate.ts +54 -0
- package/src/env/loadEnvFiles.ts +12 -4
- package/src/env/schema.ts +109 -35
- package/src/events/Emitter.ts +2 -2
- package/src/events/EventsProvider.ts +5 -5
- package/src/graphql/GraphQLEngine.ts +4 -4
- package/src/graphql/GraphQLProvider.ts +7 -3
- package/src/http/Exception.ts +214 -37
- package/src/http/HttpContext.ts +87 -18
- package/src/http/RedirectBuilder.ts +4 -1
- package/src/http/Request.ts +264 -34
- package/src/http/Response.ts +478 -27
- package/src/index.ts +11 -2
- package/src/middleware/Pipeline.ts +1 -1
- package/src/openapi/OpenApiProvider.ts +4 -4
- package/src/router/Router.ts +631 -113
- package/src/rpc/RpcProvider.ts +6 -2
- package/src/rpc/RpcRouter.ts +7 -6
- package/src/scheduler/ScheduleProvider.ts +7 -15
- package/src/server/Server.ts +49 -10
- package/src/session/SessionMiddleware.ts +4 -4
- package/src/storage/StaticProvider.ts +1 -1
- package/src/testing/RequestBuilder.ts +218 -1
- package/src/testing/TestClient.ts +57 -1
- package/src/utils/Macroable.ts +54 -0
- package/src/utils/inflect.ts +29 -0
- package/src/utils/objectPath.ts +142 -0
- package/dist/decorators/Lazy.d.ts +0 -19
- package/dist/decorators/Lazy.d.ts.map +0 -1
- package/dist/decorators/Lazy.js +0 -53
- package/dist/decorators/Lazy.js.map +0 -1
- 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
|
-
/**
|
|
123
|
-
|
|
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: '
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
216
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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(
|
|
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).
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
// (
|
|
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
|
-
// (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
/**
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
package/src/env/loadEnvFiles.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
}
|