@bymax-one/nest-config 1.2.4 → 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 CHANGED
@@ -11,6 +11,35 @@ 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
+
14
43
  ## [1.2.4] - 2026-11-01
15
44
 
16
45
  ### Fixed
@@ -556,4 +585,5 @@ have regressed from. They are kept because the reasoning is worth having.
556
585
  [1.2.2]: https://github.com/bymaxone/nest-config/compare/v1.2.1...v1.2.2
557
586
  [1.2.3]: https://github.com/bymaxone/nest-config/compare/v1.2.2...v1.2.3
558
587
  [1.2.4]: https://github.com/bymaxone/nest-config/compare/v1.2.3...v1.2.4
559
- [Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.2.4...HEAD
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
@@ -913,7 +913,8 @@ Overrides mirror the `namespace.leaf` shape with every key optional and typed fr
913
913
  a leaf what its environment variable would hold; an object becomes `'[object Object]'`, and an override
914
914
  of `undefined` is ignored, so a defaulted leaf cannot be unset. An override whose path matches no leaf
915
915
  of the schema throws, listing every such path, even when its value is `undefined`; a namespace given
916
- something other than an object is reported once, by name. Neither helper takes `strict`, a
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
917
918
  custom `source`, a hook or `isGlobal`: `configTestingModule` registers the module globally, and a
918
919
  failure case (a missing variable, a strict violation) is built with
919
920
  `BymaxConfigModule.forRoot({ schema, source, strict: true })` and a source you write.
@@ -81,22 +81,12 @@ function formatIssueReport(issues) {
81
81
 
82
82
  // src/errors.ts
83
83
  var ConfigErrorCode = Object.freeze({
84
- /** Top-level error code: one or more schema violations were collected. */
85
84
  VALIDATION: "BYMAX_CONFIG_VALIDATION",
86
- /** Issue code: a required variable is absent from the source. */
87
85
  MISSING: "BYMAX_CONFIG_MISSING",
88
- /** Issue code: a variable is present but violates its constraint. */
89
86
  INVALID: "BYMAX_CONFIG_INVALID",
90
- /** Issue code: strict mode found a source variable matching no declared leaf. */
91
87
  UNKNOWN_KEY: "BYMAX_CONFIG_UNKNOWN_KEY"
92
88
  });
93
89
  var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
94
- /**
95
- * Build the aggregated error from the collected issues.
96
- *
97
- * @param issues - Every collected violation; copied and frozen so neither the
98
- * caller nor consumer code can mutate the reported list.
99
- */
100
90
  constructor(issues) {
101
91
  const normalized = issues.map(
102
92
  (issue) => Object.freeze({
@@ -107,7 +97,6 @@ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error
107
97
  })
108
98
  );
109
99
  super(formatIssueReport(normalized));
110
- /** The stable top-level error code. */
111
100
  this.code = ConfigErrorCode.VALIDATION;
112
101
  Object.defineProperty(this, "name", {
113
102
  value: "BymaxConfigValidationError",
@@ -404,35 +393,8 @@ var bymaxConfigProvider = {
404
393
  };
405
394
  var _config, _leaves;
406
395
  exports.ConfigService = class ConfigService {
407
- /**
408
- * Bind the accessor to the frozen configuration object.
409
- *
410
- * Builds a flat `namespace.leaf` lookup once so `get`/`has` are constant-time
411
- * and never rebuild a map per call. Iteration over entries (rather than dynamic
412
- * bracket indexing) avoids introducing a non-literal property sink.
413
- *
414
- * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
415
- */
416
396
  constructor(config) {
417
- /**
418
- * The frozen configuration root.
419
- *
420
- * Held in an ECMAScript private field rather than a TypeScript `private`
421
- * property: `private` is erased at runtime, leaving an enumerable own
422
- * property that `JSON.stringify`, `Object.entries`, object spread and
423
- * `util.inspect` all walk into. This object aggregates every secret the
424
- * application declared, so any code that incidentally serializes the
425
- * injected service — a logger that renders its arguments, an observability
426
- * SDK capturing the scope of a thrown error, a stray `console.log` — would
427
- * emit them in plaintext. A `#` field is unreachable to all of those.
428
- */
429
397
  __privateAdd(this, _config);
430
- /**
431
- * Precomputed `namespace.leaf` to value lookup, built once at construction.
432
- *
433
- * An ECMAScript private field for the same reason as the config root: it
434
- * holds the same values, flattened.
435
- */
436
398
  __privateAdd(this, _leaves);
437
399
  __privateSet(this, _config, config);
438
400
  const leaves = /* @__PURE__ */ new Map();
@@ -443,86 +405,21 @@ exports.ConfigService = class ConfigService {
443
405
  }
444
406
  __privateSet(this, _leaves, leaves);
445
407
  }
446
- /**
447
- * Describe the service for serialization without disclosing any value.
448
- *
449
- * `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
450
- * a bug rather than as a deliberate omission. Returning the namespace names
451
- * keeps the output useful for debugging while the values stay unreachable.
452
- *
453
- * @returns The declared namespace names, never their contents.
454
- * @example
455
- * ```typescript
456
- * JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
457
- * ```
458
- */
459
408
  toJSON() {
460
409
  return {
461
410
  service: "ConfigService",
462
411
  namespaces: Object.keys(__privateGet(this, _config))
463
412
  };
464
413
  }
465
- /**
466
- * Read a leaf value by its `namespace.leaf` dot-path.
467
- *
468
- * The return type is inferred from the path, so `get('server.port')` is typed
469
- * `number` with no cast. Never throws for a declared path: validation ran once
470
- * at bootstrap. An optional leaf without a default may still resolve to
471
- * `undefined` when its type includes it.
472
- *
473
- * @typeParam TPath - A declared path drawn from {@link Path}<TConfig>.
474
- * @param path - The two-level dot-path to resolve.
475
- * @returns The leaf value, typed as {@link PathValue}<TConfig, TPath>.
476
- * @example
477
- * ```typescript
478
- * const port = config.get('server.port'); // number
479
- * ```
480
- */
481
414
  get(path) {
482
415
  return this.resolve(path);
483
416
  }
484
- /**
485
- * Return the whole validated configuration as its deep-frozen root.
486
- *
487
- * Yields the same frozen reference the module registered, fully typed for
488
- * arbitrary depth, which is the escape hatch for nesting deeper than the
489
- * two-level dot-path inference covers.
490
- *
491
- * @returns The frozen configuration root.
492
- * @example
493
- * ```typescript
494
- * const everything = config.getAll(); // Readonly<AppConfig>
495
- * ```
496
- */
497
417
  getAll() {
498
418
  return __privateGet(this, _config);
499
419
  }
500
- /**
501
- * Report whether a declared path resolves to a defined value.
502
- *
503
- * Returns `false` when the leaf resolved to `undefined` (an optional leaf
504
- * without a default), distinguishing "declared but unset" from "declared and
505
- * set" without a truthiness pitfall on falsy values such as `0` or `''`.
506
- *
507
- * @param path - The two-level dot-path to check.
508
- * @returns True when the resolved value is not `undefined`.
509
- * @example
510
- * ```typescript
511
- * if (config.has('redis.url')) connectRedis(config.get('redis.url'));
512
- * ```
513
- */
514
420
  has(path) {
515
421
  return this.resolve(path) !== void 0;
516
422
  }
517
- /**
518
- * Resolve a two-level dot-path to its raw leaf value.
519
- *
520
- * Looks the path up in the precomputed `namespace.leaf` map, so an unset leaf
521
- * (or, defensively, an unexpected path) yields `undefined` instead of throwing.
522
- *
523
- * @param path - The declared two-level dot-path.
524
- * @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
525
- */
526
423
  resolve(path) {
527
424
  return __privateGet(this, _leaves).get(String(path));
528
425
  }
@@ -539,35 +436,12 @@ function appendTo(list, ...items) {
539
436
  return list === void 0 ? [...items] : [...list, ...items];
540
437
  }
541
438
  exports.BymaxConfigModule = class BymaxConfigModule extends ConfigurableModuleClass {
542
- /**
543
- * Register the module synchronously from a static schema.
544
- *
545
- * @param options - The module options plus the optional `isGlobal` extra.
546
- * @returns The dynamic module augmented with the frozen config provider.
547
- */
548
439
  static forRoot(options) {
549
440
  return exports.BymaxConfigModule.withConfigProvider(super.forRoot(options));
550
441
  }
551
- /**
552
- * Register the module asynchronously, resolving options through a factory.
553
- *
554
- * @param options - The async options with `useFactory` and explicit `inject`.
555
- * @returns The dynamic module augmented with the frozen config provider.
556
- */
557
442
  static forRootAsync(options) {
558
443
  return exports.BymaxConfigModule.withConfigProvider(super.forRootAsync(options));
559
444
  }
560
- /**
561
- * Append the config provider and typed accessor to a generated definition.
562
- *
563
- * Registers the frozen {@link BYMAX_CONFIG} provider and the {@link ConfigService}
564
- * accessor (which injects that token), then exports both so consumers can
565
- * inject either the raw frozen object or the typed service.
566
- *
567
- * @param definition - The dynamic module produced by the builder base class.
568
- * @returns A definition that provides and exports {@link BYMAX_CONFIG} and
569
- * {@link ConfigService}.
570
- */
571
445
  static withConfigProvider(definition) {
572
446
  return {
573
447
  ...definition,
@@ -79,22 +79,12 @@ function formatIssueReport(issues) {
79
79
 
80
80
  // src/errors.ts
81
81
  var ConfigErrorCode = Object.freeze({
82
- /** Top-level error code: one or more schema violations were collected. */
83
82
  VALIDATION: "BYMAX_CONFIG_VALIDATION",
84
- /** Issue code: a required variable is absent from the source. */
85
83
  MISSING: "BYMAX_CONFIG_MISSING",
86
- /** Issue code: a variable is present but violates its constraint. */
87
84
  INVALID: "BYMAX_CONFIG_INVALID",
88
- /** Issue code: strict mode found a source variable matching no declared leaf. */
89
85
  UNKNOWN_KEY: "BYMAX_CONFIG_UNKNOWN_KEY"
90
86
  });
91
87
  var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
92
- /**
93
- * Build the aggregated error from the collected issues.
94
- *
95
- * @param issues - Every collected violation; copied and frozen so neither the
96
- * caller nor consumer code can mutate the reported list.
97
- */
98
88
  constructor(issues) {
99
89
  const normalized = issues.map(
100
90
  (issue) => Object.freeze({
@@ -105,7 +95,6 @@ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error
105
95
  })
106
96
  );
107
97
  super(formatIssueReport(normalized));
108
- /** The stable top-level error code. */
109
98
  this.code = ConfigErrorCode.VALIDATION;
110
99
  Object.defineProperty(this, "name", {
111
100
  value: "BymaxConfigValidationError",
@@ -402,35 +391,8 @@ var bymaxConfigProvider = {
402
391
  };
403
392
  var _config, _leaves;
404
393
  var ConfigService = class {
405
- /**
406
- * Bind the accessor to the frozen configuration object.
407
- *
408
- * Builds a flat `namespace.leaf` lookup once so `get`/`has` are constant-time
409
- * and never rebuild a map per call. Iteration over entries (rather than dynamic
410
- * bracket indexing) avoids introducing a non-literal property sink.
411
- *
412
- * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
413
- */
414
394
  constructor(config) {
415
- /**
416
- * The frozen configuration root.
417
- *
418
- * Held in an ECMAScript private field rather than a TypeScript `private`
419
- * property: `private` is erased at runtime, leaving an enumerable own
420
- * property that `JSON.stringify`, `Object.entries`, object spread and
421
- * `util.inspect` all walk into. This object aggregates every secret the
422
- * application declared, so any code that incidentally serializes the
423
- * injected service — a logger that renders its arguments, an observability
424
- * SDK capturing the scope of a thrown error, a stray `console.log` — would
425
- * emit them in plaintext. A `#` field is unreachable to all of those.
426
- */
427
395
  __privateAdd(this, _config);
428
- /**
429
- * Precomputed `namespace.leaf` to value lookup, built once at construction.
430
- *
431
- * An ECMAScript private field for the same reason as the config root: it
432
- * holds the same values, flattened.
433
- */
434
396
  __privateAdd(this, _leaves);
435
397
  __privateSet(this, _config, config);
436
398
  const leaves = /* @__PURE__ */ new Map();
@@ -441,86 +403,21 @@ var ConfigService = class {
441
403
  }
442
404
  __privateSet(this, _leaves, leaves);
443
405
  }
444
- /**
445
- * Describe the service for serialization without disclosing any value.
446
- *
447
- * `JSON.stringify` on an instance would otherwise yield `{}`, which reads as
448
- * a bug rather than as a deliberate omission. Returning the namespace names
449
- * keeps the output useful for debugging while the values stay unreachable.
450
- *
451
- * @returns The declared namespace names, never their contents.
452
- * @example
453
- * ```typescript
454
- * JSON.stringify(config); // {"service":"ConfigService","namespaces":["server","database"]}
455
- * ```
456
- */
457
406
  toJSON() {
458
407
  return {
459
408
  service: "ConfigService",
460
409
  namespaces: Object.keys(__privateGet(this, _config))
461
410
  };
462
411
  }
463
- /**
464
- * Read a leaf value by its `namespace.leaf` dot-path.
465
- *
466
- * The return type is inferred from the path, so `get('server.port')` is typed
467
- * `number` with no cast. Never throws for a declared path: validation ran once
468
- * at bootstrap. An optional leaf without a default may still resolve to
469
- * `undefined` when its type includes it.
470
- *
471
- * @typeParam TPath - A declared path drawn from {@link Path}<TConfig>.
472
- * @param path - The two-level dot-path to resolve.
473
- * @returns The leaf value, typed as {@link PathValue}<TConfig, TPath>.
474
- * @example
475
- * ```typescript
476
- * const port = config.get('server.port'); // number
477
- * ```
478
- */
479
412
  get(path) {
480
413
  return this.resolve(path);
481
414
  }
482
- /**
483
- * Return the whole validated configuration as its deep-frozen root.
484
- *
485
- * Yields the same frozen reference the module registered, fully typed for
486
- * arbitrary depth, which is the escape hatch for nesting deeper than the
487
- * two-level dot-path inference covers.
488
- *
489
- * @returns The frozen configuration root.
490
- * @example
491
- * ```typescript
492
- * const everything = config.getAll(); // Readonly<AppConfig>
493
- * ```
494
- */
495
415
  getAll() {
496
416
  return __privateGet(this, _config);
497
417
  }
498
- /**
499
- * Report whether a declared path resolves to a defined value.
500
- *
501
- * Returns `false` when the leaf resolved to `undefined` (an optional leaf
502
- * without a default), distinguishing "declared but unset" from "declared and
503
- * set" without a truthiness pitfall on falsy values such as `0` or `''`.
504
- *
505
- * @param path - The two-level dot-path to check.
506
- * @returns True when the resolved value is not `undefined`.
507
- * @example
508
- * ```typescript
509
- * if (config.has('redis.url')) connectRedis(config.get('redis.url'));
510
- * ```
511
- */
512
418
  has(path) {
513
419
  return this.resolve(path) !== void 0;
514
420
  }
515
- /**
516
- * Resolve a two-level dot-path to its raw leaf value.
517
- *
518
- * Looks the path up in the precomputed `namespace.leaf` map, so an unset leaf
519
- * (or, defensively, an unexpected path) yields `undefined` instead of throwing.
520
- *
521
- * @param path - The declared two-level dot-path.
522
- * @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
523
- */
524
421
  resolve(path) {
525
422
  return __privateGet(this, _leaves).get(String(path));
526
423
  }
@@ -537,35 +434,12 @@ function appendTo(list, ...items) {
537
434
  return list === void 0 ? [...items] : [...list, ...items];
538
435
  }
539
436
  var BymaxConfigModule = class extends ConfigurableModuleClass {
540
- /**
541
- * Register the module synchronously from a static schema.
542
- *
543
- * @param options - The module options plus the optional `isGlobal` extra.
544
- * @returns The dynamic module augmented with the frozen config provider.
545
- */
546
437
  static forRoot(options) {
547
438
  return BymaxConfigModule.withConfigProvider(super.forRoot(options));
548
439
  }
549
- /**
550
- * Register the module asynchronously, resolving options through a factory.
551
- *
552
- * @param options - The async options with `useFactory` and explicit `inject`.
553
- * @returns The dynamic module augmented with the frozen config provider.
554
- */
555
440
  static forRootAsync(options) {
556
441
  return BymaxConfigModule.withConfigProvider(super.forRootAsync(options));
557
442
  }
558
- /**
559
- * Append the config provider and typed accessor to a generated definition.
560
- *
561
- * Registers the frozen {@link BYMAX_CONFIG} provider and the {@link ConfigService}
562
- * accessor (which injects that token), then exports both so consumers can
563
- * inject either the raw frozen object or the typed service.
564
- *
565
- * @param definition - The dynamic module produced by the builder base class.
566
- * @returns A definition that provides and exports {@link BYMAX_CONFIG} and
567
- * {@link ConfigService}.
568
- */
569
443
  static withConfigProvider(definition) {
570
444
  return {
571
445
  ...definition,
@@ -167,14 +167,26 @@ function synthesizePlaceholderSource(schema) {
167
167
  }
168
168
 
169
169
  // src/testing/create-test-config.ts
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
+ }
170
175
  function unplacedIssue(path, code, message) {
171
176
  return { path, variable: "", code, message };
172
177
  }
173
178
  function flattenOverrides(overrides, declared) {
174
179
  const flat = /* @__PURE__ */ new Map();
175
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
+ }
176
188
  for (const [namespace, leaves] of Object.entries(overrides)) {
177
- if (typeof leaves !== "object") {
189
+ if (!isPlainObject(leaves)) {
178
190
  unplaced.push(
179
191
  unplacedIssue(
180
192
  namespace,
@@ -204,7 +216,7 @@ function flattenOverrides(overrides, declared) {
204
216
  function applyOverrides(schema, base, overrides, overridden) {
205
217
  const bindings = internal.resolveSourceNames(schema);
206
218
  const { flat, unplaced } = flattenOverrides(
207
- overrides ?? {},
219
+ overrides,
208
220
  new Set(bindings.map((binding) => binding.path))
209
221
  );
210
222
  if (unplaced.length > 0) throw new internal.BymaxConfigValidationError(unplaced);
@@ -165,14 +165,26 @@ function synthesizePlaceholderSource(schema) {
165
165
  }
166
166
 
167
167
  // src/testing/create-test-config.ts
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
+ }
168
173
  function unplacedIssue(path, code, message) {
169
174
  return { path, variable: "", code, message };
170
175
  }
171
176
  function flattenOverrides(overrides, declared) {
172
177
  const flat = /* @__PURE__ */ new Map();
173
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
+ }
174
186
  for (const [namespace, leaves] of Object.entries(overrides)) {
175
- if (typeof leaves !== "object") {
187
+ if (!isPlainObject(leaves)) {
176
188
  unplaced.push(
177
189
  unplacedIssue(
178
190
  namespace,
@@ -202,7 +214,7 @@ function flattenOverrides(overrides, declared) {
202
214
  function applyOverrides(schema, base, overrides, overridden) {
203
215
  const bindings = resolveSourceNames(schema);
204
216
  const { flat, unplaced } = flattenOverrides(
205
- overrides ?? {},
217
+ overrides,
206
218
  new Set(bindings.map((binding) => binding.path))
207
219
  );
208
220
  if (unplaced.length > 0) throw new BymaxConfigValidationError(unplaced);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bymax-one/nest-config",
3
- "version": "1.2.4",
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,7 @@
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
58
  "check:api": "node scripts/check-api-surface.mjs",
59
59
  "check:exports": "attw --pack . --profile strict",
60
60
  "check:lint-graph": "node scripts/check-lint-graph-rules.mjs",