@bymax-one/nest-config 1.2.3 → 1.2.5
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/CHANGELOG.md +103 -1
- package/README.md +27 -14
- package/dist/internal/index.cjs +58 -133
- package/dist/internal/index.d.cts +15 -1
- package/dist/internal/index.d.ts +15 -1
- package/dist/internal/index.mjs +58 -134
- package/dist/testing/index.cjs +51 -6
- package/dist/testing/index.mjs +52 -7
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,106 @@ as the GitHub Release body, so each released version needs a matching
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.5] - 2026-11-01
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A test override that is not a plain object is one issue.** A namespace given `null` threw a raw
|
|
19
|
+
`TypeError`, an array produced one issue per index, a `Map` or a `Date` applied
|
|
20
|
+
nothing and said nothing, and a class instance had its own fields read as overrides. `createTestConfig` and `configTestingModule` now report each of them once,
|
|
21
|
+
by name, as "the override for `x` must be an object", in the same `BymaxConfigValidationError` as any
|
|
22
|
+
unmatched path. The overrides argument itself is held to the same rule and reported as "the overrides
|
|
23
|
+
must be an object"; `undefined` and `null` still mean no overrides.
|
|
24
|
+
|
|
25
|
+
#### Apply to a derived backend
|
|
26
|
+
|
|
27
|
+
Run the test suites. A test that expected a `TypeError` for a `null` namespace now receives a
|
|
28
|
+
`BymaxConfigValidationError`. A namespace override given as a class instance was applied field by
|
|
29
|
+
field; it is now refused, so pass a plain object. A `Map` or a `Date` as a namespace override was
|
|
30
|
+
ignored; it is now refused too.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **The shipped bundles no longer carry comments.** The build runs `scripts/strip-bundle-comments.mjs`
|
|
35
|
+
after `tsup`: every comment is removed from `index.mjs` and `index.cjs` except the legal,
|
|
36
|
+
bundler-annotation and region-marker ones the script keeps, and the script proves with the
|
|
37
|
+
TypeScript parser that it changed no code. The documentation people read
|
|
38
|
+
comes from the `.d.ts` files, which are untouched. The internal bundle goes from 22,079 to 16,652
|
|
39
|
+
bytes and the package from 68.3 to 65.5 kB. Nothing to apply in a derived backend.
|
|
40
|
+
- **The internal bundle has a size budget.** `scripts/check-size.mjs` gates `dist/internal/index.mjs`,
|
|
41
|
+
which carries nearly all of the code, at 4.6 KiB brotli. Tooling only.
|
|
42
|
+
|
|
43
|
+
## [1.2.4] - 2026-11-01
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **A `source` that is present but `undefined` fails the boot.** `BymaxConfigModule.forRoot({ schema,
|
|
48
|
+
source: loadSource() })` used to validate `process.env` when the loader returned nothing, and boot
|
|
49
|
+
successfully on configuration the host never meant to use. Passing the key with the value `undefined` or
|
|
50
|
+
`null` now throws a `TypeError` ("source was provided but is undefined or null"); omitting the key still reads
|
|
51
|
+
`process.env`.
|
|
52
|
+
|
|
53
|
+
#### Apply to a derived backend
|
|
54
|
+
|
|
55
|
+
Search for a `source:` option that can be `undefined`. If the host relied on the fall-back to
|
|
56
|
+
`process.env`, leave the key out instead of passing `undefined`.
|
|
57
|
+
|
|
58
|
+
- **A `meta({ env })` that cannot name a variable is an issue.** An `env` that is present but is not
|
|
59
|
+
a non-empty string (`''`, `undefined`, a number) used to be skipped, and the leaf read the variable
|
|
60
|
+
derived from its path. The aggregated validation report now lists it, naming the variable the leaf
|
|
61
|
+
would read and the kind of value found, on any layer of the leaf: an inner layer whose `env` is
|
|
62
|
+
unusable is reported even when an outer layer's usable one wins. The issue is its own line for
|
|
63
|
+
that leaf, before the line about its value. A leaf with no `env` at all is unchanged, and
|
|
64
|
+
`resolveSourceNames` still answers the derived name.
|
|
65
|
+
|
|
66
|
+
#### Apply to a derived backend
|
|
67
|
+
|
|
68
|
+
Search the schemas for `meta({ env: ... })` whose argument can be empty or undefined (for example
|
|
69
|
+
`meta({ env: process.env.X })`) and give it a real name or remove the call.
|
|
70
|
+
|
|
71
|
+
- **A test override that matches no leaf throws.** `createTestConfig` and `configTestingModule` used to
|
|
72
|
+
drop an override for a path the schema does not declare (a misspelled leaf, an undeclared namespace,
|
|
73
|
+
a stale override after a rename) and answer with a config that lacked it. They now throw a
|
|
74
|
+
`BymaxConfigValidationError` listing every unmatched path, before any provider is built, even when
|
|
75
|
+
the override's value is `undefined`. The path is the name shown in the report, and its footer asks
|
|
76
|
+
for a fix in the test rather than a restart of the process. A namespace given something other than
|
|
77
|
+
an object is reported once, by name, instead of once per character.
|
|
78
|
+
|
|
79
|
+
#### Apply to a derived backend
|
|
80
|
+
|
|
81
|
+
Run the test suites: an override that used to be silently ignored now fails. Correct the path or
|
|
82
|
+
delete the override.
|
|
83
|
+
|
|
84
|
+
### Documentation
|
|
85
|
+
|
|
86
|
+
- A leaf that ends in `.catch(fallback)` replaces an invalid value with its fallback and reports no
|
|
87
|
+
issue; the README now says so.
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
|
|
91
|
+
- **A release that changes the public declarations must say so.** `check:api`
|
|
92
|
+
(`scripts/check-api-surface.mjs`) compares the built declarations with the previous published
|
|
93
|
+
version and fails when they differ unless the released version's CHANGELOG section has an
|
|
94
|
+
"Apply to a derived backend" note. Tooling only: nothing to apply in a derived backend.
|
|
95
|
+
- **The mutation gate is 100.** `thresholds.break` moves from 95 to 100, and `high` and `low` with it,
|
|
96
|
+
because a cold run on `main` now has no survivor. Tooling only: nothing to apply in
|
|
97
|
+
a derived backend.
|
|
98
|
+
- **The camelCase-to-variable converter has no equivalent mutant.** The acronym boundary is now a
|
|
99
|
+
zero-width lookaround instead of a capturing group with a quantifier whose shrunken form no input
|
|
100
|
+
could tell apart. Every derived variable name is unchanged: a new spec compares the converter
|
|
101
|
+
with the previous two-expression form on every string up to seven characters over
|
|
102
|
+
`a`, `B`, `C`, `1` and `_`. Nothing to apply in a derived backend.
|
|
103
|
+
|
|
104
|
+
### Tests
|
|
105
|
+
|
|
106
|
+
- **The README Quick Start is executed, not only type-checked.** A new spec takes the schema, the
|
|
107
|
+
`BymaxConfigModule.forRoot` registration and the `FeatureProvider` from the README text,
|
|
108
|
+
compiles and runs them against a real Nest testing graph, and asserts the stated outcomes: a
|
|
109
|
+
complete environment boots and the provider returns `<database url>::3000`, and an environment
|
|
110
|
+
missing both required variables fails with one error naming both. A wrong argument shape or a
|
|
111
|
+
value that fails validation now fails a test instead of reaching a reader. No source or
|
|
112
|
+
published-surface change, so there is nothing to apply in a derived backend.
|
|
113
|
+
|
|
14
114
|
## [1.2.3] - 2026-10-31
|
|
15
115
|
|
|
16
116
|
### Fixed
|
|
@@ -484,4 +584,6 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
484
584
|
[1.2.1]: https://github.com/bymaxone/nest-config/compare/v1.2.0...v1.2.1
|
|
485
585
|
[1.2.2]: https://github.com/bymaxone/nest-config/compare/v1.2.1...v1.2.2
|
|
486
586
|
[1.2.3]: https://github.com/bymaxone/nest-config/compare/v1.2.2...v1.2.3
|
|
487
|
-
[
|
|
587
|
+
[1.2.4]: https://github.com/bymaxone/nest-config/compare/v1.2.3...v1.2.4
|
|
588
|
+
[1.2.5]: https://github.com/bymaxone/nest-config/compare/v1.2.4...v1.2.5
|
|
589
|
+
[Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.2.5...HEAD
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/@bymax-one/nest-config"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
|
|
15
15
|
<a href="https://github.com/bymaxone/nest-config/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-config/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
|
|
16
16
|
<a href="https://github.com/bymaxone/nest-config/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
|
|
17
|
-
<a href="https://github.com/bymaxone/nest-config/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-
|
|
17
|
+
<a href="https://github.com/bymaxone/nest-config/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-100.00%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
|
|
18
18
|
<a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-config"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-config/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
|
|
19
19
|
<a href="https://github.com/bymaxone/nest-config/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
|
|
20
20
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
@@ -206,8 +206,10 @@ BymaxConfigValidationError: environment validation failed (3 issues)
|
|
|
206
206
|
Fix the variables above and restart the process.
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
The report has one line per
|
|
210
|
-
one Zod lists, and fixing it can reveal the next on the following start.
|
|
209
|
+
The report has one line per violation: when a leaf fails several rules, the line shows the first
|
|
210
|
+
one Zod lists, and fixing it can reveal the next on the following start. A `meta({ env })` that
|
|
211
|
+
cannot name a variable adds its own line for that leaf, before the line about its value, because it
|
|
212
|
+
is a different mistake: merging the two would hide the one that explains the other.
|
|
211
213
|
|
|
212
214
|
In a message this package generates, raw values are never printed — not
|
|
213
215
|
truncated, not masked, absent. That is a hard guarantee, verified by tests. The
|
|
@@ -265,6 +267,10 @@ including `'false'`, becomes `true`, so use `z.stringbool()` for a flag. And a v
|
|
|
265
267
|
empty string is present, not missing: `z.string()` accepts it and `z.coerce.number()` turns it into
|
|
266
268
|
`0`, so add `.min(1)` or a bound where empty should fail.
|
|
267
269
|
|
|
270
|
+
A leaf that ends in `.catch(fallback)` replaces an invalid value with its fallback and reports no
|
|
271
|
+
issue, so a mistyped variable reaches the application as the fallback; leave `.catch()` off a
|
|
272
|
+
leaf whose failure should stop the boot.
|
|
273
|
+
|
|
268
274
|
A leaf can override its derived variable name through schema metadata when a
|
|
269
275
|
legacy name must be preserved:
|
|
270
276
|
|
|
@@ -276,9 +282,12 @@ database: z.object({
|
|
|
276
282
|
```
|
|
277
283
|
|
|
278
284
|
The override is found wherever `meta` sits in the chain: `z.string().meta({ env: 'LEGACY' }).default('x')`
|
|
279
|
-
and `z.string().default('x').meta({ env: 'LEGACY' })` both read `LEGACY`. When more than one layer sets it, the outermost usable one wins
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
and `z.string().default('x').meta({ env: 'LEGACY' })` both read `LEGACY`. When more than one layer sets it, the outermost usable one wins. An `env` that is present but
|
|
286
|
+
is not a non-empty string (`''`, `undefined`, a number) cannot name a variable, so validation reports it
|
|
287
|
+
as an issue instead of letting the leaf read the variable derived from its path, on any layer: an
|
|
288
|
+
inner layer whose `env` is unusable is reported even when an outer layer's usable one wins. A leaf
|
|
289
|
+
with no `env` at all is unaffected. `resolveSourceNames` skips an unusable `env` and answers the
|
|
290
|
+
derived name.
|
|
282
291
|
|
|
283
292
|
A namespace can declare itself **open**, which waives strict unknown-key
|
|
284
293
|
detection for the rest of its prefix. Use it when the prefix is shared with
|
|
@@ -309,7 +318,7 @@ Write `.meta({ open: true })` last: methods that build a new object schema, such
|
|
|
309
318
|
| Field | Type | Required | Notes |
|
|
310
319
|
| ------------------- | ------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
311
320
|
| `schema` | `EnvSchema` | yes | Produced by `defineEnv`. |
|
|
312
|
-
| `source` | `Record<string, string \| undefined>` | no | Defaults to `process.env`
|
|
321
|
+
| `source` | `Record<string, string \| undefined>` | no | Defaults to `process.env` when the key is omitted; a key present with the value `undefined` or `null` throws at boot. Injectable for tests. |
|
|
313
322
|
| `onValidationError` | `(issues: ReadonlyArray<ConfigIssue>) => unknown` | no | Observability hook invoked before the fail-fast throw; a promise it returns is awaited. Cannot suppress the failure. |
|
|
314
323
|
| `strict` | `boolean` | no | When true, source variables matching a namespace prefix but no declared leaf raise `BYMAX_CONFIG_UNKNOWN_KEY`. Defaults to `false`. Read [Strict mode and shared prefixes](#strict-mode-and-shared-prefixes) before enabling it. |
|
|
315
324
|
|
|
@@ -604,7 +613,8 @@ resolves its variables through, so the list cannot drift from what `forRoot` rea
|
|
|
604
613
|
`SourceBinding` is `{ path: string; variable: string }`: the leaf's dot path and the
|
|
605
614
|
environment variable it reads. The list has one entry per declared leaf, in declaration
|
|
606
615
|
order. A leaf reads its `meta({ env })` override when that is a non-empty string, and
|
|
607
|
-
|
|
616
|
+
`NAMESPACE_LEAF` in SCREAMING_SNAKE_CASE, with a camelCase key split into words, when it has no
|
|
617
|
+
`env` at all.
|
|
608
618
|
It reads the schema only, never a source record, so it works on a schema exported as a
|
|
609
619
|
value.
|
|
610
620
|
|
|
@@ -816,9 +826,8 @@ Configuration decides whether a process may start at all, so the suite is held t
|
|
|
816
826
|
beyond "the tests pass".
|
|
817
827
|
|
|
818
828
|
- ✅ **100% line coverage** — statements, branches, functions and lines, enforced as a gate
|
|
819
|
-
- ✅ **
|
|
820
|
-
`break:
|
|
821
|
-
is an equivalent no directive can reach, so it stays counted rather than silenced
|
|
829
|
+
- ✅ **100.00% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at
|
|
830
|
+
`break: 100`; provable equivalents carry their reason inline and nothing else survives
|
|
822
831
|
([report](./docs/mutation_testing_results.md))
|
|
823
832
|
- ✅ **The value-free guarantee is tested, not assumed** — a sentinel secret is placed in a
|
|
824
833
|
source and the assertion is that it appears nowhere in the rendered report or in the thrown
|
|
@@ -827,7 +836,8 @@ beyond "the tests pass".
|
|
|
827
836
|
proven is the guarantee this package makes for its own generated text
|
|
828
837
|
- ✅ **Published-artifact gates** — `check:exports` resolves the types the way each module
|
|
829
838
|
system does, `check:runtime` loads every subpath from the packed tarball in ESM and
|
|
830
|
-
CommonJS,
|
|
839
|
+
CommonJS, `check:published` compiles this README's snippets against `dist/`, and `check:api`
|
|
840
|
+
requires an "Apply to a derived backend" CHANGELOG note when the built declarations change
|
|
831
841
|
- ✅ **Every suppression carries its reason** — no coverage directives anywhere; each
|
|
832
842
|
`// Stryker disable` in the production source names, after the `:` Stryker reads it from,
|
|
833
843
|
why the mutant it silences is behaviour-preserving, and `check:mutants` proves those
|
|
@@ -837,7 +847,7 @@ beyond "the tests pass".
|
|
|
837
847
|
```bash
|
|
838
848
|
pnpm test # unit suite
|
|
839
849
|
pnpm test:cov # unit suite with the 100% coverage gate
|
|
840
|
-
pnpm mutation # Stryker mutation testing (break:
|
|
850
|
+
pnpm mutation # Stryker mutation testing (break: 100)
|
|
841
851
|
pnpm typecheck # tsc strict check
|
|
842
852
|
pnpm lint # ESLint
|
|
843
853
|
```
|
|
@@ -901,7 +911,10 @@ const moduleRef = await Test.createTestingModule({
|
|
|
901
911
|
|
|
902
912
|
Overrides mirror the `namespace.leaf` shape with every key optional and typed from the schema. Each value is converted with `String(value)` before validation, so give
|
|
903
913
|
a leaf what its environment variable would hold; an object becomes `'[object Object]'`, and an override
|
|
904
|
-
of `undefined` is ignored, so a defaulted leaf cannot be unset.
|
|
914
|
+
of `undefined` is ignored, so a defaulted leaf cannot be unset. An override whose path matches no leaf
|
|
915
|
+
of the schema throws, listing every such path, even when its value is `undefined`; a namespace given
|
|
916
|
+
something other than a plain object is reported once, by name, and so is an overrides argument that is
|
|
917
|
+
not a plain object; passing `undefined` or `null` for the whole argument means no overrides. Neither helper takes `strict`, a
|
|
905
918
|
custom `source`, a hook or `isGlobal`: `configTestingModule` registers the module globally, and a
|
|
906
919
|
failure case (a missing variable, a strict violation) is built with
|
|
907
920
|
`BymaxConfigModule.forRoot({ schema, source, strict: true })` and a source you write.
|
package/dist/internal/index.cjs
CHANGED
|
@@ -53,45 +53,40 @@ var MIN_VARIABLE_COLUMN = 20;
|
|
|
53
53
|
var VARIABLE_GUTTER = 2;
|
|
54
54
|
var REPORT_HEADER = "environment validation failed";
|
|
55
55
|
var REPORT_FOOTER = "Fix the variables above and restart the process.";
|
|
56
|
+
var OVERRIDE_FOOTER = "Fix the overrides above in the test.";
|
|
56
57
|
function pluralizeIssues(count) {
|
|
57
58
|
const noun = count === 1 ? "issue" : "issues";
|
|
58
59
|
return `${count} ${noun}`;
|
|
59
60
|
}
|
|
61
|
+
function nameOf(issue) {
|
|
62
|
+
return issue.variable === "" ? issue.path : issue.variable;
|
|
63
|
+
}
|
|
60
64
|
function computeColumnWidth(issues) {
|
|
61
65
|
let longest = MIN_VARIABLE_COLUMN;
|
|
62
66
|
for (const issue of issues) {
|
|
63
|
-
longest = Math.max(longest, issue.
|
|
67
|
+
longest = Math.max(longest, nameOf(issue).length);
|
|
64
68
|
}
|
|
65
69
|
return longest + VARIABLE_GUTTER;
|
|
66
70
|
}
|
|
67
71
|
function formatIssueLine(issue, columnWidth) {
|
|
68
|
-
return `${REPORT_INDENT}${issue.
|
|
72
|
+
return `${REPORT_INDENT}${nameOf(issue).padEnd(columnWidth)}${issue.message}`;
|
|
69
73
|
}
|
|
70
74
|
function formatIssueReport(issues) {
|
|
71
75
|
const columnWidth = computeColumnWidth(issues);
|
|
72
76
|
const lines = issues.map((issue) => formatIssueLine(issue, columnWidth));
|
|
73
77
|
const header = `${REPORT_HEADER} (${pluralizeIssues(issues.length)})`;
|
|
74
|
-
|
|
78
|
+
const aboutOverrides = issues.length > 0 && issues.every((issue) => issue.variable === "");
|
|
79
|
+
return [header, "", ...lines, "", aboutOverrides ? OVERRIDE_FOOTER : REPORT_FOOTER].join("\n");
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// src/errors.ts
|
|
78
83
|
var ConfigErrorCode = Object.freeze({
|
|
79
|
-
/** Top-level error code: one or more schema violations were collected. */
|
|
80
84
|
VALIDATION: "BYMAX_CONFIG_VALIDATION",
|
|
81
|
-
/** Issue code: a required variable is absent from the source. */
|
|
82
85
|
MISSING: "BYMAX_CONFIG_MISSING",
|
|
83
|
-
/** Issue code: a variable is present but violates its constraint. */
|
|
84
86
|
INVALID: "BYMAX_CONFIG_INVALID",
|
|
85
|
-
/** Issue code: strict mode found a source variable matching no declared leaf. */
|
|
86
87
|
UNKNOWN_KEY: "BYMAX_CONFIG_UNKNOWN_KEY"
|
|
87
88
|
});
|
|
88
89
|
var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
|
|
89
|
-
/**
|
|
90
|
-
* Build the aggregated error from the collected issues.
|
|
91
|
-
*
|
|
92
|
-
* @param issues - Every collected violation; copied and frozen so neither the
|
|
93
|
-
* caller nor consumer code can mutate the reported list.
|
|
94
|
-
*/
|
|
95
90
|
constructor(issues) {
|
|
96
91
|
const normalized = issues.map(
|
|
97
92
|
(issue) => Object.freeze({
|
|
@@ -102,7 +97,6 @@ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error
|
|
|
102
97
|
})
|
|
103
98
|
);
|
|
104
99
|
super(formatIssueReport(normalized));
|
|
105
|
-
/** The stable top-level error code. */
|
|
106
100
|
this.code = ConfigErrorCode.VALIDATION;
|
|
107
101
|
Object.defineProperty(this, "name", {
|
|
108
102
|
value: "BymaxConfigValidationError",
|
|
@@ -119,7 +113,7 @@ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error
|
|
|
119
113
|
|
|
120
114
|
// src/source-mapping.ts
|
|
121
115
|
function toScreamingSnake(segment) {
|
|
122
|
-
return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]
|
|
116
|
+
return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/(?<=[A-Z])(?=[A-Z][a-z])/g, "_").toUpperCase();
|
|
123
117
|
}
|
|
124
118
|
function readEnvOverride(leaf) {
|
|
125
119
|
for (let node = leaf; node !== void 0; ) {
|
|
@@ -143,6 +137,33 @@ function resolveSourceNames(schema) {
|
|
|
143
137
|
}))
|
|
144
138
|
);
|
|
145
139
|
}
|
|
140
|
+
function describeUnusable(value) {
|
|
141
|
+
if (value === "") return "an empty string";
|
|
142
|
+
if (value === void 0) return "undefined";
|
|
143
|
+
if (value === null) return "null";
|
|
144
|
+
return `${typeof value === "object" ? "an" : "a"} ${typeof value}`;
|
|
145
|
+
}
|
|
146
|
+
function describeUnusableEnv(leaf) {
|
|
147
|
+
for (let node = leaf; node !== void 0; ) {
|
|
148
|
+
const meta = node.meta();
|
|
149
|
+
if (meta !== void 0 && "env" in meta) {
|
|
150
|
+
const value = meta.env;
|
|
151
|
+
if (typeof value !== "string" || value.length === 0) return describeUnusable(value);
|
|
152
|
+
}
|
|
153
|
+
node = node._zod.def.innerType ?? node._zod.def.in;
|
|
154
|
+
}
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
function findUnusableEnvOverrides(schema) {
|
|
158
|
+
const unusable = [];
|
|
159
|
+
for (const [namespace, namespaceSchema] of Object.entries(schema.shape)) {
|
|
160
|
+
for (const [leafKey, leafSchema] of Object.entries(namespaceSchema.shape)) {
|
|
161
|
+
const found = describeUnusableEnv(leafSchema);
|
|
162
|
+
if (found !== void 0) unusable.push({ path: `${namespace}.${leafKey}`, found });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return unusable;
|
|
166
|
+
}
|
|
146
167
|
function resolveNamespacePrefixes(schema) {
|
|
147
168
|
return Object.entries(schema.shape).map(([namespace, namespaceSchema]) => ({
|
|
148
169
|
namespace,
|
|
@@ -302,6 +323,18 @@ function assertDistinctVariables(bindings) {
|
|
|
302
323
|
}
|
|
303
324
|
if (issues.length > 0) throw new BymaxConfigValidationError(issues);
|
|
304
325
|
}
|
|
326
|
+
function unusableOverrideIssues(schema, bindings) {
|
|
327
|
+
const variables = new Map(bindings.map((binding) => [binding.path, binding.variable]));
|
|
328
|
+
return findUnusableEnvOverrides(schema).map(({ path, found }) => {
|
|
329
|
+
const variable = variables.get(path);
|
|
330
|
+
return {
|
|
331
|
+
path,
|
|
332
|
+
variable,
|
|
333
|
+
code: ConfigErrorCode.INVALID,
|
|
334
|
+
message: `meta({ env }) must be a non-empty string, found ${found}; the leaf would read ${variable}`
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
}
|
|
305
338
|
function validateEnv(schema, source, options) {
|
|
306
339
|
const bindings = resolveSourceNames(schema);
|
|
307
340
|
assertDistinctVariables(bindings);
|
|
@@ -311,12 +344,13 @@ function validateEnv(schema, source, options) {
|
|
|
311
344
|
const result = schema.safeParse(candidate);
|
|
312
345
|
const prefixes = resolveNamespacePrefixes(schema);
|
|
313
346
|
const unknown = options?.strict === true ? detectUnknownKeys(prefixes, bindings, sourceMap) : [];
|
|
347
|
+
const overrides = unusableOverrideIssues(schema, bindings);
|
|
314
348
|
if (result.success) {
|
|
315
|
-
if (unknown.length === 0) return result.data;
|
|
316
|
-
throw new BymaxConfigValidationError(unknown);
|
|
349
|
+
if (unknown.length === 0 && overrides.length === 0) return result.data;
|
|
350
|
+
throw new BymaxConfigValidationError([...overrides, ...unknown]);
|
|
317
351
|
}
|
|
318
352
|
const issues = translateIssues(result.error.issues, bindings, prefixes, sourceMap);
|
|
319
|
-
throw new BymaxConfigValidationError([...issues, ...unknown]);
|
|
353
|
+
throw new BymaxConfigValidationError([...overrides, ...issues, ...unknown]);
|
|
320
354
|
}
|
|
321
355
|
|
|
322
356
|
// src/config.providers.ts
|
|
@@ -334,6 +368,11 @@ async function reportIssues(hook, error) {
|
|
|
334
368
|
}
|
|
335
369
|
}
|
|
336
370
|
function createValidatedConfig(options) {
|
|
371
|
+
if ("source" in options && (options.source === void 0 || options.source === null)) {
|
|
372
|
+
throw new TypeError(
|
|
373
|
+
"BymaxConfigModule: source was provided but is undefined or null; omit the key to validate process.env, or pass the source that failed to load"
|
|
374
|
+
);
|
|
375
|
+
}
|
|
337
376
|
const source = options.source ?? process.env;
|
|
338
377
|
return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
|
|
339
378
|
}
|
|
@@ -354,35 +393,8 @@ var bymaxConfigProvider = {
|
|
|
354
393
|
};
|
|
355
394
|
var _config, _leaves;
|
|
356
395
|
exports.ConfigService = class ConfigService {
|
|
357
|
-
/**
|
|
358
|
-
* Bind the accessor to the frozen configuration object.
|
|
359
|
-
*
|
|
360
|
-
* Builds a flat `namespace.leaf` lookup once so `get`/`has` are constant-time
|
|
361
|
-
* and never rebuild a map per call. Iteration over entries (rather than dynamic
|
|
362
|
-
* bracket indexing) avoids introducing a non-literal property sink.
|
|
363
|
-
*
|
|
364
|
-
* @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
|
|
365
|
-
*/
|
|
366
396
|
constructor(config) {
|
|
367
|
-
/**
|
|
368
|
-
* The frozen configuration root.
|
|
369
|
-
*
|
|
370
|
-
* Held in an ECMAScript private field rather than a TypeScript `private`
|
|
371
|
-
* property: `private` is erased at runtime, leaving an enumerable own
|
|
372
|
-
* property that `JSON.stringify`, `Object.entries`, object spread and
|
|
373
|
-
* `util.inspect` all walk into. This object aggregates every secret the
|
|
374
|
-
* application declared, so any code that incidentally serializes the
|
|
375
|
-
* injected service — a logger that renders its arguments, an observability
|
|
376
|
-
* SDK capturing the scope of a thrown error, a stray `console.log` — would
|
|
377
|
-
* emit them in plaintext. A `#` field is unreachable to all of those.
|
|
378
|
-
*/
|
|
379
397
|
__privateAdd(this, _config);
|
|
380
|
-
/**
|
|
381
|
-
* Precomputed `namespace.leaf` to value lookup, built once at construction.
|
|
382
|
-
*
|
|
383
|
-
* An ECMAScript private field for the same reason as the config root: it
|
|
384
|
-
* holds the same values, flattened.
|
|
385
|
-
*/
|
|
386
398
|
__privateAdd(this, _leaves);
|
|
387
399
|
__privateSet(this, _config, config);
|
|
388
400
|
const leaves = /* @__PURE__ */ new Map();
|
|
@@ -393,86 +405,21 @@ exports.ConfigService = class ConfigService {
|
|
|
393
405
|
}
|
|
394
406
|
__privateSet(this, _leaves, leaves);
|
|
395
407
|
}
|
|
396
|
-
/**
|
|
397
|
-
* Describe the service for serialization without disclosing any value.
|
|
398
|
-
*
|
|
399
|
-
* `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
|
|
400
|
-
* a bug rather than as a deliberate omission. Returning the namespace names
|
|
401
|
-
* keeps the output useful for debugging while the values stay unreachable.
|
|
402
|
-
*
|
|
403
|
-
* @returns The declared namespace names, never their contents.
|
|
404
|
-
* @example
|
|
405
|
-
* ```typescript
|
|
406
|
-
* JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
408
|
toJSON() {
|
|
410
409
|
return {
|
|
411
410
|
service: "ConfigService",
|
|
412
411
|
namespaces: Object.keys(__privateGet(this, _config))
|
|
413
412
|
};
|
|
414
413
|
}
|
|
415
|
-
/**
|
|
416
|
-
* Read a leaf value by its `namespace.leaf` dot-path.
|
|
417
|
-
*
|
|
418
|
-
* The return type is inferred from the path, so `get('server.port')` is typed
|
|
419
|
-
* `number` with no cast. Never throws for a declared path: validation ran once
|
|
420
|
-
* at bootstrap. An optional leaf without a default may still resolve to
|
|
421
|
-
* `undefined` when its type includes it.
|
|
422
|
-
*
|
|
423
|
-
* @typeParam TPath - A declared path drawn from {@link Path}<TConfig>.
|
|
424
|
-
* @param path - The two-level dot-path to resolve.
|
|
425
|
-
* @returns The leaf value, typed as {@link PathValue}<TConfig, TPath>.
|
|
426
|
-
* @example
|
|
427
|
-
* ```typescript
|
|
428
|
-
* const port = config.get('server.port'); // number
|
|
429
|
-
* ```
|
|
430
|
-
*/
|
|
431
414
|
get(path) {
|
|
432
415
|
return this.resolve(path);
|
|
433
416
|
}
|
|
434
|
-
/**
|
|
435
|
-
* Return the whole validated configuration as its deep-frozen root.
|
|
436
|
-
*
|
|
437
|
-
* Yields the same frozen reference the module registered, fully typed for
|
|
438
|
-
* arbitrary depth, which is the escape hatch for nesting deeper than the
|
|
439
|
-
* two-level dot-path inference covers.
|
|
440
|
-
*
|
|
441
|
-
* @returns The frozen configuration root.
|
|
442
|
-
* @example
|
|
443
|
-
* ```typescript
|
|
444
|
-
* const everything = config.getAll(); // Readonly<AppConfig>
|
|
445
|
-
* ```
|
|
446
|
-
*/
|
|
447
417
|
getAll() {
|
|
448
418
|
return __privateGet(this, _config);
|
|
449
419
|
}
|
|
450
|
-
/**
|
|
451
|
-
* Report whether a declared path resolves to a defined value.
|
|
452
|
-
*
|
|
453
|
-
* Returns `false` when the leaf resolved to `undefined` (an optional leaf
|
|
454
|
-
* without a default), distinguishing "declared but unset" from "declared and
|
|
455
|
-
* set" without a truthiness pitfall on falsy values such as `0` or `''`.
|
|
456
|
-
*
|
|
457
|
-
* @param path - The two-level dot-path to check.
|
|
458
|
-
* @returns True when the resolved value is not `undefined`.
|
|
459
|
-
* @example
|
|
460
|
-
* ```typescript
|
|
461
|
-
* if (config.has('redis.url')) connectRedis(config.get('redis.url'));
|
|
462
|
-
* ```
|
|
463
|
-
*/
|
|
464
420
|
has(path) {
|
|
465
421
|
return this.resolve(path) !== void 0;
|
|
466
422
|
}
|
|
467
|
-
/**
|
|
468
|
-
* Resolve a two-level dot-path to its raw leaf value.
|
|
469
|
-
*
|
|
470
|
-
* Looks the path up in the precomputed `namespace.leaf` map, so an unset leaf
|
|
471
|
-
* (or, defensively, an unexpected path) yields `undefined` instead of throwing.
|
|
472
|
-
*
|
|
473
|
-
* @param path - The declared two-level dot-path.
|
|
474
|
-
* @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
|
|
475
|
-
*/
|
|
476
423
|
resolve(path) {
|
|
477
424
|
return __privateGet(this, _leaves).get(String(path));
|
|
478
425
|
}
|
|
@@ -489,35 +436,12 @@ function appendTo(list, ...items) {
|
|
|
489
436
|
return list === void 0 ? [...items] : [...list, ...items];
|
|
490
437
|
}
|
|
491
438
|
exports.BymaxConfigModule = class BymaxConfigModule extends ConfigurableModuleClass {
|
|
492
|
-
/**
|
|
493
|
-
* Register the module synchronously from a static schema.
|
|
494
|
-
*
|
|
495
|
-
* @param options - The module options plus the optional `isGlobal` extra.
|
|
496
|
-
* @returns The dynamic module augmented with the frozen config provider.
|
|
497
|
-
*/
|
|
498
439
|
static forRoot(options) {
|
|
499
440
|
return exports.BymaxConfigModule.withConfigProvider(super.forRoot(options));
|
|
500
441
|
}
|
|
501
|
-
/**
|
|
502
|
-
* Register the module asynchronously, resolving options through a factory.
|
|
503
|
-
*
|
|
504
|
-
* @param options - The async options with `useFactory` and explicit `inject`.
|
|
505
|
-
* @returns The dynamic module augmented with the frozen config provider.
|
|
506
|
-
*/
|
|
507
442
|
static forRootAsync(options) {
|
|
508
443
|
return exports.BymaxConfigModule.withConfigProvider(super.forRootAsync(options));
|
|
509
444
|
}
|
|
510
|
-
/**
|
|
511
|
-
* Append the config provider and typed accessor to a generated definition.
|
|
512
|
-
*
|
|
513
|
-
* Registers the frozen {@link BYMAX_CONFIG} provider and the {@link ConfigService}
|
|
514
|
-
* accessor (which injects that token), then exports both so consumers can
|
|
515
|
-
* inject either the raw frozen object or the typed service.
|
|
516
|
-
*
|
|
517
|
-
* @param definition - The dynamic module produced by the builder base class.
|
|
518
|
-
* @returns A definition that provides and exports {@link BYMAX_CONFIG} and
|
|
519
|
-
* {@link ConfigService}.
|
|
520
|
-
*/
|
|
521
445
|
static withConfigProvider(definition) {
|
|
522
446
|
return {
|
|
523
447
|
...definition,
|
|
@@ -541,3 +465,4 @@ exports.assertDistinctVariables = assertDistinctVariables;
|
|
|
541
465
|
exports.createValidatedConfig = createValidatedConfig;
|
|
542
466
|
exports.defineEnv = defineEnv;
|
|
543
467
|
exports.resolveSourceNames = resolveSourceNames;
|
|
468
|
+
exports.unusableOverrideIssues = unusableOverrideIssues;
|
|
@@ -557,6 +557,8 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
|
|
|
557
557
|
* @param options - The resolved module options (schema, source, strict).
|
|
558
558
|
* @returns The validated, deep-frozen configuration output.
|
|
559
559
|
* @throws {BymaxConfigValidationError} When the source fails the schema.
|
|
560
|
+
* @throws {TypeError} When `source` is present with the value `undefined` or `null`, which would
|
|
561
|
+
* otherwise validate `process.env` in place of the source that failed to load.
|
|
560
562
|
* @example
|
|
561
563
|
* ```typescript
|
|
562
564
|
* const config = createValidatedConfig({ schema: envSchema });
|
|
@@ -644,5 +646,17 @@ declare function resolveSourceNames<TShape extends EnvShape = EnvShape>(schema:
|
|
|
644
646
|
* @throws {BymaxConfigValidationError} When any variable is bound by more than one leaf.
|
|
645
647
|
*/
|
|
646
648
|
declare function assertDistinctVariables(bindings: ReadonlyArray<SourceBinding>): void;
|
|
649
|
+
/**
|
|
650
|
+
* Describe every leaf whose `meta({ env })` is present but cannot name a variable.
|
|
651
|
+
*
|
|
652
|
+
* The leaf would silently read the variable derived from its path, so it is a mistake in the
|
|
653
|
+
* schema, reported beside the violations of the source. The message names the variable the
|
|
654
|
+
* leaf reads and the kind of value `env` held, never the value.
|
|
655
|
+
*
|
|
656
|
+
* @param schema - A schema produced by `defineEnv`.
|
|
657
|
+
* @param bindings - Every leaf binding of the schema, from `resolveSourceNames`.
|
|
658
|
+
* @returns One issue per affected leaf, in schema order.
|
|
659
|
+
*/
|
|
660
|
+
declare function unusableOverrideIssues<TShape extends EnvShape>(schema: EnvSchema<TShape>, bindings: ReadonlyArray<SourceBinding>): ConfigIssue[];
|
|
647
661
|
|
|
648
|
-
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, type BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, type ConfigIssue, type ConfigIssueCode, ConfigService, type ConfigValidationCode, type EnvLeaf, type EnvNamespace, type EnvOutput, type EnvSchema, type EnvShape, type Path, type PathValue, type SourceBinding, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames };
|
|
662
|
+
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, type BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, type ConfigIssue, type ConfigIssueCode, ConfigService, type ConfigValidationCode, type EnvLeaf, type EnvNamespace, type EnvOutput, type EnvSchema, type EnvShape, type Path, type PathValue, type SourceBinding, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames, unusableOverrideIssues };
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -557,6 +557,8 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
|
|
|
557
557
|
* @param options - The resolved module options (schema, source, strict).
|
|
558
558
|
* @returns The validated, deep-frozen configuration output.
|
|
559
559
|
* @throws {BymaxConfigValidationError} When the source fails the schema.
|
|
560
|
+
* @throws {TypeError} When `source` is present with the value `undefined` or `null`, which would
|
|
561
|
+
* otherwise validate `process.env` in place of the source that failed to load.
|
|
560
562
|
* @example
|
|
561
563
|
* ```typescript
|
|
562
564
|
* const config = createValidatedConfig({ schema: envSchema });
|
|
@@ -644,5 +646,17 @@ declare function resolveSourceNames<TShape extends EnvShape = EnvShape>(schema:
|
|
|
644
646
|
* @throws {BymaxConfigValidationError} When any variable is bound by more than one leaf.
|
|
645
647
|
*/
|
|
646
648
|
declare function assertDistinctVariables(bindings: ReadonlyArray<SourceBinding>): void;
|
|
649
|
+
/**
|
|
650
|
+
* Describe every leaf whose `meta({ env })` is present but cannot name a variable.
|
|
651
|
+
*
|
|
652
|
+
* The leaf would silently read the variable derived from its path, so it is a mistake in the
|
|
653
|
+
* schema, reported beside the violations of the source. The message names the variable the
|
|
654
|
+
* leaf reads and the kind of value `env` held, never the value.
|
|
655
|
+
*
|
|
656
|
+
* @param schema - A schema produced by `defineEnv`.
|
|
657
|
+
* @param bindings - Every leaf binding of the schema, from `resolveSourceNames`.
|
|
658
|
+
* @returns One issue per affected leaf, in schema order.
|
|
659
|
+
*/
|
|
660
|
+
declare function unusableOverrideIssues<TShape extends EnvShape>(schema: EnvSchema<TShape>, bindings: ReadonlyArray<SourceBinding>): ConfigIssue[];
|
|
647
661
|
|
|
648
|
-
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, type BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, type ConfigIssue, type ConfigIssueCode, ConfigService, type ConfigValidationCode, type EnvLeaf, type EnvNamespace, type EnvOutput, type EnvSchema, type EnvShape, type Path, type PathValue, type SourceBinding, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames };
|
|
662
|
+
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, type BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, type ConfigIssue, type ConfigIssueCode, ConfigService, type ConfigValidationCode, type EnvLeaf, type EnvNamespace, type EnvOutput, type EnvSchema, type EnvShape, type Path, type PathValue, type SourceBinding, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames, unusableOverrideIssues };
|
package/dist/internal/index.mjs
CHANGED
|
@@ -51,45 +51,40 @@ var MIN_VARIABLE_COLUMN = 20;
|
|
|
51
51
|
var VARIABLE_GUTTER = 2;
|
|
52
52
|
var REPORT_HEADER = "environment validation failed";
|
|
53
53
|
var REPORT_FOOTER = "Fix the variables above and restart the process.";
|
|
54
|
+
var OVERRIDE_FOOTER = "Fix the overrides above in the test.";
|
|
54
55
|
function pluralizeIssues(count) {
|
|
55
56
|
const noun = count === 1 ? "issue" : "issues";
|
|
56
57
|
return `${count} ${noun}`;
|
|
57
58
|
}
|
|
59
|
+
function nameOf(issue) {
|
|
60
|
+
return issue.variable === "" ? issue.path : issue.variable;
|
|
61
|
+
}
|
|
58
62
|
function computeColumnWidth(issues) {
|
|
59
63
|
let longest = MIN_VARIABLE_COLUMN;
|
|
60
64
|
for (const issue of issues) {
|
|
61
|
-
longest = Math.max(longest, issue.
|
|
65
|
+
longest = Math.max(longest, nameOf(issue).length);
|
|
62
66
|
}
|
|
63
67
|
return longest + VARIABLE_GUTTER;
|
|
64
68
|
}
|
|
65
69
|
function formatIssueLine(issue, columnWidth) {
|
|
66
|
-
return `${REPORT_INDENT}${issue.
|
|
70
|
+
return `${REPORT_INDENT}${nameOf(issue).padEnd(columnWidth)}${issue.message}`;
|
|
67
71
|
}
|
|
68
72
|
function formatIssueReport(issues) {
|
|
69
73
|
const columnWidth = computeColumnWidth(issues);
|
|
70
74
|
const lines = issues.map((issue) => formatIssueLine(issue, columnWidth));
|
|
71
75
|
const header = `${REPORT_HEADER} (${pluralizeIssues(issues.length)})`;
|
|
72
|
-
|
|
76
|
+
const aboutOverrides = issues.length > 0 && issues.every((issue) => issue.variable === "");
|
|
77
|
+
return [header, "", ...lines, "", aboutOverrides ? OVERRIDE_FOOTER : REPORT_FOOTER].join("\n");
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
// src/errors.ts
|
|
76
81
|
var ConfigErrorCode = Object.freeze({
|
|
77
|
-
/** Top-level error code: one or more schema violations were collected. */
|
|
78
82
|
VALIDATION: "BYMAX_CONFIG_VALIDATION",
|
|
79
|
-
/** Issue code: a required variable is absent from the source. */
|
|
80
83
|
MISSING: "BYMAX_CONFIG_MISSING",
|
|
81
|
-
/** Issue code: a variable is present but violates its constraint. */
|
|
82
84
|
INVALID: "BYMAX_CONFIG_INVALID",
|
|
83
|
-
/** Issue code: strict mode found a source variable matching no declared leaf. */
|
|
84
85
|
UNKNOWN_KEY: "BYMAX_CONFIG_UNKNOWN_KEY"
|
|
85
86
|
});
|
|
86
87
|
var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
|
|
87
|
-
/**
|
|
88
|
-
* Build the aggregated error from the collected issues.
|
|
89
|
-
*
|
|
90
|
-
* @param issues - Every collected violation; copied and frozen so neither the
|
|
91
|
-
* caller nor consumer code can mutate the reported list.
|
|
92
|
-
*/
|
|
93
88
|
constructor(issues) {
|
|
94
89
|
const normalized = issues.map(
|
|
95
90
|
(issue) => Object.freeze({
|
|
@@ -100,7 +95,6 @@ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error
|
|
|
100
95
|
})
|
|
101
96
|
);
|
|
102
97
|
super(formatIssueReport(normalized));
|
|
103
|
-
/** The stable top-level error code. */
|
|
104
98
|
this.code = ConfigErrorCode.VALIDATION;
|
|
105
99
|
Object.defineProperty(this, "name", {
|
|
106
100
|
value: "BymaxConfigValidationError",
|
|
@@ -117,7 +111,7 @@ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error
|
|
|
117
111
|
|
|
118
112
|
// src/source-mapping.ts
|
|
119
113
|
function toScreamingSnake(segment) {
|
|
120
|
-
return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]
|
|
114
|
+
return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/(?<=[A-Z])(?=[A-Z][a-z])/g, "_").toUpperCase();
|
|
121
115
|
}
|
|
122
116
|
function readEnvOverride(leaf) {
|
|
123
117
|
for (let node = leaf; node !== void 0; ) {
|
|
@@ -141,6 +135,33 @@ function resolveSourceNames(schema) {
|
|
|
141
135
|
}))
|
|
142
136
|
);
|
|
143
137
|
}
|
|
138
|
+
function describeUnusable(value) {
|
|
139
|
+
if (value === "") return "an empty string";
|
|
140
|
+
if (value === void 0) return "undefined";
|
|
141
|
+
if (value === null) return "null";
|
|
142
|
+
return `${typeof value === "object" ? "an" : "a"} ${typeof value}`;
|
|
143
|
+
}
|
|
144
|
+
function describeUnusableEnv(leaf) {
|
|
145
|
+
for (let node = leaf; node !== void 0; ) {
|
|
146
|
+
const meta = node.meta();
|
|
147
|
+
if (meta !== void 0 && "env" in meta) {
|
|
148
|
+
const value = meta.env;
|
|
149
|
+
if (typeof value !== "string" || value.length === 0) return describeUnusable(value);
|
|
150
|
+
}
|
|
151
|
+
node = node._zod.def.innerType ?? node._zod.def.in;
|
|
152
|
+
}
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
155
|
+
function findUnusableEnvOverrides(schema) {
|
|
156
|
+
const unusable = [];
|
|
157
|
+
for (const [namespace, namespaceSchema] of Object.entries(schema.shape)) {
|
|
158
|
+
for (const [leafKey, leafSchema] of Object.entries(namespaceSchema.shape)) {
|
|
159
|
+
const found = describeUnusableEnv(leafSchema);
|
|
160
|
+
if (found !== void 0) unusable.push({ path: `${namespace}.${leafKey}`, found });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return unusable;
|
|
164
|
+
}
|
|
144
165
|
function resolveNamespacePrefixes(schema) {
|
|
145
166
|
return Object.entries(schema.shape).map(([namespace, namespaceSchema]) => ({
|
|
146
167
|
namespace,
|
|
@@ -300,6 +321,18 @@ function assertDistinctVariables(bindings) {
|
|
|
300
321
|
}
|
|
301
322
|
if (issues.length > 0) throw new BymaxConfigValidationError(issues);
|
|
302
323
|
}
|
|
324
|
+
function unusableOverrideIssues(schema, bindings) {
|
|
325
|
+
const variables = new Map(bindings.map((binding) => [binding.path, binding.variable]));
|
|
326
|
+
return findUnusableEnvOverrides(schema).map(({ path, found }) => {
|
|
327
|
+
const variable = variables.get(path);
|
|
328
|
+
return {
|
|
329
|
+
path,
|
|
330
|
+
variable,
|
|
331
|
+
code: ConfigErrorCode.INVALID,
|
|
332
|
+
message: `meta({ env }) must be a non-empty string, found ${found}; the leaf would read ${variable}`
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
}
|
|
303
336
|
function validateEnv(schema, source, options) {
|
|
304
337
|
const bindings = resolveSourceNames(schema);
|
|
305
338
|
assertDistinctVariables(bindings);
|
|
@@ -309,12 +342,13 @@ function validateEnv(schema, source, options) {
|
|
|
309
342
|
const result = schema.safeParse(candidate);
|
|
310
343
|
const prefixes = resolveNamespacePrefixes(schema);
|
|
311
344
|
const unknown = options?.strict === true ? detectUnknownKeys(prefixes, bindings, sourceMap) : [];
|
|
345
|
+
const overrides = unusableOverrideIssues(schema, bindings);
|
|
312
346
|
if (result.success) {
|
|
313
|
-
if (unknown.length === 0) return result.data;
|
|
314
|
-
throw new BymaxConfigValidationError(unknown);
|
|
347
|
+
if (unknown.length === 0 && overrides.length === 0) return result.data;
|
|
348
|
+
throw new BymaxConfigValidationError([...overrides, ...unknown]);
|
|
315
349
|
}
|
|
316
350
|
const issues = translateIssues(result.error.issues, bindings, prefixes, sourceMap);
|
|
317
|
-
throw new BymaxConfigValidationError([...issues, ...unknown]);
|
|
351
|
+
throw new BymaxConfigValidationError([...overrides, ...issues, ...unknown]);
|
|
318
352
|
}
|
|
319
353
|
|
|
320
354
|
// src/config.providers.ts
|
|
@@ -332,6 +366,11 @@ async function reportIssues(hook, error) {
|
|
|
332
366
|
}
|
|
333
367
|
}
|
|
334
368
|
function createValidatedConfig(options) {
|
|
369
|
+
if ("source" in options && (options.source === void 0 || options.source === null)) {
|
|
370
|
+
throw new TypeError(
|
|
371
|
+
"BymaxConfigModule: source was provided but is undefined or null; omit the key to validate process.env, or pass the source that failed to load"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
335
374
|
const source = options.source ?? process.env;
|
|
336
375
|
return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
|
|
337
376
|
}
|
|
@@ -352,35 +391,8 @@ var bymaxConfigProvider = {
|
|
|
352
391
|
};
|
|
353
392
|
var _config, _leaves;
|
|
354
393
|
var ConfigService = class {
|
|
355
|
-
/**
|
|
356
|
-
* Bind the accessor to the frozen configuration object.
|
|
357
|
-
*
|
|
358
|
-
* Builds a flat `namespace.leaf` lookup once so `get`/`has` are constant-time
|
|
359
|
-
* and never rebuild a map per call. Iteration over entries (rather than dynamic
|
|
360
|
-
* bracket indexing) avoids introducing a non-literal property sink.
|
|
361
|
-
*
|
|
362
|
-
* @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
|
|
363
|
-
*/
|
|
364
394
|
constructor(config) {
|
|
365
|
-
/**
|
|
366
|
-
* The frozen configuration root.
|
|
367
|
-
*
|
|
368
|
-
* Held in an ECMAScript private field rather than a TypeScript `private`
|
|
369
|
-
* property: `private` is erased at runtime, leaving an enumerable own
|
|
370
|
-
* property that `JSON.stringify`, `Object.entries`, object spread and
|
|
371
|
-
* `util.inspect` all walk into. This object aggregates every secret the
|
|
372
|
-
* application declared, so any code that incidentally serializes the
|
|
373
|
-
* injected service — a logger that renders its arguments, an observability
|
|
374
|
-
* SDK capturing the scope of a thrown error, a stray `console.log` — would
|
|
375
|
-
* emit them in plaintext. A `#` field is unreachable to all of those.
|
|
376
|
-
*/
|
|
377
395
|
__privateAdd(this, _config);
|
|
378
|
-
/**
|
|
379
|
-
* Precomputed `namespace.leaf` to value lookup, built once at construction.
|
|
380
|
-
*
|
|
381
|
-
* An ECMAScript private field for the same reason as the config root: it
|
|
382
|
-
* holds the same values, flattened.
|
|
383
|
-
*/
|
|
384
396
|
__privateAdd(this, _leaves);
|
|
385
397
|
__privateSet(this, _config, config);
|
|
386
398
|
const leaves = /* @__PURE__ */ new Map();
|
|
@@ -391,86 +403,21 @@ var ConfigService = class {
|
|
|
391
403
|
}
|
|
392
404
|
__privateSet(this, _leaves, leaves);
|
|
393
405
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Describe the service for serialization without disclosing any value.
|
|
396
|
-
*
|
|
397
|
-
* `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
|
|
398
|
-
* a bug rather than as a deliberate omission. Returning the namespace names
|
|
399
|
-
* keeps the output useful for debugging while the values stay unreachable.
|
|
400
|
-
*
|
|
401
|
-
* @returns The declared namespace names, never their contents.
|
|
402
|
-
* @example
|
|
403
|
-
* ```typescript
|
|
404
|
-
* JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
|
|
405
|
-
* ```
|
|
406
|
-
*/
|
|
407
406
|
toJSON() {
|
|
408
407
|
return {
|
|
409
408
|
service: "ConfigService",
|
|
410
409
|
namespaces: Object.keys(__privateGet(this, _config))
|
|
411
410
|
};
|
|
412
411
|
}
|
|
413
|
-
/**
|
|
414
|
-
* Read a leaf value by its `namespace.leaf` dot-path.
|
|
415
|
-
*
|
|
416
|
-
* The return type is inferred from the path, so `get('server.port')` is typed
|
|
417
|
-
* `number` with no cast. Never throws for a declared path: validation ran once
|
|
418
|
-
* at bootstrap. An optional leaf without a default may still resolve to
|
|
419
|
-
* `undefined` when its type includes it.
|
|
420
|
-
*
|
|
421
|
-
* @typeParam TPath - A declared path drawn from {@link Path}<TConfig>.
|
|
422
|
-
* @param path - The two-level dot-path to resolve.
|
|
423
|
-
* @returns The leaf value, typed as {@link PathValue}<TConfig, TPath>.
|
|
424
|
-
* @example
|
|
425
|
-
* ```typescript
|
|
426
|
-
* const port = config.get('server.port'); // number
|
|
427
|
-
* ```
|
|
428
|
-
*/
|
|
429
412
|
get(path) {
|
|
430
413
|
return this.resolve(path);
|
|
431
414
|
}
|
|
432
|
-
/**
|
|
433
|
-
* Return the whole validated configuration as its deep-frozen root.
|
|
434
|
-
*
|
|
435
|
-
* Yields the same frozen reference the module registered, fully typed for
|
|
436
|
-
* arbitrary depth, which is the escape hatch for nesting deeper than the
|
|
437
|
-
* two-level dot-path inference covers.
|
|
438
|
-
*
|
|
439
|
-
* @returns The frozen configuration root.
|
|
440
|
-
* @example
|
|
441
|
-
* ```typescript
|
|
442
|
-
* const everything = config.getAll(); // Readonly<AppConfig>
|
|
443
|
-
* ```
|
|
444
|
-
*/
|
|
445
415
|
getAll() {
|
|
446
416
|
return __privateGet(this, _config);
|
|
447
417
|
}
|
|
448
|
-
/**
|
|
449
|
-
* Report whether a declared path resolves to a defined value.
|
|
450
|
-
*
|
|
451
|
-
* Returns `false` when the leaf resolved to `undefined` (an optional leaf
|
|
452
|
-
* without a default), distinguishing "declared but unset" from "declared and
|
|
453
|
-
* set" without a truthiness pitfall on falsy values such as `0` or `''`.
|
|
454
|
-
*
|
|
455
|
-
* @param path - The two-level dot-path to check.
|
|
456
|
-
* @returns True when the resolved value is not `undefined`.
|
|
457
|
-
* @example
|
|
458
|
-
* ```typescript
|
|
459
|
-
* if (config.has('redis.url')) connectRedis(config.get('redis.url'));
|
|
460
|
-
* ```
|
|
461
|
-
*/
|
|
462
418
|
has(path) {
|
|
463
419
|
return this.resolve(path) !== void 0;
|
|
464
420
|
}
|
|
465
|
-
/**
|
|
466
|
-
* Resolve a two-level dot-path to its raw leaf value.
|
|
467
|
-
*
|
|
468
|
-
* Looks the path up in the precomputed `namespace.leaf` map, so an unset leaf
|
|
469
|
-
* (or, defensively, an unexpected path) yields `undefined` instead of throwing.
|
|
470
|
-
*
|
|
471
|
-
* @param path - The declared two-level dot-path.
|
|
472
|
-
* @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
|
|
473
|
-
*/
|
|
474
421
|
resolve(path) {
|
|
475
422
|
return __privateGet(this, _leaves).get(String(path));
|
|
476
423
|
}
|
|
@@ -487,35 +434,12 @@ function appendTo(list, ...items) {
|
|
|
487
434
|
return list === void 0 ? [...items] : [...list, ...items];
|
|
488
435
|
}
|
|
489
436
|
var BymaxConfigModule = class extends ConfigurableModuleClass {
|
|
490
|
-
/**
|
|
491
|
-
* Register the module synchronously from a static schema.
|
|
492
|
-
*
|
|
493
|
-
* @param options - The module options plus the optional `isGlobal` extra.
|
|
494
|
-
* @returns The dynamic module augmented with the frozen config provider.
|
|
495
|
-
*/
|
|
496
437
|
static forRoot(options) {
|
|
497
438
|
return BymaxConfigModule.withConfigProvider(super.forRoot(options));
|
|
498
439
|
}
|
|
499
|
-
/**
|
|
500
|
-
* Register the module asynchronously, resolving options through a factory.
|
|
501
|
-
*
|
|
502
|
-
* @param options - The async options with `useFactory` and explicit `inject`.
|
|
503
|
-
* @returns The dynamic module augmented with the frozen config provider.
|
|
504
|
-
*/
|
|
505
440
|
static forRootAsync(options) {
|
|
506
441
|
return BymaxConfigModule.withConfigProvider(super.forRootAsync(options));
|
|
507
442
|
}
|
|
508
|
-
/**
|
|
509
|
-
* Append the config provider and typed accessor to a generated definition.
|
|
510
|
-
*
|
|
511
|
-
* Registers the frozen {@link BYMAX_CONFIG} provider and the {@link ConfigService}
|
|
512
|
-
* accessor (which injects that token), then exports both so consumers can
|
|
513
|
-
* inject either the raw frozen object or the typed service.
|
|
514
|
-
*
|
|
515
|
-
* @param definition - The dynamic module produced by the builder base class.
|
|
516
|
-
* @returns A definition that provides and exports {@link BYMAX_CONFIG} and
|
|
517
|
-
* {@link ConfigService}.
|
|
518
|
-
*/
|
|
519
443
|
static withConfigProvider(definition) {
|
|
520
444
|
return {
|
|
521
445
|
...definition,
|
|
@@ -531,4 +455,4 @@ function defineEnv(shape) {
|
|
|
531
455
|
return z.object(shape);
|
|
532
456
|
}
|
|
533
457
|
|
|
534
|
-
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigValidationError, ConfigErrorCode, ConfigService, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames };
|
|
458
|
+
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigValidationError, ConfigErrorCode, ConfigService, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames, unusableOverrideIssues };
|
package/dist/testing/index.cjs
CHANGED
|
@@ -167,19 +167,61 @@ function synthesizePlaceholderSource(schema) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// src/testing/create-test-config.ts
|
|
170
|
-
function
|
|
170
|
+
function isPlainObject(value) {
|
|
171
|
+
if (typeof value !== "object" || value === null) return false;
|
|
172
|
+
const prototype = Object.getPrototypeOf(value);
|
|
173
|
+
return prototype === Object.prototype || prototype === null;
|
|
174
|
+
}
|
|
175
|
+
function unplacedIssue(path, code, message) {
|
|
176
|
+
return { path, variable: "", code, message };
|
|
177
|
+
}
|
|
178
|
+
function flattenOverrides(overrides, declared) {
|
|
171
179
|
const flat = /* @__PURE__ */ new Map();
|
|
180
|
+
const unplaced = [];
|
|
181
|
+
if (overrides === void 0 || overrides === null) return { flat, unplaced };
|
|
182
|
+
if (!isPlainObject(overrides)) {
|
|
183
|
+
unplaced.push(
|
|
184
|
+
unplacedIssue("(overrides)", internal.ConfigErrorCode.INVALID, "the overrides must be an object")
|
|
185
|
+
);
|
|
186
|
+
return { flat, unplaced };
|
|
187
|
+
}
|
|
172
188
|
for (const [namespace, leaves] of Object.entries(overrides)) {
|
|
189
|
+
if (!isPlainObject(leaves)) {
|
|
190
|
+
unplaced.push(
|
|
191
|
+
unplacedIssue(
|
|
192
|
+
namespace,
|
|
193
|
+
internal.ConfigErrorCode.INVALID,
|
|
194
|
+
`the override for \`${namespace}\` must be an object`
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
173
199
|
for (const [leaf, value] of Object.entries(leaves)) {
|
|
174
|
-
|
|
200
|
+
const path = `${namespace}.${leaf}`;
|
|
201
|
+
if (declared.has(path)) {
|
|
202
|
+
flat.set(path, value);
|
|
203
|
+
} else {
|
|
204
|
+
unplaced.push(
|
|
205
|
+
unplacedIssue(
|
|
206
|
+
path,
|
|
207
|
+
internal.ConfigErrorCode.UNKNOWN_KEY,
|
|
208
|
+
"no leaf of the schema is at this path; the override was not applied"
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
|
-
return flat;
|
|
214
|
+
return { flat, unplaced };
|
|
178
215
|
}
|
|
179
216
|
function applyOverrides(schema, base, overrides, overridden) {
|
|
180
|
-
const
|
|
217
|
+
const bindings = internal.resolveSourceNames(schema);
|
|
218
|
+
const { flat, unplaced } = flattenOverrides(
|
|
219
|
+
overrides,
|
|
220
|
+
new Set(bindings.map((binding) => binding.path))
|
|
221
|
+
);
|
|
222
|
+
if (unplaced.length > 0) throw new internal.BymaxConfigValidationError(unplaced);
|
|
181
223
|
const entries = new Map(Object.entries(base));
|
|
182
|
-
for (const binding of
|
|
224
|
+
for (const binding of bindings) {
|
|
183
225
|
const value = flat.get(binding.path);
|
|
184
226
|
if (value === void 0) continue;
|
|
185
227
|
entries.set(binding.variable, String(value));
|
|
@@ -200,7 +242,10 @@ function explainUnsatisfiedLeaves(error, overridden) {
|
|
|
200
242
|
);
|
|
201
243
|
}
|
|
202
244
|
function prepareTestConfig(schema, overrides) {
|
|
203
|
-
internal.
|
|
245
|
+
const bindings = internal.resolveSourceNames(schema);
|
|
246
|
+
internal.assertDistinctVariables(bindings);
|
|
247
|
+
const unusable = internal.unusableOverrideIssues(schema, bindings);
|
|
248
|
+
if (unusable.length > 0) throw new internal.BymaxConfigValidationError(unusable);
|
|
204
249
|
const overridden = /* @__PURE__ */ new Set();
|
|
205
250
|
const source = applyOverrides(schema, synthesizePlaceholderSource(schema), overrides, overridden);
|
|
206
251
|
try {
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BymaxConfigModule, assertDistinctVariables,
|
|
1
|
+
import { BymaxConfigModule, resolveSourceNames, assertDistinctVariables, unusableOverrideIssues, BymaxConfigValidationError, createValidatedConfig, ConfigErrorCode } from '@bymax-one/nest-config/internal';
|
|
2
2
|
|
|
3
3
|
// src/testing/create-test-config.ts
|
|
4
4
|
var FILLER_CHARACTER = "a";
|
|
@@ -165,19 +165,61 @@ function synthesizePlaceholderSource(schema) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// src/testing/create-test-config.ts
|
|
168
|
-
function
|
|
168
|
+
function isPlainObject(value) {
|
|
169
|
+
if (typeof value !== "object" || value === null) return false;
|
|
170
|
+
const prototype = Object.getPrototypeOf(value);
|
|
171
|
+
return prototype === Object.prototype || prototype === null;
|
|
172
|
+
}
|
|
173
|
+
function unplacedIssue(path, code, message) {
|
|
174
|
+
return { path, variable: "", code, message };
|
|
175
|
+
}
|
|
176
|
+
function flattenOverrides(overrides, declared) {
|
|
169
177
|
const flat = /* @__PURE__ */ new Map();
|
|
178
|
+
const unplaced = [];
|
|
179
|
+
if (overrides === void 0 || overrides === null) return { flat, unplaced };
|
|
180
|
+
if (!isPlainObject(overrides)) {
|
|
181
|
+
unplaced.push(
|
|
182
|
+
unplacedIssue("(overrides)", ConfigErrorCode.INVALID, "the overrides must be an object")
|
|
183
|
+
);
|
|
184
|
+
return { flat, unplaced };
|
|
185
|
+
}
|
|
170
186
|
for (const [namespace, leaves] of Object.entries(overrides)) {
|
|
187
|
+
if (!isPlainObject(leaves)) {
|
|
188
|
+
unplaced.push(
|
|
189
|
+
unplacedIssue(
|
|
190
|
+
namespace,
|
|
191
|
+
ConfigErrorCode.INVALID,
|
|
192
|
+
`the override for \`${namespace}\` must be an object`
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
171
197
|
for (const [leaf, value] of Object.entries(leaves)) {
|
|
172
|
-
|
|
198
|
+
const path = `${namespace}.${leaf}`;
|
|
199
|
+
if (declared.has(path)) {
|
|
200
|
+
flat.set(path, value);
|
|
201
|
+
} else {
|
|
202
|
+
unplaced.push(
|
|
203
|
+
unplacedIssue(
|
|
204
|
+
path,
|
|
205
|
+
ConfigErrorCode.UNKNOWN_KEY,
|
|
206
|
+
"no leaf of the schema is at this path; the override was not applied"
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
173
210
|
}
|
|
174
211
|
}
|
|
175
|
-
return flat;
|
|
212
|
+
return { flat, unplaced };
|
|
176
213
|
}
|
|
177
214
|
function applyOverrides(schema, base, overrides, overridden) {
|
|
178
|
-
const
|
|
215
|
+
const bindings = resolveSourceNames(schema);
|
|
216
|
+
const { flat, unplaced } = flattenOverrides(
|
|
217
|
+
overrides,
|
|
218
|
+
new Set(bindings.map((binding) => binding.path))
|
|
219
|
+
);
|
|
220
|
+
if (unplaced.length > 0) throw new BymaxConfigValidationError(unplaced);
|
|
179
221
|
const entries = new Map(Object.entries(base));
|
|
180
|
-
for (const binding of
|
|
222
|
+
for (const binding of bindings) {
|
|
181
223
|
const value = flat.get(binding.path);
|
|
182
224
|
if (value === void 0) continue;
|
|
183
225
|
entries.set(binding.variable, String(value));
|
|
@@ -198,7 +240,10 @@ function explainUnsatisfiedLeaves(error, overridden) {
|
|
|
198
240
|
);
|
|
199
241
|
}
|
|
200
242
|
function prepareTestConfig(schema, overrides) {
|
|
201
|
-
|
|
243
|
+
const bindings = resolveSourceNames(schema);
|
|
244
|
+
assertDistinctVariables(bindings);
|
|
245
|
+
const unusable = unusableOverrideIssues(schema, bindings);
|
|
246
|
+
if (unusable.length > 0) throw new BymaxConfigValidationError(unusable);
|
|
202
247
|
const overridden = /* @__PURE__ */ new Set();
|
|
203
248
|
const source = applyOverrides(schema, synthesizePlaceholderSource(schema), overrides, overridden);
|
|
204
249
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-config",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "Typed and validated environment configuration entry point for NestJS 11 applications, built on Zod v4.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"./package.json": "./package.json"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
|
-
"build": "pnpm clean && tsup",
|
|
57
|
+
"build": "pnpm clean && tsup && node scripts/strip-bundle-comments.mjs",
|
|
58
|
+
"check:api": "node scripts/check-api-surface.mjs",
|
|
58
59
|
"check:exports": "attw --pack . --profile strict",
|
|
59
60
|
"check:lint-graph": "node scripts/check-lint-graph-rules.mjs",
|
|
60
61
|
"check:mutants": "node scripts/check-mutation-directives.mjs",
|