molex-env 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -125,8 +126,8 @@ OPTIONAL_KEY=
125
126
 
126
127
  Files are loaded and merged in this order (later files override earlier ones):
127
128
 
128
- 1. `.menv` - Base configuration (committed to git)
129
- 2. `.menv.local` - Local overrides (ignored by git)
129
+ 1. `.menv` - Base configuration
130
+ 2. `.menv.local` - Local overrides
130
131
  3. `.menv.{profile}` - Profile-specific config (e.g., `.menv.prod`)
131
132
  4. `.menv.{profile}.local` - Profile + local overrides (e.g., `.menv.prod.local`)
132
133
 
@@ -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
@@ -261,23 +277,62 @@ Watch .menv files and reload automatically when they change. Perfect for develop
261
277
 
262
278
  **Arguments:**
263
279
 
264
- - `options` - Same options as `load()`
280
+ - `options` - Same options as `load()`, including `debug` for automatic change logging
265
281
  - `onChange(error, result)` - Callback fired on file changes
266
282
 
267
- **Example:**
283
+ **Automatic Change Detection:**
284
+
285
+ When `debug: true` is enabled, watch mode automatically logs what values changed on each reload:
268
286
 
269
287
  ```javascript
270
288
  const { watch } = require('molex-env');
271
289
 
272
- // Watch with callback
273
- watch({ profile: 'dev', strict: true }, (err, result) => {
290
+ watch({
291
+ profile: 'dev',
292
+ debug: true, // Automatically logs changes
293
+ schema: {
294
+ PORT: 'number',
295
+ DEBUG: 'boolean',
296
+ SERVICE_URL: 'string'
297
+ }
298
+ }, (err, result) => {
299
+ if (err) {
300
+ console.error('Config reload failed:', err.message);
301
+ return;
302
+ }
303
+ console.log('✅ Config successfully reloaded');
304
+ });
305
+
306
+ // When you edit .menv files, automatic output:
307
+ // [molex-env] Config reloaded - changes detected:
308
+ // PORT: 3000 → 8080
309
+ // SERVICE_URL: https://api.example.com → https://api.production.com
310
+ // ✅ Config successfully reloaded
311
+ ```
312
+
313
+ **Manual Change Detection:**
314
+
315
+ Without `debug: true`, you can manually detect changes in your callback:
316
+
317
+ ```javascript
318
+ let currentConfig;
319
+
320
+ watch({ profile: 'dev' }, (err, result) => {
274
321
  if (err) {
275
322
  console.error('Config reload failed:', err.message);
276
323
  return;
277
324
  }
278
325
 
279
- console.log('Config reloaded!');
280
- console.log('New PORT:', result.parsed.PORT);
326
+ if (!currentConfig) {
327
+ console.log('Initial config loaded');
328
+ } else {
329
+ // Manually check what changed
330
+ if (currentConfig.PORT !== result.parsed.PORT) {
331
+ console.log(`PORT changed: ${currentConfig.PORT} → ${result.parsed.PORT}`);
332
+ }
333
+ }
334
+
335
+ currentConfig = result.parsed;
281
336
 
282
337
  // Restart your server or update app state here
283
338
  if (global.server) {
@@ -309,10 +364,17 @@ function startServer(config) {
309
364
  const initial = require('molex-env').load({ profile: 'dev' });
310
365
  server = startServer(initial.parsed);
311
366
 
312
- // Watch for changes
313
- watch({ profile: 'dev' }, (err, result) => {
367
+ // Watch for changes with automatic change logging
368
+ watch({
369
+ profile: 'dev',
370
+ debug: true,
371
+ schema: {
372
+ PORT: 'number',
373
+ DEBUG: 'boolean'
374
+ }
375
+ }, (err, result) => {
314
376
  if (!err && result.parsed.PORT !== initial.parsed.PORT) {
315
- console.log('Port changed, restarting...');
377
+ console.log('Port changed, restarting server...');
316
378
  server.close(() => {
317
379
  server = startServer(result.parsed);
318
380
  });
@@ -452,9 +514,10 @@ Strict mode provides rigorous validation to catch configuration errors early.
452
514
 
453
515
  When `strict: true`:
454
516
  - ❌ **Unknown keys** - Keys not in schema are rejected
455
- - ❌ **Duplicate keys** - Same key in multiple files throws error
517
+ - ❌ **Duplicate keys** - Same key appearing twice **in the same file** throws error
518
+ - **Note:** File precedence still works - different files can define the same key
456
519
  - ❌ **Invalid lines** - Malformed lines throw errors
457
- - **Type mismatches** - Values that can't be parsed as specified type
520
+ - **Type validation** - When schema is present, type mismatches throw errors (enabled by default with schema)
458
521
 
459
522
  **Example:**
460
523
 
@@ -473,28 +536,56 @@ load({
473
536
  });
474
537
  ```
