molex-env 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - **Type-safe parsing** - Automatic conversion of booleans, numbers, JSON, and dates
17
17
  - **Strict validation** - Schema enforcement with required fields and type checking
18
18
  - **Origin tracking** - Know exactly which file and line each value came from
19
+ - **Debug mode** - See which files override values during cascading
19
20
  - **Immutable config** - Deep-freeze protection prevents accidental modifications
20
21
  - **Live reload** - Watch mode automatically reloads on file changes
21
22
  - **Deterministic merging** - Predictable cascading from base to profile files
@@ -139,6 +140,18 @@ Files are loaded and merged in this order (later files override earlier ones):
139
140
  Final result: PORT=9000, DEBUG=false
140
141
  ```
141
142
 
143
+ **Debug mode** - Use `debug: true` to see which files override values:
144
+ ```javascript
145
+ load({ profile: 'prod', debug: true });
146
+ // Console output:
147
+ // [molex-env] Override: DEBUG
148
+ // Previous: .menv:2 = true
149
+ // New: .menv.local:1 = false
150
+ // [molex-env] Override: PORT
151
+ // Previous: .menv.local:3 = 3000
152
+ // New: .menv.prod:1 = 8080
153
+ ```
154
+
142
155
  ## API Reference
143
156
 
144
157
  ### `load(options)` → `Object`
@@ -153,12 +166,13 @@ Load, merge, parse, and validate .menv files. This is the primary method you'll
153
166
  | `profile` | `string` | `undefined` | Profile name for `.menv.{profile}` files |
154
167
  | `files` | `Array<string>` | Auto-detected | Custom file list (absolute or relative to cwd) |
155
168
  | `schema` | `Object` | `{}` | Schema definition for validation and typing |
156
- | `strict` | `boolean` | `false` | Reject unknown keys, duplicates, and invalid lines |
169
+ | `strict` | `boolean` | `false` | Reject unknown keys, within-file duplicates, and invalid lines |
157
170
  | `cast` | `boolean\|Object` | `true` | Enable/disable type casting (see Type Casting) |
158
171
  | `exportEnv` | `boolean` | `false` | Write parsed values to `process.env` |
159
172
  | `override` | `boolean` | `false` | Override existing `process.env` values |
160
173
  | `attach` | `boolean` | `true` | Attach parsed values to `process.menv` |
161
174
  | `freeze` | `boolean` | `true` | Deep-freeze the parsed config object |
175
+ | `debug` | `boolean` | `false` | Log file precedence overrides to console |
162
176
  | `onWarning` | `Function` | `undefined` | Callback for non-strict warnings |
163
177
 
164
178
  **Returns:**
@@ -213,10 +227,12 @@ Parse a string of .menv content without loading files. Useful for testing or pro
213
227
  | Option | Type | Default | Description |
214
228
  |--------|------|---------|-------------|
215
229
  | `schema` | `Object` | `{}` | Schema definition for validation |
216
- | `strict` | `boolean` | `false` | Enable strict validation |
230
+ | `strict` | `boolean` | `false` | Enable strict validation (rejects unknown keys, within-file duplicates, invalid lines) |
217
231
  | `cast` | `boolean\|Object` | `true` | Enable/disable type casting |
218
232
  | `freeze` | `boolean` | `true` | Deep-freeze the result |
219
233
 
234
+ > **Note:** The `parse()` function processes a single string, so the `debug` option for file precedence and cross-file features don't apply here.
235
+
220
236
  **Returns:**
221
237
 
222
238
  ```javascript
@@ -452,9 +468,10 @@ Strict mode provides rigorous validation to catch configuration errors early.
452
468
 
453
469
  When `strict: true`:
454
470
  - ❌ **Unknown keys** - Keys not in schema are rejected
455
- - ❌ **Duplicate keys** - Same key in multiple files throws error
471
+ - ❌ **Duplicate keys** - Same key appearing twice **in the same file** throws error
472
+ - **Note:** File precedence still works - different files can define the same key
456
473
  - ❌ **Invalid lines** - Malformed lines throw errors
