@digital-alchemy/core 26.2.17 → 26.5.1
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/CLAUDE.md +302 -0
- package/README.md +19 -3
- package/dist/helpers/async.d.mts +37 -0
- package/dist/helpers/async.mjs +50 -15
- package/dist/helpers/async.mjs.map +1 -1
- package/dist/helpers/config-environment-loader.d.mts +39 -0
- package/dist/helpers/config-environment-loader.mjs +51 -11
- package/dist/helpers/config-environment-loader.mjs.map +1 -1
- package/dist/helpers/config-file-loader.d.mts +65 -0
- package/dist/helpers/config-file-loader.mjs +80 -4
- package/dist/helpers/config-file-loader.mjs.map +1 -1
- package/dist/helpers/config.d.mts +202 -5
- package/dist/helpers/config.mjs +60 -0
- package/dist/helpers/config.mjs.map +1 -1
- package/dist/helpers/context.d.mts +12 -1
- package/dist/helpers/cron.d.mts +154 -7
- package/dist/helpers/cron.mjs +47 -4
- package/dist/helpers/cron.mjs.map +1 -1
- package/dist/helpers/errors.d.mts +45 -0
- package/dist/helpers/errors.mjs +45 -0
- package/dist/helpers/errors.mjs.map +1 -1
- package/dist/helpers/events.d.mts +23 -0
- package/dist/helpers/events.mjs +23 -0
- package/dist/helpers/events.mjs.map +1 -1
- package/dist/helpers/extend.d.mts +50 -0
- package/dist/helpers/extend.mjs +63 -0
- package/dist/helpers/extend.mjs.map +1 -1
- package/dist/helpers/index.d.mts +9 -0
- package/dist/helpers/index.mjs +9 -0
- package/dist/helpers/index.mjs.map +1 -1
- package/dist/helpers/lifecycle.d.mts +102 -16
- package/dist/helpers/lifecycle.mjs +19 -1
- package/dist/helpers/lifecycle.mjs.map +1 -1
- package/dist/helpers/logger.d.mts +178 -17
- package/dist/helpers/logger.mjs +41 -1
- package/dist/helpers/logger.mjs.map +1 -1
- package/dist/helpers/module.d.mts +110 -0
- package/dist/helpers/module.mjs +55 -6
- package/dist/helpers/module.mjs.map +1 -1
- package/dist/helpers/service-runner.d.mts +27 -1
- package/dist/helpers/service-runner.mjs +27 -1
- package/dist/helpers/service-runner.mjs.map +1 -1
- package/dist/helpers/utilities.d.mts +123 -3
- package/dist/helpers/utilities.mjs +110 -3
- package/dist/helpers/utilities.mjs.map +1 -1
- package/dist/helpers/wiring.d.mts +385 -0
- package/dist/helpers/wiring.mjs +120 -0
- package/dist/helpers/wiring.mjs.map +1 -1
- package/dist/services/als.service.d.mts +10 -0
- package/dist/services/als.service.mjs +49 -0
- package/dist/services/als.service.mjs.map +1 -1
- package/dist/services/configuration.service.d.mts +22 -0
- package/dist/services/configuration.service.mjs +140 -12
- package/dist/services/configuration.service.mjs.map +1 -1
- package/dist/services/index.d.mts +8 -0
- package/dist/services/index.mjs +8 -0
- package/dist/services/index.mjs.map +1 -1
- package/dist/services/internal.service.d.mts +98 -19
- package/dist/services/internal.service.mjs +91 -9
- package/dist/services/internal.service.mjs.map +1 -1
- package/dist/services/is.service.d.mts +64 -4
- package/dist/services/is.service.mjs +67 -4
- package/dist/services/is.service.mjs.map +1 -1
- package/dist/services/lifecycle.service.d.mts +26 -0
- package/dist/services/lifecycle.service.mjs +67 -9
- package/dist/services/lifecycle.service.mjs.map +1 -1
- package/dist/services/logger.service.d.mts +27 -0
- package/dist/services/logger.service.mjs +133 -9
- package/dist/services/logger.service.mjs.map +1 -1
- package/dist/services/scheduler.service.d.mts +19 -0
- package/dist/services/scheduler.service.mjs +87 -4
- package/dist/services/scheduler.service.mjs.map +1 -1
- package/dist/services/wiring.service.d.mts +28 -0
- package/dist/services/wiring.service.mjs +152 -19
- package/dist/services/wiring.service.mjs.map +1 -1
- package/dist/testing/index.d.mts +4 -0
- package/dist/testing/index.mjs +4 -0
- package/dist/testing/index.mjs.map +1 -1
- package/dist/testing/mock-logger.d.mts +8 -0
- package/dist/testing/mock-logger.mjs +9 -0
- package/dist/testing/mock-logger.mjs.map +1 -1
- package/dist/testing/test-module.d.mts +107 -27
- package/dist/testing/test-module.mjs +58 -1
- package/dist/testing/test-module.mjs.map +1 -1
- package/package.json +33 -31
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# CLAUDE.md — @digital-alchemy/core
|
|
2
|
+
|
|
3
|
+
## 1. What core is
|
|
4
|
+
|
|
5
|
+
`@digital-alchemy/core` is a zero-magic dependency-injection framework for TypeScript. There is no reflection, no decorators, no class hierarchy. Every service is a plain function that receives its dependencies through a single destructured parameter — `TServiceParams`.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
export function MyService({ logger, lifecycle, config }: TServiceParams) {
|
|
9
|
+
lifecycle.onReady(() => {
|
|
10
|
+
logger.info("ready");
|
|
11
|
+
});
|
|
12
|
+
return { doThing };
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The framework wires up all declared services at bootstrap time, building a dependency graph from the `libraries` and `services` fields of each `CreateApplication` / `CreateLibrary` call. Once wired, every service receives its full `TServiceParams` synchronously (with async factories awaited).
|
|
17
|
+
|
|
18
|
+
This repo is the foundation every other `@digital-alchemy` library depends on. Changes to exports, lifecycle semantics, or `TServiceParams` shape ripple downstream into every consumer. Treat public API surface changes as breaking.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. Repo orientation
|
|
23
|
+
|
|
24
|
+
### `src/services/*.mts` — stateful service factories
|
|
25
|
+
|
|
26
|
+
| File | Owns / responsibility | Read by |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `als.service.mts` | `AsyncLocalStorage` wrapper; `enterWith`, `run`, `getStore`, `getLogData` | Logger (ALS log merging), any code needing per-request context |
|
|
29
|
+
| `configuration.service.mts` | Config state map, loader orchestration, proxy for `config.*`, `setConfig`, `mergeConfig`, `validateConfig`, `onUpdate` | `wiring.service.mts` at bootstrap; every service via `config` injection |
|
|
30
|
+
| `internal.service.mts` | `InternalDefinition` class (boot state, module maps, lifecycle ref); `InternalUtils` (object path get/set/del, `is`, `relativeDate`, `titleCase`); `safeExec` | All services — injected as `internal` |
|
|
31
|
+
| `is.service.mts` | `IsIt` class — type guards and small utilities (`array`, `boolean`, `empty`, `equal`, `function`, `object`, `string`, etc.); exports singleton `is` | Imported directly by `lifecycle.service.mts` and `wiring.service.mts` to break circular deps; everywhere else reached as `internal.utils.is` |
|
|
32
|
+
| `lifecycle.service.mts` | `CreateLifecycle` — per-bootstrap lifecycle event registry; priority-sorted callback execution across all seven stages | `wiring.service.mts` owns the lifecycle instance; services attach via `lifecycle.*` injection |
|
|
33
|
+
| `logger.service.mts` | `Logger` factory — chalk/stdout formatter, `context(ctx)` builder, level filtering, `addTarget`, `updateShouldLog`, ALS integration, `systemLogger` | Every service via `logger` injection; `internal.boilerplate.logger` |
|
|
34
|
+
| `scheduler.service.mts` | `Scheduler` factory — cron, interval, sliding, `setTimeout`, `setInterval`, `sleep`; registers stop callbacks for clean shutdown; returns a builder called with `(context: TContext)` | Every service via `scheduler` injection |
|
|
35
|
+
| `wiring.service.mts` | Bootstrap orchestration — `CreateApplication`, `wireService`, `CreateBoilerplate`, `teardown`, SIGINT/SIGTERM handling; owns `LIB_BOILERPLATE` | Entry point for all apps; do not call `wireService` directly |
|
|
36
|
+
| `index.mts` | Re-exports all service exports | Everything in `src/` imports from `../index.mts` |
|
|
37
|
+
|
|
38
|
+
### `src/helpers/*.mts` — pure logic, types, and utilities
|
|
39
|
+
|
|
40
|
+
**What distinguishes helpers from services:** helpers are side-effect-free modules containing types, utility functions, constants, and small classes. They do not receive `TServiceParams`. Services are factories that receive DI params and return an API object.
|
|
41
|
+
|
|
42
|
+
| File | Owns |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `async.mts` | `each`, `eachSeries`, `eachLimit` — async iteration helpers that replace inconsistent `async` library behavior |
|
|
45
|
+
| `config-environment-loader.mts` | `ConfigLoaderEnvironment` — reads env vars and CLI switches; search order is `MODULE__KEY` (double-underscore) → `MODULE_KEY` (single-underscore) → `KEY` |
|
|
46
|
+
| `config-file-loader.mts` | `configLoaderFile`, `configFilePaths`, `loadConfigFromFile`, `withExtensions` — file-based config loading (JSON, YAML, INI, auto-detect) |
|
|
47
|
+
| `config.mts` | All config types (`AnyConfig`, `StringConfig`, `BooleanConfig`, etc.), `ConfigLoaderParams`, `ConfigLoaderReturn`, `findKey`, `iSearchKey`, `loadDotenv`, `parseConfig`, `KnownConfigs` |
|
|
48
|
+
| `context.mts` | `TContext` branded string type; `IContextBrand` |
|
|
49
|
+
| `cron.mts` | `CronExpression` enum, `TOffset`, `SchedulerCronOptions`, `SchedulerIntervalOptions`, `SchedulerSlidingOptions`, `DigitalAlchemyScheduler`, `SchedulerBuilder` |
|
|
50
|
+
| `errors.mts` | `BootstrapException` (wiring-time errors), `InternalError` (runtime errors) — both carry `context`, `cause`, `timestamp` |
|
|
51
|
+
| `events.mts` | Global error event name constants (`DIGITAL_ALCHEMY_NODE_GLOBAL_ERROR`, etc.) |
|
|
52
|
+
| `extend.mts` | `deepExtend`, `deepCloneArray`, `cloneSpecificValue` — deep merge utilities |
|
|
53
|
+
| `index.mts` | Re-exports all helper exports; new public helpers thread through here |
|
|
54
|
+
| `lifecycle.mts` | `TLifecycleBase`, `TLifeCycleRegister`, `LIFECYCLE_STAGES` array, `LifecycleStages` type, `LifecycleCallback` |
|
|
55
|
+
| `logger.mts` | `ILogger`, `TLoggerFunction`, `DigitalAlchemyLogger`, `GetLogger`, `METHOD_COLORS`, `fatalLog`, `EVENT_UPDATE_LOG_LEVELS` |
|
|
56
|
+
| `module.mts` | `createModule`, `DigitalAlchemyModule`, `ModuleExtension` — chainable module builder that can export to application, library, or test runner |
|
|
57
|
+
| `service-runner.mts` | `ServiceRunner` — minimal one-service bootstrap helper for scripts |
|
|
58
|
+
| `utilities.mts` | Numeric constants (`START`, `NONE`, `EMPTY`, `FIRST`, `ARRAY_OFFSET`, `SINGLE`, `UP`, `DOWN`, `MINUTE`, `HOUR`, `DAY`, `SECOND`, `YEAR`, etc.); `sleep`, `debounce`, `toOffsetMs`, `toOffsetDuration`, `SleepReturn`, `TBlackHole` |
|
|
59
|
+
| `wiring.mts` | `TServiceParams`, `ServiceFunction`, `ServiceMap`, `ApplicationDefinition`, `LibraryDefinition`, `BootstrapOptions`, `TInjectedConfig`, `LoadedModules`, `buildSortOrder`, `CreateLibrary`, `wireOrder`, `COERCE_CONTEXT`, `WIRE_PROJECT` |
|
|
60
|
+
|
|
61
|
+
### `src/testing/*.mts` — test infrastructure (not test cases)
|
|
62
|
+
|
|
63
|
+
| File | Owns |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `mock-logger.mts` | `createMockLogger()` — returns a no-op `ILogger`; import in specs to suppress output |
|
|
66
|
+
| `test-module.mts` | `TestRunner` factory + `iTestRunner` interface — boots a real DI graph scoped to a test; chainable API: `.configure`, `.setOptions`, `.run`, `.serviceParams`, `.setup`, `.appendLibrary`, `.appendService`, `.replaceLibrary`, `.teardown` |
|
|
67
|
+
| `index.mts` | Re-exports `mock-logger` and `test-module` |
|
|
68
|
+
|
|
69
|
+
### `testing/` (sibling of `src/`, not inside it)
|
|
70
|
+
|
|
71
|
+
All `.spec.mts` files live here. Each spec file maps to a service or helper:
|
|
72
|
+
`als.spec.mts`, `configuration.spec.mts`, `internal.spec.mts`, `is.spec.mts`, `logger.spec.mts`, `scheduler.spec.mts`, `testing.spec.mts`, `utilities.spec.mts`, `wiring.spec.mts`. A `setup.mts` file and `pipelines/` subdirectory handle vitest global setup.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. The TServiceParams pattern in core specifically
|
|
77
|
+
|
|
78
|
+
### Reaching `is`
|
|
79
|
+
|
|
80
|
+
**Inside core services and helpers**, use `internal.utils.is` — do not `import { is }` from the index:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// correct inside a service factory
|
|
84
|
+
export function MyService({ internal }: TServiceParams) {
|
|
85
|
+
const { is } = internal.utils;
|
|
86
|
+
if (is.empty(value)) { ... }
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**The one exception:** `is.service.mts` is imported directly in `lifecycle.service.mts` and `wiring.service.mts` to break a circular dependency that would form if they went through `../index.mts`. This is intentional — leave it.
|
|
91
|
+
|
|
92
|
+
### Dual-arity logger with `{ name: fnRef }` tagging
|
|
93
|
+
|
|
94
|
+
Every function entry logs with its own function reference as the `name` field. This appears in the rendered output as the function name, enabling precise call-site identification without string literals.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
function loadThing(id: string) {
|
|
98
|
+
logger.trace({ name: loadThing, id }, "loading");
|
|
99
|
+
// ...
|
|
100
|
+
logger.debug({ name: loadThing }, "loaded");
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Pass the function reference directly (not `loadThing.name`). The logger extracts `.name` from the object or function automatically.
|
|
105
|
+
|
|
106
|
+
### Lifecycle hook order
|
|
107
|
+
|
|
108
|
+
Hooks are attached in service constructors but executed by the wiring engine in strict order:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
onPreInit → onPostConfig → onBootstrap → onReady
|
|
112
|
+
→ onPreShutdown → onShutdownStart → onShutdownComplete
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `onPreInit`: safe for attaching state; config is not yet loaded — do not read `config.*`
|
|
116
|
+
- `onPostConfig`: config values are available from this point forward
|
|
117
|
+
- `onBootstrap`: all modules are wired; safe to call other services
|
|
118
|
+
- `onReady`: app is fully running; schedulers start here
|
|
119
|
+
- `onPreShutdown` / `onShutdownStart` / `onShutdownComplete`: teardown in order
|
|
120
|
+
|
|
121
|
+
Each hook accepts an optional `priority` number. Positive priorities run first (high → low), un-prioritized callbacks run in parallel, negative priorities run last.
|
|
122
|
+
|
|
123
|
+
### No `metrics` injection
|
|
124
|
+
|
|
125
|
+
`TServiceParams` in this repo does not include `metrics`. Do not write `metrics.perf()`, `metrics.histogram(...)`, or any metrics-instrumentation patterns. Those patterns exist in sibling repos (e.g., vault-ts) that build on top of core. Core itself has no metrics service.
|
|
126
|
+
|
|
127
|
+
### `context: TContext` for builder-style services
|
|
128
|
+
|
|
129
|
+
`scheduler` is a builder: it returns a function that accepts `context: TContext` and returns the actual scheduler API. This is the pattern for any service that needs to bind per-caller context at use time:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// wiring.service.mts — how scheduler is injected
|
|
133
|
+
scheduler: boilerplate?.scheduler?.(context),
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Services that need to pass context down into callbacks capture the context injected into their own factory:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
export function MyService({ context, scheduler }: TServiceParams) {
|
|
140
|
+
scheduler.cron({ exec: doWork, schedule: CronExpression.EVERY_MINUTE });
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 4. Style rules
|
|
147
|
+
|
|
148
|
+
### TSDoc
|
|
149
|
+
|
|
150
|
+
- Every exported factory function gets a TSDoc comment with at minimum a one-line summary.
|
|
151
|
+
- Every method on the returned object gets a one-line summary.
|
|
152
|
+
- Use `@remarks` for non-obvious behavior (e.g., "only valid after `onPostConfig`").
|
|
153
|
+
- Use `@throws` when the function throws a documented exception.
|
|
154
|
+
- `@internal` on symbols not intended for downstream consumers.
|
|
155
|
+
|
|
156
|
+
### Inline comments
|
|
157
|
+
|
|
158
|
+
Write `// why`, never `// what`. The code describes what. The comment explains the reason:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// only read config after PostConfig fires; value is undefined before that
|
|
162
|
+
lifecycle.onPostConfig(() => {
|
|
163
|
+
CURRENT_LOG_LEVEL = config.boilerplate.LOG_LEVEL;
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Logging conventions
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// entry — always trace with name + relevant inputs
|
|
171
|
+
logger.trace({ name: fnRef, ...inputs }, "brief description");
|
|
172
|
+
|
|
173
|
+
// decision points — trace
|
|
174
|
+
logger.trace({ name: fnRef, chosen }, "selected path");
|
|
175
|
+
|
|
176
|
+
// successful completion — debug
|
|
177
|
+
logger.debug({ name: fnRef }, "operation complete");
|
|
178
|
+
|
|
179
|
+
// expected-but-notable — info
|
|
180
|
+
// unexpected non-fatal — warn
|
|
181
|
+
// errors — error or fatal
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Error types
|
|
185
|
+
|
|
186
|
+
- `BootstrapException(context, "SCREAMING_SNAKE_CODE", "human message")` — for wiring failures, misconfiguration, or anything that should prevent boot.
|
|
187
|
+
- `InternalError(context, "CODE", "message")` — for runtime logic errors after boot.
|
|
188
|
+
- No bare `new Error(...)` in service or helper code.
|
|
189
|
+
|
|
190
|
+
### Hard prohibitions
|
|
191
|
+
|
|
192
|
+
- No `console.*` — use `logger.*` or `fatalLog` for last-resort stderr writes.
|
|
193
|
+
- No `process.stdout` writes.
|
|
194
|
+
- No `process.env.*` access outside of `config-environment-loader.mts` and `wiring.service.mts` (where `IS_TEST` / `NODE_ENV` defaults are set).
|
|
195
|
+
- No module-level side effects beyond constant declarations and `dayjs.extend(...)`.
|
|
196
|
+
- No `.catch()` — use `internal.safeExec` for wrapped async execution, or handle errors explicitly.
|
|
197
|
+
- No classic `for` / `for...of` loops — use `each`, `eachSeries`, `eachLimit`, `.forEach`, `.map`, `.filter`, `.some`, `.find`.
|
|
198
|
+
- No `ts-ignore` on new code; `ts-expect-error` is acceptable only with a comment explaining why.
|
|
199
|
+
|
|
200
|
+
### Cast restrictions
|
|
201
|
+
|
|
202
|
+
Follow the existing eslint config. In particular: `@typescript-eslint/no-magic-numbers` is enforced — declare named constants for non-obvious numeric literals.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 5. Validation
|
|
207
|
+
|
|
208
|
+
All commands run from the repo root (`/home/zoe/Repos/DigitalAlchemyTS/core`).
|
|
209
|
+
|
|
210
|
+
### `yarn lint`
|
|
211
|
+
|
|
212
|
+
Runs eslint over `src/` and `testing/`. Must pass with zero errors and zero warnings. A clean run exits 0 with no output.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
yarn lint
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### `yarn test`
|
|
219
|
+
|
|
220
|
+
Runs vitest. All specs in `testing/` must pass. A clean run shows all tests green with no skipped specs (unless the test is explicitly marked `.skip` for a documented reason).
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
yarn test
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Type check the publish surface
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
tsc -p tsconfig.lib.json --noEmit
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This checks only the files that end up in `dist/`. Errors here are publish-blocking.
|
|
233
|
+
|
|
234
|
+
### `yarn build`
|
|
235
|
+
|
|
236
|
+
Produces `dist/` cleanly. Use to verify the publish output compiles.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
yarn build
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Do not touch
|
|
243
|
+
|
|
244
|
+
- `coverage/` — generated artifact, gitignored
|
|
245
|
+
- `node_modules/` — managed by yarn
|
|
246
|
+
- `yarn.lock` — only update deliberately with `yarn add` / `yarn remove`
|
|
247
|
+
- `dist/` — gitignored; never commit it
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 6. Footguns specific to core
|
|
252
|
+
|
|
253
|
+
### `wiring.mts` and `wiring.service.mts` — the engine was deliberately split
|
|
254
|
+
|
|
255
|
+
`helpers/wiring.mts` contains all the types and pure functions (`CreateLibrary`, `buildSortOrder`, `TServiceParams`, etc.). `services/wiring.service.mts` contains the runtime bootstrap logic (`CreateApplication`, `bootstrap`, `wireService`, `teardown`).
|
|
256
|
+
|
|
257
|
+
This split exists to break a circular reference that would form if types and the bootstrap runtime lived in the same file. **Do not merge them.** Read both before touching either. Changes to `TServiceParams` shape in `helpers/wiring.mts` affect every downstream library.
|
|
258
|
+
|
|
259
|
+
### `index.mts` is the re-export hub
|
|
260
|
+
|
|
261
|
+
`src/index.mts` re-exports everything. New public symbols thread through either `src/helpers/index.mts` or `src/services/index.mts` (whichever is appropriate), not directly into `src/index.mts`.
|
|
262
|
+
|
|
263
|
+
### Services import from `../index.mts`, not from siblings
|
|
264
|
+
|
|
265
|
+
Services import from `../index.mts` rather than directly from sibling files:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// correct
|
|
269
|
+
import { deepExtend, eachSeries } from "../index.mts";
|
|
270
|
+
|
|
271
|
+
// wrong — breaks the circular-ref mitigation
|
|
272
|
+
import { deepExtend } from "./extend.mts";
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The only exceptions are the deliberate direct imports in `lifecycle.service.mts` and `wiring.service.mts` of `is.service.mts`.
|
|
276
|
+
|
|
277
|
+
### `lifecycle.service.mts` imports `is` directly
|
|
278
|
+
|
|
279
|
+
`lifecycle.service.mts` and `wiring.service.mts` both `import { is } from "./is.service.mts"` instead of going through `../index.mts`. This breaks the circular dep that would form at module init time. Leave this pattern intact.
|
|
280
|
+
|
|
281
|
+
### `TestRunner` boots a real DI graph
|
|
282
|
+
|
|
283
|
+
`TestRunner` in `src/testing/test-module.mts` bootstraps a full application with real lifecycle execution. It is not a mock framework. This means:
|
|
284
|
+
|
|
285
|
+
- Lifecycle hooks fire in real order.
|
|
286
|
+
- Assertions inside `lifecycle.onPostConfig(...)` only see post-config values.
|
|
287
|
+
- Services passed to `.appendLibrary` / `.appendService` execute normally.
|
|
288
|
+
- Always call `.teardown()` in `afterEach` — open handles will keep vitest hanging.
|
|
289
|
+
|
|
290
|
+
Mocking depth matters: spying on a method only replaces that method, not the service it belongs to. When in doubt, use `.replaceLibrary` or inject a stub via `.appendService`.
|
|
291
|
+
|
|
292
|
+
### `configSources` controls loader activation
|
|
293
|
+
|
|
294
|
+
By default, `TestRunner` does not load env vars or config files (this is the safe default for tests). To test env-var resolution, pass `loadConfigs: true` or `setOptions({ configSources: { env: true } })`. To assert that env loading is disabled, pass `configSources: { env: false }`.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 7. Where to find more
|
|
299
|
+
|
|
300
|
+
Long-form learning material — architecture overviews, declaration merging docs, advanced recipes — exists in a sibling documentation repository. Refer to that repo abstractly; its path and URL are out of scope here.
|
|
301
|
+
|
|
302
|
+
This `CLAUDE.md` is the canonical working reference for the `core` repo itself. When this file and external docs disagree about what to do *in this repo*, this file wins.
|
package/README.md
CHANGED
|
@@ -17,11 +17,27 @@ yarn add @digital-alchemy/core
|
|
|
17
17
|
|
|
18
18
|
## Introduction
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
`@digital-alchemy/core` is a dependency-injection framework for TypeScript — no decorators, no reflection, no class hierarchy. Services are plain functions that receive their dependencies through a single typed parameter. The framework wires everything at boot time, with full type safety across your entire service graph.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Targets the latest ESModule syntax and runs on Bun, Deno, and modern Node.
|
|
23
|
+
|
|
24
|
+
## At a glance
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { CreateApplication, TServiceParams } from "@digital-alchemy/core";
|
|
28
|
+
|
|
29
|
+
function HelloService({ logger, lifecycle }: TServiceParams) {
|
|
30
|
+
lifecycle.onReady(() => logger.info("hello world"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const app = CreateApplication({
|
|
34
|
+
name: "hello",
|
|
35
|
+
services: { hello: HelloService },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await app.bootstrap();
|
|
39
|
+
```
|
|
23
40
|
|
|
24
|
-
The framework adds minimal overhead to boot times, making it well-suited for a wide range of applications, such as web servers, serverless functions, automation tools, and long-running background scripts.
|
|
25
41
|
## What it does
|
|
26
42
|
|
|
27
43
|
- **Service wiring** - Automatic dependency injection with full type safety
|
package/dist/helpers/async.d.mts
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async iteration helpers — drop-in replacements for the `async` library with
|
|
3
|
+
* more predictable behavior.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Provides `each`, `eachSeries`, and `eachLimit` functions that mirror common
|
|
7
|
+
* async iteration patterns but with consistent semantics. All functions accept
|
|
8
|
+
* either an array or a Set as input and execute a callback on each item,
|
|
9
|
+
* returning a promise that resolves when all iterations complete.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Execute an async callback in parallel on each item in a collection.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Invokes all callbacks concurrently using `Promise.all`. Accepts either an
|
|
16
|
+
* array or a Set; Sets are converted to arrays before processing. If `callback`
|
|
17
|
+
* throws or rejects, the entire operation fails with that error.
|
|
18
|
+
*/
|
|
1
19
|
export declare function each<T = unknown>(item: T[] | Set<T>, callback: (item: T) => Promise<void | unknown>): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Execute an async callback serially on each item in a collection.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Invokes callbacks one after the other, waiting for each to complete before
|
|
25
|
+
* starting the next. Accepts either an array or a Set; Sets are converted to
|
|
26
|
+
* arrays. Throws if the input is not a Set or array after conversion.
|
|
27
|
+
*
|
|
28
|
+
* @throws {TypeError} when the input is neither an array nor a Set.
|
|
29
|
+
*/
|
|
2
30
|
export declare function eachSeries<T = void>(item: T[] | Set<T>, callback: (item: T) => Promise<void | unknown>): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Execute an async callback on each item in an array while limiting concurrency.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Respects a maximum number of concurrent callbacks by tracking active promises
|
|
36
|
+
* and awaiting one to resolve before starting a new one. Processes the first
|
|
37
|
+
* `limit` items immediately, then maintains the limit as remaining items are
|
|
38
|
+
* queued. Resolves only when all callbacks complete.
|
|
39
|
+
*/
|
|
3
40
|
export declare function eachLimit<T = unknown>(items: T[], limit: number, callback: (item: T) => Promise<void | unknown>): Promise<void>;
|
package/dist/helpers/async.mjs
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async iteration helpers — drop-in replacements for the `async` library with
|
|
3
|
+
* more predictable behavior.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Provides `each`, `eachSeries`, and `eachLimit` functions that mirror common
|
|
7
|
+
* async iteration patterns but with consistent semantics. All functions accept
|
|
8
|
+
* either an array or a Set as input and execute a callback on each item,
|
|
9
|
+
* returning a promise that resolves when all iterations complete.
|
|
10
|
+
*/
|
|
1
11
|
import { is } from "../index.mjs";
|
|
2
12
|
import { ARRAY_OFFSET, SINGLE, START } from "./utilities.mjs";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Execute an async callback in parallel on each item in a collection.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Invokes all callbacks concurrently using `Promise.all`. Accepts either an
|
|
18
|
+
* array or a Set; Sets are converted to arrays before processing. If `callback`
|
|
19
|
+
* throws or rejects, the entire operation fails with that error.
|
|
20
|
+
*/
|
|
8
21
|
export async function each(item, callback) {
|
|
22
|
+
// convert Set to array for uniform handling
|
|
9
23
|
if (item instanceof Set) {
|
|
10
24
|
item = [...item.values()];
|
|
11
25
|
}
|
|
12
26
|
await Promise.all(item.map(async (i) => await callback(i)));
|
|
13
27
|
}
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Execute an async callback serially on each item in a collection.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* Invokes callbacks one after the other, waiting for each to complete before
|
|
33
|
+
* starting the next. Accepts either an array or a Set; Sets are converted to
|
|
34
|
+
* arrays. Throws if the input is not a Set or array after conversion.
|
|
35
|
+
*
|
|
36
|
+
* @throws {TypeError} when the input is neither an array nor a Set.
|
|
37
|
+
*/
|
|
15
38
|
export async function eachSeries(item, callback) {
|
|
39
|
+
// convert Set to array for uniform handling
|
|
16
40
|
if (item instanceof Set) {
|
|
17
41
|
item = [...item.values()];
|
|
18
42
|
}
|
|
43
|
+
// ensure we have an array; failing fast helps catch misuse early
|
|
19
44
|
if (!is.array(item)) {
|
|
20
45
|
throw new TypeError(`not provided an array`);
|
|
21
46
|
}
|
|
@@ -23,29 +48,39 @@ export async function eachSeries(item, callback) {
|
|
|
23
48
|
await callback(item[i]);
|
|
24
49
|
}
|
|
25
50
|
}
|
|
26
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Execute an async callback on each item in an array while limiting concurrency.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Respects a maximum number of concurrent callbacks by tracking active promises
|
|
56
|
+
* and awaiting one to resolve before starting a new one. Processes the first
|
|
57
|
+
* `limit` items immediately, then maintains the limit as remaining items are
|
|
58
|
+
* queued. Resolves only when all callbacks complete.
|
|
59
|
+
*/
|
|
27
60
|
export async function eachLimit(items, limit, callback) {
|
|
28
|
-
//
|
|
61
|
+
// track promises to enforce the concurrency limit
|
|
29
62
|
const activePromises = new Set();
|
|
30
|
-
//
|
|
63
|
+
// queue a new callback and manage the promise lifecycle
|
|
31
64
|
async function addTask(item) {
|
|
32
65
|
const promise = callback(item).then(() => {
|
|
33
|
-
|
|
66
|
+
// clean up completed promise so we can detect when to start new ones
|
|
67
|
+
activePromises.delete(promise);
|
|
34
68
|
});
|
|
35
69
|
activePromises.add(promise);
|
|
70
|
+
// if at or over limit, block until at least one promise resolves
|
|
36
71
|
if (activePromises.size >= limit) {
|
|
37
|
-
await Promise.race(activePromises);
|
|
72
|
+
await Promise.race(activePromises);
|
|
38
73
|
}
|
|
39
74
|
}
|
|
40
|
-
//
|
|
75
|
+
// seed the concurrency pool with the first `limit` items
|
|
41
76
|
const initialTasks = items.slice(SINGLE, limit).map(item => addTask(item));
|
|
42
|
-
//
|
|
77
|
+
// wait for all initial tasks to be queued (not necessarily complete)
|
|
43
78
|
await Promise.all(initialTasks);
|
|
44
|
-
//
|
|
79
|
+
// process remaining items while respecting the limit
|
|
45
80
|
for (let i = limit - ARRAY_OFFSET; i < items.length; i++) {
|
|
46
81
|
await addTask(items[i]);
|
|
47
82
|
}
|
|
48
|
-
//
|
|
83
|
+
// wait for all in-flight operations to finish
|
|
49
84
|
await Promise.all(activePromises);
|
|
50
85
|
}
|
|
51
86
|
//# sourceMappingURL=async.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async.mjs","sourceRoot":"","sources":["../../src/helpers/async.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE9D
|
|
1
|
+
{"version":3,"file":"async.mjs","sourceRoot":"","sources":["../../src/helpers/async.mts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,IAAkB,EAClB,QAA8C;IAE9C,4CAA4C;IAC5C,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC,CAAC,EAAC,EAAE,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAkB,EAClB,QAA8C;IAE9C,4CAA4C;IAC5C,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5B,CAAC;IACD,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAU,EACV,KAAa,EACb,QAA8C;IAE9C,kDAAkD;IAClD,MAAM,cAAc,GAAiC,IAAI,GAAG,EAAE,CAAC;IAE/D,wDAAwD;IACxD,KAAK,UAAU,OAAO,CAAC,IAAO;QAC5B,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACvC,qEAAqE;YACrE,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QACH,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5B,iEAAiE;QACjE,IAAI,cAAc,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC;YACjC,MAAM,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,yDAAyD;IACzD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAE3E,qEAAqE;IACrE,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAEhC,qDAAqD;IACrD,KAAK,IAAI,CAAC,GAAG,KAAK,GAAG,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzD,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC;IAED,8CAA8C;IAC9C,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -1,5 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable and CLI switch config loader.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Reads config from Node.js env vars and command-line arguments. For each config
|
|
6
|
+
* key, searches using a three-tier precedence: CLI switches (highest), then env
|
|
7
|
+
* vars (middle), then defaults (lowest). Both argv and env sources support a
|
|
8
|
+
* double-underscore variant (`MODULE__KEY`) in addition to single-underscore,
|
|
9
|
+
* enabling env vars with embedded dots/slashes that would be shell-escaped.
|
|
10
|
+
* Timing information is captured and returned for observability.
|
|
11
|
+
*/
|
|
1
12
|
import type { ConfigLoaderParams, ConfigLoaderReturn, ModuleConfiguration } from "./config.mts";
|
|
2
13
|
import type { ServiceMap } from "./wiring.mts";
|
|
14
|
+
/**
|
|
15
|
+
* Load configuration from environment variables and CLI switches.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Merges CLI arguments and environment variables into the config tree, respecting
|
|
19
|
+
* the configured sources (argv, env) and individual config key source restrictions.
|
|
20
|
+
* Searches for each key in three forms (in order):
|
|
21
|
+
* 1. `MODULE__KEY` (double underscore) — preferred, allows shell-escaped env vars
|
|
22
|
+
* 2. `MODULE_KEY` (single underscore) — classic format
|
|
23
|
+
* 3. `KEY` (bare key) — fallback for globals
|
|
24
|
+
*
|
|
25
|
+
* CLI arguments are checked first (highest precedence); env vars are checked second.
|
|
26
|
+
* Both are optional and controlled by `internal.boot.options.configSources`.
|
|
27
|
+
*
|
|
28
|
+
* Timing data is collected for argv and env separately and returned if the
|
|
29
|
+
* optional `timings` object is provided.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```
|
|
33
|
+
* // Search order for app.NODE_ENV config key:
|
|
34
|
+
* // 1. --app__NODE_ENV cli switch
|
|
35
|
+
* // 2. APP__NODE_ENV env var
|
|
36
|
+
* // 3. --app_NODE_ENV cli switch
|
|
37
|
+
* // 4. APP_NODE_ENV env var
|
|
38
|
+
* // 5. --NODE_ENV cli switch
|
|
39
|
+
* // 6. NODE_ENV env var
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
3
42
|
export declare function ConfigLoaderEnvironment<S extends ServiceMap = ServiceMap, C extends ModuleConfiguration = ModuleConfiguration>({ configs, internal, logger, timings, }: ConfigLoaderParams<S, C> & {
|
|
4
43
|
timings?: Record<string, string>;
|
|
5
44
|
}): ConfigLoaderReturn;
|
|
@@ -1,7 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable and CLI switch config loader.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Reads config from Node.js env vars and command-line arguments. For each config
|
|
6
|
+
* key, searches using a three-tier precedence: CLI switches (highest), then env
|
|
7
|
+
* vars (middle), then defaults (lowest). Both argv and env sources support a
|
|
8
|
+
* double-underscore variant (`MODULE__KEY`) in addition to single-underscore,
|
|
9
|
+
* enabling env vars with embedded dots/slashes that would be shell-escaped.
|
|
10
|
+
* Timing information is captured and returned for observability.
|
|
11
|
+
*/
|
|
1
12
|
import { env } from "node:process";
|
|
2
13
|
import minimist from "minimist";
|
|
3
14
|
import { is, NONE } from "../index.mjs";
|
|
4
15
|
import { findKey, iSearchKey, loadDotenv, parseConfig } from "./config.mjs";
|
|
16
|
+
/**
|
|
17
|
+
* Load configuration from environment variables and CLI switches.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Merges CLI arguments and environment variables into the config tree, respecting
|
|
21
|
+
* the configured sources (argv, env) and individual config key source restrictions.
|
|
22
|
+
* Searches for each key in three forms (in order):
|
|
23
|
+
* 1. `MODULE__KEY` (double underscore) — preferred, allows shell-escaped env vars
|
|
24
|
+
* 2. `MODULE_KEY` (single underscore) — classic format
|
|
25
|
+
* 3. `KEY` (bare key) — fallback for globals
|
|
26
|
+
*
|
|
27
|
+
* CLI arguments are checked first (highest precedence); env vars are checked second.
|
|
28
|
+
* Both are optional and controlled by `internal.boot.options.configSources`.
|
|
29
|
+
*
|
|
30
|
+
* Timing data is collected for argv and env separately and returned if the
|
|
31
|
+
* optional `timings` object is provided.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```
|
|
35
|
+
* // Search order for app.NODE_ENV config key:
|
|
36
|
+
* // 1. --app__NODE_ENV cli switch
|
|
37
|
+
* // 2. APP__NODE_ENV env var
|
|
38
|
+
* // 3. --app_NODE_ENV cli switch
|
|
39
|
+
* // 4. APP_NODE_ENV env var
|
|
40
|
+
* // 5. --NODE_ENV cli switch
|
|
41
|
+
* // 6. NODE_ENV env var
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
5
44
|
export async function ConfigLoaderEnvironment({ configs, internal, logger, timings, }) {
|
|
6
45
|
const DECIMALS = 2;
|
|
7
46
|
const CLI_SWITCHES = minimist(process.argv);
|
|
@@ -11,27 +50,26 @@ export async function ConfigLoaderEnvironment({ configs, internal, logger, timin
|
|
|
11
50
|
const canArgv = internal.boot.options?.configSources?.argv ?? true;
|
|
12
51
|
const shouldArgv = (source) => canArgv && (!is.array(source) || source.includes("argv"));
|
|
13
52
|
const shouldEnv = (source) => canEnvironment && (!is.array(source) || source.includes("env"));
|
|
14
|
-
//
|
|
15
|
-
// accounts for `--env-file` switches, and whatever is passed in via bootstrap
|
|
53
|
+
// merge dotenv files and env-file switches into process.env
|
|
16
54
|
loadDotenv(internal, CLI_SWITCHES, logger);
|
|
17
55
|
const environmentKeys = Object.keys(env);
|
|
18
|
-
//
|
|
56
|
+
// track timing for argv and env separately
|
|
19
57
|
let argvTime = NONE;
|
|
20
58
|
let envTime = NONE;
|
|
21
|
-
//
|
|
59
|
+
// iterate through all module configurations
|
|
22
60
|
configs.forEach((configuration, project) => {
|
|
23
61
|
const cleanedProject = project.replaceAll("-", "_");
|
|
24
|
-
//
|
|
62
|
+
// iterate through each config key within the module
|
|
25
63
|
Object.keys(configuration).forEach(key => {
|
|
26
64
|
const { source } = configs.get(project)[key];
|
|
27
|
-
//
|
|
28
|
-
// -
|
|
29
|
-
// - CONFIG_KEY (only key, ex: NODE_ENV)
|
|
65
|
+
// search keys in order: double-underscore (preferred), single-underscore, bare key
|
|
66
|
+
// double-underscore allows env var names with embedded special chars (dots, slashes)
|
|
30
67
|
const noAppPath = `${cleanedProject}_${key}`;
|
|
31
|
-
const
|
|
68
|
+
const noAppPathDouble = `${cleanedProject}__${key}`;
|
|
69
|
+
const search = [noAppPathDouble, noAppPath, key];
|
|
32
70
|
const configPath = `${project}.${key}`;
|
|
33
71
|
if (canArgv) {
|
|
34
|
-
//
|
|
72
|
+
// CLI switches take precedence; find a matching flag in the argv
|
|
35
73
|
const argvStart = performance.now();
|
|
36
74
|
const flag = findKey(search, switchKeys);
|
|
37
75
|
if (flag && shouldArgv(source)) {
|
|
@@ -47,12 +85,13 @@ export async function ConfigLoaderEnvironment({ configs, internal, logger, timin
|
|
|
47
85
|
}
|
|
48
86
|
argvTime += performance.now() - argvStart;
|
|
49
87
|
}
|
|
50
|
-
//
|
|
88
|
+
// fallback to environment variable if no CLI switch was found
|
|
51
89
|
if (canEnvironment) {
|
|
52
90
|
const envStart = performance.now();
|
|
53
91
|
const environment = findKey(search, environmentKeys);
|
|
54
92
|
if (!is.empty(environment) && shouldEnv(source)) {
|
|
55
93
|
const environmentName = iSearchKey(environment, environmentKeys);
|
|
94
|
+
// only set if the env var is defined and non-empty
|
|
56
95
|
if (!is.string(env[environmentName]) || !is.empty(env[environmentName])) {
|
|
57
96
|
internal.utils.object.set(out, configPath, parseConfig(configuration[key], env[environmentName]));
|
|
58
97
|
}
|
|
@@ -69,6 +108,7 @@ export async function ConfigLoaderEnvironment({ configs, internal, logger, timin
|
|
|
69
108
|
}
|
|
70
109
|
});
|
|
71
110
|
});
|
|
111
|
+
// record timing if provided
|
|
72
112
|
if (timings) {
|
|
73
113
|
if (argvTime) {
|
|
74
114
|
timings.argv = `${argvTime.toFixed(DECIMALS)}ms`;
|