475
538
 
539
+ **Valid with strict mode (different files):**
540
+ ```javascript
541
+ // .menv
542
+ PORT=3000
543
+
544
+ // .menv.prod
545
+ PORT=8080 // ✅ OK - overrides from different file
546
+
547
+ load({ profile: 'prod', strict: true });
548
+ // Result: PORT=8080
549
+ ```
550
+
551
+ **Invalid with strict mode (same file):**
552
+ ```javascript
553
+ // .menv
554
+ PORT=3000
555
+ PORT=8080 // ❌ ERROR - duplicate in same file
556
+
557
+ load({ strict: true }); // Throws error
558
+ ```
559
+
476
560
  ### Non-Strict Mode (Default)
477
561
 
478
- Without strict mode:
562
+ Without strict mode, the file precedence feature works as intended:
479
563
  - ✅ Unknown keys are allowed and parsed
480
- - ✅ Duplicates override (later files win)
564
+ - ✅ **Duplicate keys override** - Later files can override keys from earlier files
481
565
  - ✅ Invalid lines are skipped
482
- - ⚠️ Warnings can be logged via `onWarning` callback
566
+ - ⚠️ Warnings can be logged via `onWarning` callback for within-file duplicates
483
567
 
484
568
  **Example with warning handler:**
485
569
 
486
570
  ```javascript
571
+ // .menv file with duplicate keys
572
+ // PORT=3000
573
+ // PORT=8080
574
+
487
575
  load({
488
- schema: { PORT: 'number' },
489
576
  strict: false,
490
577
  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}`);
578
+ if (info.type === 'duplicate') {
579
+ console.warn(`Warning: Duplicate key '${info.key}' in ${info.file}:${info.line}`);
580
+ }
494
581
  }
495
582
  });
583
+ // Output: Warning: Duplicate key 'PORT' in .menv:2
584
+ // Result: PORT=8080 (last value wins)
496
585
  ```
497
586
 
587
+ **Tip:** Use `debug: true` to see cross-file overrides (file precedence), or `onWarning` to catch within-file duplicates.
588
+
498
589
  ---
499
590
 
500
591
  ## Origin Tracking
@@ -612,10 +703,9 @@ console.log(`PORT: ${process.menv.PORT}`);
612
703
  ```javascript
613
704
  const { watch } = require('molex-env');
614
705
 
