@c9up/ream 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- 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 +115 -7
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +254 -40
- package/dist/Ignitor.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +217 -0
- package/dist/console/BaseCommand.d.ts.map +1 -0
- package/dist/console/BaseCommand.js +340 -0
- package/dist/console/BaseCommand.js.map +1 -0
- package/dist/console/Console.d.ts +86 -0
- package/dist/console/Console.d.ts.map +1 -0
- package/dist/console/Console.js +134 -0
- package/dist/console/Console.js.map +1 -0
- package/dist/console/ExceptionHandler.d.ts +28 -0
- package/dist/console/ExceptionHandler.d.ts.map +1 -0
- package/dist/console/ExceptionHandler.js +76 -0
- package/dist/console/ExceptionHandler.js.map +1 -0
- package/dist/console/HelpCommand.d.ts +16 -0
- package/dist/console/HelpCommand.d.ts.map +1 -0
- package/dist/console/HelpCommand.js +40 -0
- package/dist/console/HelpCommand.js.map +1 -0
- package/dist/console/IndexGenerator.d.ts +24 -0
- package/dist/console/IndexGenerator.d.ts.map +1 -0
- package/dist/console/IndexGenerator.js +65 -0
- package/dist/console/IndexGenerator.js.map +1 -0
- package/dist/console/Kernel.d.ts +298 -0
- package/dist/console/Kernel.d.ts.map +1 -0
- package/dist/console/Kernel.js +1120 -0
- package/dist/console/Kernel.js.map +1 -0
- package/dist/console/ListCommand.d.ts +18 -0
- package/dist/console/ListCommand.d.ts.map +1 -0
- package/dist/console/ListCommand.js +60 -0
- package/dist/console/ListCommand.js.map +1 -0
- package/dist/console/assertions.d.ts +20 -0
- package/dist/console/assertions.d.ts.map +1 -0
- package/dist/console/assertions.js +84 -0
- package/dist/console/assertions.js.map +1 -0
- package/dist/console/cliui.d.ts +248 -0
- package/dist/console/cliui.d.ts.map +1 -0
- package/dist/console/cliui.js +587 -0
- package/dist/console/cliui.js.map +1 -0
- package/dist/console/decorators.d.ts +90 -0
- package/dist/console/decorators.d.ts.map +1 -0
- package/dist/console/decorators.js +142 -0
- package/dist/console/decorators.js.map +1 -0
- package/dist/console/index.d.ts +32 -0
- package/dist/console/index.d.ts.map +1 -0
- package/dist/console/index.js +24 -0
- package/dist/console/index.js.map +1 -0
- package/dist/console/loaders.d.ts +52 -0
- package/dist/console/loaders.d.ts.map +1 -0
- package/dist/console/loaders.js +131 -0
- package/dist/console/loaders.js.map +1 -0
- package/dist/console/parser.d.ts +86 -0
- package/dist/console/parser.d.ts.map +1 -0
- package/dist/console/parser.js +440 -0
- package/dist/console/parser.js.map +1 -0
- package/dist/console/prompts.d.ts +124 -0
- package/dist/console/prompts.d.ts.map +1 -0
- package/dist/console/prompts.js +356 -0
- package/dist/console/prompts.js.map +1 -0
- package/dist/console/tracing.d.ts +17 -0
- package/dist/console/tracing.d.ts.map +1 -0
- package/dist/console/tracing.js +11 -0
- package/dist/console/tracing.js.map +1 -0
- package/dist/console/types.d.ts +255 -0
- package/dist/console/types.d.ts.map +1 -0
- package/dist/console/types.js +25 -0
- package/dist/console/types.js.map +1 -0
- package/dist/console/ui.d.ts +20 -0
- package/dist/console/ui.d.ts.map +1 -0
- package/dist/console/ui.js +31 -0
- package/dist/console/ui.js.map +1 -0
- package/dist/console/utils.d.ts +27 -0
- package/dist/console/utils.d.ts.map +1 -0
- package/dist/console/utils.js +56 -0
- package/dist/console/utils.js.map +1 -0
- package/dist/container/Container.d.ts +23 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +177 -70
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +14 -2
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +7 -0
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/decorators/Service.js +23 -3
- package/dist/decorators/Service.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/errors/ReamError.d.ts +25 -0
- package/dist/errors/ReamError.d.ts.map +1 -1
- package/dist/errors/ReamError.js +5 -0
- package/dist/errors/ReamError.js.map +1 -1
- package/dist/errors/prettyPrintError.d.ts +9 -0
- package/dist/errors/prettyPrintError.d.ts.map +1 -0
- package/dist/errors/prettyPrintError.js +22 -0
- package/dist/errors/prettyPrintError.js.map +1 -0
- 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 +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +29 -8
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +40 -2
- 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 +17 -5
- 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/services/app.d.ts +1 -1
- package/dist/services/app.js +1 -1
- package/dist/services/console.d.ts +21 -0
- package/dist/services/console.d.ts.map +1 -0
- package/dist/services/console.js +35 -0
- package/dist/services/console.js.map +1 -0
- 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/ApiResponse.d.ts +218 -0
- package/dist/testing/ApiResponse.d.ts.map +1 -0
- package/dist/testing/ApiResponse.js +576 -0
- package/dist/testing/ApiResponse.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +155 -35
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +455 -109
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +53 -37
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +84 -52
- 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 +20 -2
- 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 +364 -45
- package/src/console/BaseCommand.ts +467 -0
- package/src/console/Console.ts +174 -0
- package/src/console/ExceptionHandler.ts +91 -0
- package/src/console/HelpCommand.ts +37 -0
- package/src/console/IndexGenerator.ts +79 -0
- package/src/console/Kernel.ts +1382 -0
- package/src/console/ListCommand.ts +55 -0
- package/src/console/assertions.ts +114 -0
- package/src/console/cliui.ts +756 -0
- package/src/console/decorators.ts +199 -0
- package/src/console/index.ts +67 -0
- package/src/console/loaders.ts +145 -0
- package/src/console/parser.ts +598 -0
- package/src/console/prompts.ts +548 -0
- package/src/console/tracing.ts +22 -0
- package/src/console/types.ts +280 -0
- package/src/console/ui.ts +37 -0
- package/src/console/utils.ts +69 -0
- package/src/container/Container.ts +198 -72
- package/src/container/types.ts +15 -2
- package/src/decorators/Service.ts +28 -3
- 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/errors/ReamError.ts +31 -0
- package/src/errors/prettyPrintError.ts +20 -0
- 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 +41 -6
- package/src/middleware/Pipeline.ts +77 -6
- 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 +22 -7
- package/src/scheduler/ScheduleProvider.ts +7 -15
- package/src/server/Server.ts +49 -10
- package/src/services/app.ts +1 -1
- package/src/services/console.ts +42 -0
- package/src/session/SessionMiddleware.ts +4 -4
- package/src/storage/StaticProvider.ts +1 -1
- package/src/testing/ApiResponse.ts +691 -0
- package/src/testing/RequestBuilder.ts +577 -137
- package/src/testing/TestClient.ts +110 -77
- package/src/utils/Macroable.ts +54 -0
- package/src/utils/inflect.ts +29 -0
- package/src/utils/objectPath.ts +142 -0
- package/dist/console/CommandRunner.d.ts +0 -18
- package/dist/console/CommandRunner.d.ts.map +0 -1
- package/dist/console/CommandRunner.js +0 -65
- package/dist/console/CommandRunner.js.map +0 -1
- 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/console/CommandRunner.ts +0 -82
- package/src/decorators/Lazy.ts +0 -57
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import 'reflect-metadata'
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
14
|
+
import {
|
|
15
|
+
getInjectTokens,
|
|
16
|
+
getMethodInjectTokens,
|
|
17
|
+
getServiceMetadata,
|
|
18
|
+
getServiceRegistry,
|
|
19
|
+
} from '../decorators/Service.js'
|
|
15
20
|
import { didYouMean } from '../errors/FuzzyMatcher.js'
|
|
16
21
|
import { ReamError } from '../errors/ReamError.js'
|
|
17
22
|
import type { Binding, ServiceFactory, ServiceToken } from './types.js'
|
|
@@ -23,6 +28,12 @@ import type { Binding, ServiceFactory, ServiceToken } from './types.js'
|
|
|
23
28
|
*/
|
|
24
29
|
const RESERVED_TOKEN_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
|
|
25
30
|
|
|
31
|
+
/** One resolution chain: the tokens on it, and a set for O(1) membership. */
|
|
32
|
+
interface ResolutionChain {
|
|
33
|
+
stack: string[]
|
|
34
|
+
set: Set<string>
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export class Container {
|
|
27
38
|
#bindings: Map<string, Binding> = new Map()
|
|
28
39
|
#singletons: Map<string, unknown> = new Map()
|
|
@@ -34,8 +45,20 @@ export class Container {
|
|
|
34
45
|
* pointing at the same object after the test ends.
|
|
35
46
|
*/
|
|
36
47
|
#singletonBackup: Map<string, unknown> = new Map()
|
|
37
|
-
|
|
38
|
-
#
|
|
48
|
+
/** Alias key → the token it forwards to (AdonisJS `container.alias`). */
|
|
49
|
+
#aliases: Map<string, ServiceToken> = new Map()
|
|
50
|
+
/** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
|
|
51
|
+
#resolvingHooks: Map<string, Array<(value: unknown) => void | Promise<void>>> = new Map()
|
|
52
|
+
/**
|
|
53
|
+
* The resolution chain currently being walked, for cycle detection.
|
|
54
|
+
*
|
|
55
|
+
* Scoped to the async context, NOT to the container: `resolve()` is async, so
|
|
56
|
+
* two independent chains interleave, and a stack held on the instance would
|
|
57
|
+
* splice them together and report a cycle neither of them has. Per-container
|
|
58
|
+
* storage keeps a chain that crosses two containers from colliding on a token
|
|
59
|
+
* name they happen to share.
|
|
60
|
+
*/
|
|
61
|
+
readonly #chain = new AsyncLocalStorage<ResolutionChain>()
|
|
39
62
|
|
|
40
63
|
// ─── Explicit bindings ────────────────────────────────────
|
|
41
64
|
|
|
@@ -58,6 +81,33 @@ export class Container {
|
|
|
58
81
|
this.#bindings.set(key, { token, factory: () => value, scope: 'singleton', dependencies: [] })
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Register an alias — resolving `alias` forwards to `target` (AdonisJS
|
|
86
|
+
* `container.alias('db', Database)`). The target may be a class, string, or
|
|
87
|
+
* symbol that itself resolves to a binding or an auto-constructable class.
|
|
88
|
+
*/
|
|
89
|
+
alias(alias: ServiceToken, target: ServiceToken): void {
|
|
90
|
+
this.#aliases.set(this.#tokenToKey(alias), target)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register a callback that runs each time `token` is constructed (AdonisJS
|
|
95
|
+
* `container.resolving`) — for lazy init (`db.connect()`) or decorating the
|
|
96
|
+
* resolved instance. Runs after construction of a binding/auto-constructed
|
|
97
|
+
* class; NOT for raw `bindValue` values or an already-cached singleton.
|
|
98
|
+
*/
|
|
99
|
+
resolving(token: ServiceToken, callback: (value: unknown) => void | Promise<void>): void {
|
|
100
|
+
const key = this.#tokenToKey(token)
|
|
101
|
+
const hooks = this.#resolvingHooks.get(key)
|
|
102
|
+
if (hooks) hooks.push(callback)
|
|
103
|
+
else this.#resolvingHooks.set(key, [callback])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #runResolvingHooks(key: string, value: unknown): Promise<void> {
|
|
107
|
+
const hooks = this.#resolvingHooks.get(key)
|
|
108
|
+
if (hooks) for (const hook of hooks) await hook(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
61
111
|
// ─── Testing ──────────────────────────────────────────────
|
|
62
112
|
|
|
63
113
|
/**
|
|
@@ -115,29 +165,47 @@ export class Container {
|
|
|
115
165
|
* 4. Auto-construct if class has @inject() or @Service()
|
|
116
166
|
* 5. Auto-construct any class (plain `new Class()`) as fallback
|
|
117
167
|
*/
|
|
118
|
-
make<T>(token: ServiceToken): T {
|
|
119
|
-
return this.resolve<T>(token)
|
|
168
|
+
make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
|
|
169
|
+
return this.resolve<T>(token, runtimeValues)
|
|
120
170
|
}
|
|
121
171
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Resolve/construct a class or binding (AdonisJS `container.make` — async, so
|
|
174
|
+
* factories, providers and `resolving` hooks may be async). `runtimeValues`
|
|
175
|
+
* fill the resolved class's constructor slots by index (`make(Class, [req,
|
|
176
|
+
* res])`); only the `undefined` slots are container-resolved, top-level only.
|
|
177
|
+
*/
|
|
178
|
+
async resolve<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
|
|
179
|
+
// Dereference an alias to its target before anything else.
|
|
180
|
+
const aliasTarget = this.#aliases.get(this.#tokenToKey(token))
|
|
181
|
+
if (aliasTarget !== undefined) return this.resolve<T>(aliasTarget, runtimeValues)
|
|
182
|
+
|
|
124
183
|
const key = this.#tokenToKey(token)
|
|
125
184
|
|
|
126
|
-
|
|
127
|
-
|
|
185
|
+
// Top of a chain: open one, so everything this resolution triggers shares
|
|
186
|
+
// it and nothing outside it does.
|
|
187
|
+
const chain = this.#chain.getStore()
|
|
188
|
+
if (chain === undefined) {
|
|
189
|
+
return this.#chain.run({ stack: [], set: new Set() }, () =>
|
|
190
|
+
this.resolve<T>(token, runtimeValues),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (chain.set.has(key)) {
|
|
195
|
+
const cycle = [...chain.stack, key].join(' → ')
|
|
128
196
|
throw new ReamError('CIRCULAR_DEPENDENCY', `Circular dependency detected: ${cycle}`, {
|
|
129
|
-
hint: '
|
|
197
|
+
hint: 'Break the cycle by resolving one dependency lazily inside a method (`await container.make(Dep)`) instead of injecting it in the constructor.',
|
|
130
198
|
context: { chain: cycle },
|
|
131
199
|
})
|
|
132
200
|
}
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
chain.stack.push(key)
|
|
202
|
+
chain.set.add(key)
|
|
135
203
|
|
|
136
204
|
try {
|
|
137
|
-
return this.#resolveInner<T>(key, token)
|
|
205
|
+
return await this.#resolveInner<T>(key, token, runtimeValues)
|
|
138
206
|
} finally {
|
|
139
|
-
|
|
140
|
-
|
|
207
|
+
chain.stack.pop()
|
|
208
|
+
chain.set.delete(key)
|
|
141
209
|
}
|
|
142
210
|
}
|
|
143
211
|
|
|
@@ -157,15 +225,32 @@ export class Container {
|
|
|
157
225
|
const target: Ctor = Object.getPrototypeOf(instance).constructor
|
|
158
226
|
const paramTypes: unknown[] =
|
|
159
227
|
Reflect.getMetadata('design:paramtypes', target.prototype, method) ?? []
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
// `@Inject('token')` on a method parameter — a named binding the reflected
|
|
229
|
+
// type cannot express (an interface, or a class registered under a name).
|
|
230
|
+
const injectTokens = getMethodInjectTokens(target.prototype, method)
|
|
231
|
+
|
|
232
|
+
// `@Inject` counts towards the parameter span too: a method may carry named
|
|
233
|
+
// tokens without `design:paramtypes` being emitted for it, and computing the
|
|
234
|
+
// span from the reflected types alone left the loop at zero iterations —
|
|
235
|
+
// the tokens were read and then never used.
|
|
236
|
+
const injectSpan = injectTokens.size === 0 ? 0 : Math.max(...injectTokens.keys()) + 1
|
|
237
|
+
const len = Math.max(paramTypes.length, runtimeValues?.length ?? 0, injectSpan)
|
|
238
|
+
// Sequential resolution keeps the shared cycle-detection stack consistent.
|
|
239
|
+
const args: unknown[] = []
|
|
240
|
+
for (let index = 0; index < len; index += 1) {
|
|
241
|
+
// Runtime values take precedence (and fill slots beyond paramTypes).
|
|
242
|
+
if (runtimeValues && index < runtimeValues.length) {
|
|
243
|
+
args.push(runtimeValues[index])
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
const named = injectTokens.get(index)
|
|
247
|
+
if (named !== undefined) {
|
|
248
|
+
args.push(await this.resolve(named))
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
165
251
|
const type = paramTypes[index]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
})
|
|
252
|
+
args.push(isInjectableClass(type) ? await this.resolve(type) : undefined)
|
|
253
|
+
}
|
|
169
254
|
|
|
170
255
|
const member: unknown = instance[method]
|
|
171
256
|
if (!isCallable(member)) {
|
|
@@ -183,10 +268,16 @@ export class Container {
|
|
|
183
268
|
this.#bindings.has(key) ||
|
|
184
269
|
this.#overrides.has(key) ||
|
|
185
270
|
this.#singletons.has(key) ||
|
|
271
|
+
this.#aliases.has(key) ||
|
|
186
272
|
(typeof token === 'function' && getServiceMetadata(token) !== undefined)
|
|
187
273
|
)
|
|
188
274
|
}
|
|
189
275
|
|
|
276
|
+
/** True only when EVERY token is registered/resolvable (AdonisJS `hasAllBindings`). */
|
|
277
|
+
hasAllBindings(tokens: ServiceToken[]): boolean {
|
|
278
|
+
return tokens.every((token) => this.has(token))
|
|
279
|
+
}
|
|
280
|
+
|
|
190
281
|
get size(): number {
|
|
191
282
|
return this.#bindings.size
|
|
192
283
|
}
|
|
@@ -209,34 +300,35 @@ export class Container {
|
|
|
209
300
|
|
|
210
301
|
// ─── Internal resolution ──────────────────────────────────
|
|
211
302
|
|
|
212
|
-
#resolveInner<T>(key: string, token: ServiceToken): T {
|
|
213
|
-
// 1. Check swaps (test overrides)
|
|
303
|
+
async #resolveInner<T>(key: string, token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
|
|
304
|
+
// 1. Check swaps (test overrides) — a swap factory may be async.
|
|
214
305
|
if (this.#overrides.has(key)) {
|
|
215
|
-
|
|
216
|
-
return
|
|
306
|
+
const swapped = await this.#overrides.get(key)?.(this)
|
|
307
|
+
return swapped as T
|
|
217
308
|
}
|
|
218
309
|
|
|
219
310
|
// 2. Check cached singletons
|
|
220
311
|
if (this.#singletons.has(key)) {
|
|
221
|
-
|
|
222
|
-
return this.#singletons.get(key) as any as T
|
|
312
|
+
return this.#singletons.get(key) as T
|
|
223
313
|
}
|
|
224
314
|
|
|
225
|
-
// 3. Check explicit bindings
|
|
315
|
+
// 3. Check explicit bindings — the factory may be async.
|
|
226
316
|
const binding = this.#bindings.get(key)
|
|
227
317
|
if (binding) {
|
|
228
|
-
const instance = binding.factory ? binding.factory() : undefined
|
|
318
|
+
const instance = binding.factory ? await binding.factory(this) : undefined
|
|
229
319
|
if (binding.scope === 'singleton') {
|
|
230
320
|
this.#singletons.set(key, instance)
|
|
231
321
|
}
|
|
232
|
-
|
|
233
|
-
|
|
322
|
+
await this.#runResolvingHooks(key, instance)
|
|
323
|
+
// Boundary cast from `unknown` — the caller brands the resolved type via T.
|
|
324
|
+
return instance as T
|
|
234
325
|
}
|
|
235
326
|
|
|
236
327
|
// 4. Auto-construct if it's a class
|
|
237
328
|
if (typeof token === 'function') {
|
|
238
|
-
|
|
239
|
-
|
|
329
|
+
const instance = (await this.#autoConstruct(token, runtimeValues)) as T
|
|
330
|
+
await this.#runResolvingHooks(key, instance)
|
|
331
|
+
return instance
|
|
240
332
|
}
|
|
241
333
|
|
|
242
334
|
// 5. Not found
|
|
@@ -259,7 +351,10 @@ export class Container {
|
|
|
259
351
|
* 2. Reflect.getMetadata('design:paramtypes') — decorator metadata (requires SWC/tsc)
|
|
260
352
|
* 3. No params → plain `new Class()`
|
|
261
353
|
*/
|
|
262
|
-
#autoConstruct(
|
|
354
|
+
async #autoConstruct(
|
|
355
|
+
target: new (...args: unknown[]) => unknown,
|
|
356
|
+
runtimeValues?: unknown[],
|
|
357
|
+
): Promise<unknown> {
|
|
263
358
|
const metadata = getServiceMetadata(target)
|
|
264
359
|
const scope = metadata?.scope ?? 'transient'
|
|
265
360
|
const key = metadata?.as ?? target.name
|
|
@@ -268,10 +363,12 @@ export class Container {
|
|
|
268
363
|
return this.#singletons.get(key)
|
|
269
364
|
}
|
|
270
365
|
|
|
366
|
+
// A runtime value at this index (from `make(Class, [req, res])`) wins over
|
|
367
|
+
// container resolution for that constructor slot.
|
|
368
|
+
const runtimeAt = (index: number): unknown => runtimeValues?.[index]
|
|
369
|
+
|
|
271
370
|
// 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.
|
|
371
|
+
// without emitDecoratorMetadata).
|
|
275
372
|
const explicitDeps = readContainerInjectionDeps(target)
|
|
276
373
|
|
|
277
374
|
// 2. Fallback to reflect-metadata
|
|
@@ -279,7 +376,6 @@ export class Container {
|
|
|
279
376
|
explicitDeps ?? Reflect.getMetadata('design:paramtypes', target) ?? []
|
|
280
377
|
|
|
281
378
|
const injectTokens = getInjectTokens(target)
|
|
282
|
-
const lazyIndices = getLazyParams(target)
|
|
283
379
|
|
|
284
380
|
// No `design:paramtypes` (a dev transpiler may not emit decorator metadata —
|
|
285
381
|
// esbuild / tsx don't). Recover what we can, and NEVER construct with
|
|
@@ -287,22 +383,34 @@ export class Container {
|
|
|
287
383
|
if (paramTypes.length === 0) {
|
|
288
384
|
// (a) `@Inject(token)` records its tokens INDEPENDENTLY of decorator
|
|
289
385
|
// metadata, so even without `design:paramtypes` we can resolve the
|
|
290
|
-
// constructor from that map alone.
|
|
386
|
+
// constructor from that map alone. Deps resolve SEQUENTIALLY so the
|
|
387
|
+
// shared cycle-detection stack stays consistent.
|
|
291
388
|
if (injectTokens.size > 0) {
|
|
292
|
-
const maxIndex = Math.max(...injectTokens.keys())
|
|
293
|
-
const deps
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
389
|
+
const maxIndex = Math.max(...injectTokens.keys(), (runtimeValues?.length ?? 0) - 1)
|
|
390
|
+
const deps: unknown[] = []
|
|
391
|
+
for (let index = 0; index <= maxIndex; index += 1) {
|
|
392
|
+
const rt = runtimeAt(index)
|
|
393
|
+
if (rt !== undefined) {
|
|
394
|
+
deps.push(rt)
|
|
395
|
+
continue
|
|
298
396
|
}
|
|
299
|
-
|
|
300
|
-
|
|
397
|
+
const depToken = injectTokens.get(index)
|
|
398
|
+
deps.push(depToken ? await this.resolve(depToken) : undefined)
|
|
399
|
+
}
|
|
301
400
|
const instance = new target(...deps)
|
|
302
401
|
if (scope === 'singleton') this.#singletons.set(key, instance)
|
|
303
402
|
return instance
|
|
304
403
|
}
|
|
305
|
-
// (b)
|
|
404
|
+
// (b) No @Inject/metadata, but the caller supplied runtime values — build
|
|
405
|
+
// from those (the `make(Controller, [req, res])` pattern with a plain
|
|
406
|
+
// constructor). Slots beyond the runtime values stay undefined.
|
|
407
|
+
if (target.length > 0 && (runtimeValues?.length ?? 0) > 0) {
|
|
408
|
+
const deps = Array.from({ length: target.length }, (_value, index) => runtimeAt(index))
|
|
409
|
+
const instance = new target(...deps)
|
|
410
|
+
if (scope === 'singleton') this.#singletons.set(key, instance)
|
|
411
|
+
return instance
|
|
412
|
+
}
|
|
413
|
+
// (c) The constructor declares parameters but we have no way to resolve
|
|
306
414
|
// them (no metadata, no @Inject, no containerInjections). Fail LOUDLY
|
|
307
415
|
// instead of `new target()` with undefined deps.
|
|
308
416
|
if (target.length > 0) {
|
|
@@ -314,29 +422,29 @@ export class Container {
|
|
|
314
422
|
},
|
|
315
423
|
)
|
|
316
424
|
}
|
|
317
|
-
// (
|
|
425
|
+
// (d) Genuine zero-argument class.
|
|
318
426
|
const instance = new target()
|
|
319
427
|
if (scope === 'singleton') this.#singletons.set(key, instance)
|
|
320
428
|
return instance
|
|
321
429
|
}
|
|
322
430
|
|
|
323
|
-
|
|
431
|
+
// Resolve constructor deps SEQUENTIALLY (the cycle-detection stack is
|
|
432
|
+
// instance-shared, so parallel Promise.all would interleave and corrupt it).
|
|
433
|
+
const deps: unknown[] = []
|
|
434
|
+
for (let index = 0; index < paramTypes.length; index += 1) {
|
|
435
|
+
const rt = runtimeAt(index)
|
|
436
|
+
if (rt !== undefined) {
|
|
437
|
+
deps.push(rt)
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
324
440
|
const namedToken = injectTokens.get(index)
|
|
325
|
-
|
|
441
|
+
const type = paramTypes[index]
|
|
442
|
+
// A native primitive constructor (String/Number/Boolean/…) is NOT
|
|
443
|
+
// injectable — leave the slot undefined so a constructor default can fill it.
|
|
326
444
|
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
|
-
})
|
|
445
|
+
namedToken ?? (isInjectableClass(type) ? type : undefined)
|
|
446
|
+
deps.push(depToken ? await this.resolve(depToken) : undefined)
|
|
447
|
+
}
|
|
340
448
|
|
|
341
449
|
const instance = new target(...deps)
|
|
342
450
|
if (scope === 'singleton') this.#singletons.set(key, instance)
|
|
@@ -378,11 +486,29 @@ export class Container {
|
|
|
378
486
|
}
|
|
379
487
|
}
|
|
380
488
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
489
|
+
/**
|
|
490
|
+
* Native constructors emitDecoratorMetadata uses for primitive / `any` /
|
|
491
|
+
* `unknown` parameters. They are NOT injectable — resolving them would try to
|
|
492
|
+
* auto-construct `String`/`Number`/… (or silently inject `undefined`), so we
|
|
493
|
+
* skip them and let a constructor default fill the slot instead.
|
|
494
|
+
*/
|
|
495
|
+
const NON_INJECTABLE_CONSTRUCTORS = new Set<unknown>([
|
|
496
|
+
Object,
|
|
497
|
+
String,
|
|
498
|
+
Number,
|
|
499
|
+
Boolean,
|
|
500
|
+
Array,
|
|
501
|
+
Function,
|
|
502
|
+
Symbol,
|
|
503
|
+
Date,
|
|
504
|
+
RegExp,
|
|
505
|
+
Promise,
|
|
506
|
+
Error,
|
|
507
|
+
])
|
|
508
|
+
|
|
509
|
+
/** True for a real, injectable class token (excludes native primitive constructors). */
|
|
510
|
+
function isInjectableClass(value: unknown): value is new (...args: unknown[]) => unknown {
|
|
511
|
+
return typeof value === 'function' && !NON_INJECTABLE_CONSTRUCTORS.has(value)
|
|
386
512
|
}
|
|
387
513
|
|
|
388
514
|
function isCallable(value: unknown): value is (...args: unknown[]) => unknown {
|
package/src/container/types.ts
CHANGED
|
@@ -17,8 +17,21 @@ export interface ServiceMetadata {
|
|
|
17
17
|
// biome-ignore lint/suspicious/noExplicitAny: IoC container tokens must accept any constructor signature
|
|
18
18
|
export type ServiceToken = (new (...args: any[]) => unknown) | string | symbol
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Resolver handed to a binding factory so it can resolve its own dependencies
|
|
22
|
+
* (AdonisJS parity: `container.singleton(token, async (resolver) =>
|
|
23
|
+
* await resolver.make(Dep))`). The Container itself satisfies this contract.
|
|
24
|
+
*/
|
|
25
|
+
export interface ContainerResolver {
|
|
26
|
+
make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Factory function for creating service instances. Receives a {@link
|
|
31
|
+
* ContainerResolver} (AdonisJS parity) — the async container means a factory
|
|
32
|
+
* that needs another binding must `await resolver.make(Dep)`.
|
|
33
|
+
*/
|
|
34
|
+
export type ServiceFactory = (resolver: ContainerResolver) => unknown
|
|
22
35
|
|
|
23
36
|
/** Binding entry in the container. */
|
|
24
37
|
export interface Binding {
|
|
@@ -51,11 +51,23 @@ export function Service(options: { scope?: ServiceScope; as?: string } = {}): Cl
|
|
|
51
51
|
* @implements FR16
|
|
52
52
|
*/
|
|
53
53
|
export function Inject(token: ServiceToken): ParameterDecorator {
|
|
54
|
-
return (target,
|
|
54
|
+
return (target, propertyKey, parameterIndex) => {
|
|
55
|
+
// A constructor parameter reports no property key and lands on the class;
|
|
56
|
+
// a method parameter lands on the prototype and MUST be keyed by method
|
|
57
|
+
// name, or every method of the class would share one map and overwrite
|
|
58
|
+
// each other's tokens.
|
|
55
59
|
const existingTokens: Map<number, ServiceToken> =
|
|
56
|
-
|
|
60
|
+
(propertyKey === undefined
|
|
61
|
+
? Reflect.getOwnMetadata(INJECT_METADATA_KEY, target)
|
|
62
|
+
: Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey)) ?? new Map()
|
|
63
|
+
|
|
57
64
|
existingTokens.set(parameterIndex, token)
|
|
58
|
-
|
|
65
|
+
|
|
66
|
+
if (propertyKey === undefined) {
|
|
67
|
+
Reflect.defineMetadata(INJECT_METADATA_KEY, existingTokens, target)
|
|
68
|
+
} else {
|
|
69
|
+
Reflect.defineMetadata(INJECT_METADATA_KEY, existingTokens, target, propertyKey)
|
|
70
|
+
}
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -66,6 +78,19 @@ export function getInjectTokens(target: AnyConstructor): Map<number, ServiceToke
|
|
|
66
78
|
return Reflect.getOwnMetadata(INJECT_METADATA_KEY, target) ?? new Map()
|
|
67
79
|
}
|
|
68
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Get named injection tokens for one method's parameters.
|
|
83
|
+
*
|
|
84
|
+
* `target` is the prototype carrying the method — that is where a parameter
|
|
85
|
+
* decorator on a method writes.
|
|
86
|
+
*/
|
|
87
|
+
export function getMethodInjectTokens(
|
|
88
|
+
target: object,
|
|
89
|
+
propertyKey: string | symbol,
|
|
90
|
+
): Map<number, ServiceToken> {
|
|
91
|
+
return Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) ?? new Map()
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
/**
|
|
70
95
|
* @inject() class decorator — marks a controller (or service) for transient IoC resolution.
|
|
71
96
|
*
|
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
|
+
}
|