457
- - **Type mismatches** - Values that can't be parsed as specified type
474
+ - **Type validation** - When schema is present, type mismatches throw errors (enabled by default with schema)
458
475
 
459
476
  **Example:**
460
477
 
@@ -473,28 +490,56 @@ load({
473
490
  });
474
491
  ```
475
492
 
493
+ **Valid with strict mode (different files):**
494
+ ```javascript
495
+ // .menv
496
+ PORT=3000
497
+
498
+ // .menv.prod
499
+ PORT=8080 // ✅ OK - overrides from different file
500
+
501
+ load({ profile: 'prod', strict: true });
502
+ // Result: PORT=8080
503
+ ```
504
+
505
+ **Invalid with strict mode (same file):**
506
+ ```javascript
507
+ // .menv
508
+ PORT=3000
509
+ PORT=8080 // ❌ ERROR - duplicate in same file
510
+
511
+ load({ strict: true }); // Throws error
512
+ ```
513
+
476
514
  ### Non-Strict Mode (Default)
477
515
 
478
- Without strict mode:
516
+ Without strict mode, the file precedence feature works as intended:
479
517
  - ✅ Unknown keys are allowed and parsed
480
- - ✅ Duplicates override (later files win)
518
+ - ✅ **Duplicate keys override** - Later files can override keys from earlier files
481
519
  - ✅ Invalid lines are skipped
482
- - ⚠️ Warnings can be logged via `onWarning` callback
520
+ - ⚠️ Warnings can be logged via `onWarning` callback for within-file duplicates
483
521
 
484
522
  **Example with warning handler:**
485
523
 
486
524
  ```javascript
525
+ // .menv file with duplicate keys
526
+ // PORT=3000
527
+ // PORT=8080
528
+
487
529
  load({
488
- schema: { PORT: 'number' },
489
530
  strict: false,
490
531
  onWarning: (info) => {
491
- console.warn(`Warning: ${info.key} redefined`);
492
- console.warn(` Previous: ${info.previous.file}:${info.previous.line}`);
493
- console.warn(` New: ${info.next.file}:${info.next.line}`);
532
+ if (info.type === 'duplicate') {
533
+ console.warn(`Warning: Duplicate key '${info.key}' in ${info.file}:${info.line}`);
534
+ }
494
535
  }
495
536
  });
537
+ // Output: Warning: Duplicate key 'PORT' in .menv:2
538
+ // Result: PORT=8080 (last value wins)
496
539
  ```
497
540
 
541
+ **Tip:** Use `debug: true` to see cross-file overrides (file precedence), or `onWarning` to catch within-file duplicates.
542
+
498
543
  ---
499
544
 
500
545
  ## Origin Tracking
@@ -710,7 +755,8 @@ Production: .menv + .menv.prod + .menv.prod.local (secrets)
710
755
  ### Security Tips
711
756
 
712
757
  - ✅ **DO** use `.menv.local` for secrets and add to `.gitignore`
713
- - ✅ **DO** use `strict: true` in production to catch misconfigurations
758
+ - ✅ **DO** use `strict: true` in production to catch unknown keys and configuration errors
759
+ - ✅ **DO** use `debug: true` during development to understand file precedence
714
760
  - ✅ **DO** validate sensitive values (URLs, ports, etc.) after loading
715
761
  - ❌ **DON'T** commit production secrets to git
716
762
  - ❌ **DON'T** use `exportEnv: true` if you need immutable config
@@ -786,6 +832,22 @@ METADATA={"key":"value"}
786
832
  START_DATE=2026-02-02
787
833
  ```
788
834
 
835
+ ### Understanding which file sets a value
836
+
837
+ **Problem:** Not sure which file is providing a specific config value.
838
+
839
+ **Solution:** Use `debug: true` to see file precedence in action:
840
+ ```javascript
841
+ load({ profile: 'prod', debug: true });
842
+ // Shows console output for each override
843
+ ```
844
+
845
+ Or check the `origins` object:
846
+ ```javascript
847
+ const result = load({ profile: 'prod' });
848
+ console.log(result.origins.PORT); // { file: '.menv.prod', line: 1, raw: '8080' }
849
+ ```
850
+
789
851
  ---