615
- let currentConfig;
616
-
617
706
  watch({
618
707
  profile: 'dev',
708
+ debug: true, // Automatic change detection
619
709
  schema: {
620
710
  PORT: 'number',
621
711
  DEBUG: 'boolean',
@@ -627,24 +717,15 @@ watch({
627
717
  return;
628
718
  }
629
719
 
630
- const changed = [];
631
- if (!currentConfig) {
632
- console.log('Initial config loaded');
633
- } else {
634
- // Detect what changed
635
- Object.keys(result.parsed).forEach(key => {
636
- if (currentConfig[key] !== result.parsed[key]) {
637
- changed.push(`${key}: ${currentConfig[key]} → ${result.parsed[key]}`);
638
- }
639
- });
640
-
641
- if (changed.length > 0) {
642
- console.log('Config updated:', changed.join(', '));
643
- }
644
- }
645
-
646
- currentConfig = result.parsed;
720
+ console.log('Config reloaded and ready to use');
721
+ // result.parsed has the new values
647
722
  });
723
+
724
+ console.log('Watching for changes...');
725
+ // Output on file change:
726
+ // [molex-env] Config reloaded - changes detected:
727
+ // DEBUG: false → true
728
+ // API_URL: https://api.example.com → https://api.dev.local
648
729
  ```
649
730
 
650
731
  ### Validation and Error Handling
@@ -685,34 +766,19 @@ try {
685
766
 
686
767
  ## Best Practices
687
768
 
688
- ### Git Configuration
689
-
690
- Add to `.gitignore`:
691
- ```gitignore
692
- # Keep base configs in git
693
- # .menv
694
- # .menv.dev
695
- # .menv.prod
696
-
697
- # Ignore local overrides (machine-specific, secrets)
698
- .menv.local
699
- .menv.*.local
700
- ```
701
-
702
769
  ### Environment Strategy
703
770
 
704
771
  ```
705
772
  Development: .menv + .menv.local
706
773
  Staging: .menv + .menv.staging
707
- Production: .menv + .menv.prod + .menv.prod.local (secrets)
774
+ Production: .menv + .menv.prod + .menv.prod.local
708
775
  ```
709
776
 
710
777
  ### Security Tips
711
778
 
712
- - ✅ **DO** use `.menv.local` for secrets and add to `.gitignore`
713
- - ✅ **DO** use `strict: true` in production to catch misconfigurations
779
+ - ✅ **DO** use `strict: true` in production to catch unknown keys and configuration errors
780
+ - ✅ **DO** use `debug: true` during development to understand file precedence
714
781
  - ✅ **DO** validate sensitive values (URLs, ports, etc.) after loading
715
- - ❌ **DON'T** commit production secrets to git
716
782
  - ❌ **DON'T** use `exportEnv: true` if you need immutable config
717
783
 
718
784
  ### Performance
@@ -748,11 +814,25 @@ The example demonstrates:
748
814
 
749
815
  **Problem:** Getting errors about unknown keys when loading config.
750
816
 
751
- **Solution:** Add all keys to your schema or disable strict mode:
817
+ **Cause:** You have a key in your `.menv` file that isn't defined in your schema, and `strict: true` is enabled.
818
+
819
+ **Solution:** Either add the key to your schema or disable strict mode:
752
820
  ```javascript
753
- load({ strict: false }); // Allow unknown keys
821
+ // Option 1: Add missing key to schema
822
+ load({
823
+ strict: true,
824
+ schema: {
825
+ PORT: 'number',
826
+ YOUR_MISSING_KEY: 'string' // Add this
827
+ }
828
+ });
829
+
830
+ // Option 2: Disable strict mode to allow unknown keys
831
+ load({ strict: false });
754
832
  ```
755
833
 
834
+ > **Note:** This is different from "required" - unknown keys exist in your file but not in schema. Required keys exist in schema but not in your file.
835
+
756
836
  ### Values are strings instead of typed
757
837
 
758
838
  **Problem:** `PORT` is `"3000"` (string) instead of `3000` (number).
@@ -786,6 +866,22 @@ METADATA={"key":"value"}
786
866
  START_DATE=2026-02-02
787
867
  ```
788
868
 
869
+ ### Understanding which file sets a value
870
+
871
+ **Problem:** Not sure which file is providing a specific config value.
872
+
873
+ **Solution:** Use `debug: true` to see file precedence in action:
874
+ ```javascript
875
+ load({ profile: 'prod', debug: true });
876
+ // Shows console output for each override
877
+ ```
878
+
879
+ Or check the `origins` object:
880
+ ```javascript
881
+ const result = load({ profile: 'prod' });
882
+ console.log(result.origins.PORT); // { file: '.menv.prod', line: 1, raw: '8080' }
883
+ ```
884
+
789
885
  ---
790
886
 
791
887
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molex-env",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
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);
package/src/lib/watch.js CHANGED
@@ -27,6 +27,7 @@ function watch(options, onChange, load)
27
27
  const basenames = new Set(files.map((file) => path.basename(file)));
28
28
  const watchers = [];
29
29
  let timer = null;
30
+ let previousConfig = null;
30
31
 
31
32
  const trigger = () =>
32
33
  {
@@ -35,7 +36,40 @@ function watch(options, onChange, load)
35
36
  {
36
37
  try
37
38
  {
38
- const result = load(options);
39
+ // Disable file precedence debug during reload to avoid conflicts with watch change detection
40
+ const reloadOptions = { ...options, debug: false };
41
+ const result = load(reloadOptions);
42
+
43
+ // Auto-detect changes if debug mode was enabled
44
+ if (options.debug && previousConfig)
45
+ {
46
+ const changes = [];
47
+ for (const key in result.parsed)
48
+ {
49
+ const oldValue = previousConfig[key];
50
+ const newValue = result.parsed[key];
51
+
52
+ // Compare values (handle dates and objects)
53
+ const oldStr = oldValue instanceof Date ? oldValue.toISOString() : JSON.stringify(oldValue);
54
+ const newStr = newValue instanceof Date ? newValue.toISOString() : JSON.stringify(newValue);
55
+
56
+ if (oldStr !== newStr)
57
+ {
58
+ changes.push({ key, old: oldValue, new: newValue });
59
+ }
60
+ }
61
+
62
+ if (changes.length > 0)
63
+ {
64
+ console.log('[molex-env] Config reloaded - changes detected:');
65
+ changes.forEach(({ key, old, new: newVal }) =>
66
+ {
67
+ console.log(` ${key}: ${old} → ${newVal}`);
68
+ });
69
+ }
70
+ }
71
+
72
+ previousConfig = { ...result.parsed };
39
73
  onChange(null, result);
40
74
  } catch (err)
41
75
  {