790
852
 
791
853
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molex-env",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Native .menv loader with profiles, typing, and origin tracking.",
5
5
  "main": "src/index.js",
6
6
  "files": [
package/src/lib/apply.js CHANGED
@@ -5,15 +5,15 @@ const { coerceType, autoCast } = require('./cast');
5
5
 
6
6
  /**
7
7
  * Apply a parsed entry to the state with schema/type checks.
8
- * @param {{values: object, origins: object, seen: Set<string>}} state
8
+ * @param {{values: object, origins: object, seenPerFile: Map<string, Set<string>>}} state
9
9
  * @param {{key: string, raw: string, line: number}} entry
10
- * @param {{schema: object|null, strict: boolean, cast: object, onWarning?: Function}} options
10
+ * @param {{schema: object|null, strict: boolean, cast: object, onWarning?: Function, debug?: boolean}} options
11
11
  * @param {string} filePath
12
12
  * @returns {void}
13
13
  */
14
14
  function applyEntry(state, entry, options, filePath)
15
15
  {
16
- const { schema, strict, cast, onWarning } = options;
16
+ const { schema, strict, cast, onWarning, debug } = options;
17
17
  const { key, raw, line } = entry;
18
18
 
19
19
  if (schema && strict && !schema[key])
@@ -21,7 +21,15 @@ function applyEntry(state, entry, options, filePath)
21
21
  throw unknownKeyError(key, filePath, line);
22
22
  }
23
23
 
24
- if (state.seen.has(key))
24
+ // Initialize per-file tracking if needed
25
+ if (!state.seenPerFile.has(filePath))
26
+ {
27
+ state.seenPerFile.set(filePath, new Set());
28
+ }
29
+ const fileKeys = state.seenPerFile.get(filePath);
30
+
31
+ // Check for duplicates ONLY within the same file
32
+ if (fileKeys.has(key))
25
33
  {
26
34
  if (strict)
27
35
  {
@@ -38,6 +46,15 @@ function applyEntry(state, entry, options, filePath)
38
46
  }
39
47
  }
40
48
 
49
+ // Debug logging for file precedence
50
+ if (debug && state.values[key] !== undefined)
51
+ {
52
+ const prevOrigin = state.origins[key];
53
+ console.log(`[molex-env] Override: ${key}`);
54
+ console.log(` Previous: ${prevOrigin.file}:${prevOrigin.line} = ${prevOrigin.raw}`);
55
+ console.log(` New: ${filePath}:${line} = ${raw}`);
56
+ }
57
+
41
58
  const def = schema ? schema[key] : null;
42
59
  let value;
43
60
  if (def && def.type)
@@ -50,7 +67,7 @@ function applyEntry(state, entry, options, filePath)
50
67
 
51
68
  state.values[key] = value;
52
69
  state.origins[key] = { file: filePath, line, raw };
53
- state.seen.add(key);
70
+ fileKeys.add(key);
54
71
  }
55
72
 
56
73
  module.exports = {
package/src/lib/core.js CHANGED
@@ -11,14 +11,14 @@ const { deepFreeze } = require('./utils');
11
11
 
12
12
  /**
13
13
  * Build a new parsing state container.
14
- * @returns {{values: object, origins: object, seen: Set<string>}}
14
+ * @returns {{values: object, origins: object, seenPerFile: Map<string, Set<string>>}}
15
15
  */
16
16
  function buildState()
17
17
  {
18
18
  return {
19
19
  values: {},
20
20
  origins: {},
21
- seen: new Set()
21
+ seenPerFile: new Map()
22
22
  };
23
23
  }
24
24
 
@@ -79,7 +79,8 @@ function load(options = {})
79
79
  schema: normalizedSchema,
80
80
  strict,
81
81
  cast,
82
- onWarning: options.onWarning
82
+ onWarning: options.onWarning,
83
+ debug: options.debug
83
84
  }, filePath);
84
85
  }
85
86
  readFiles.push(filePath);