molex-env 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ molex-env
2
+
3
+ Native .menv loader with profiles, typing, origin tracking, and optional reload.
4
+
5
+ Highlights
6
+ - Deterministic profile merging
7
+ - Typed parsing (boolean, number, json, date)
8
+ - Strict mode for unknown or duplicate keys
9
+ - Origin tracking (file + line)
10
+ - Immutable config objects
11
+ - Optional file watching for reload
12
+
13
+ Install
14
+ - npm install molex-env
15
+
16
+ Basic usage
17
+ const { load } = require('molex-env');
18
+
19
+ const result = load({
20
+ profile: 'prod',
21
+ export: true,
22
+ strict: true,
23
+ schema: {
24
+ PORT: 'number',
25
+ DEBUG: 'boolean',
26
+ SERVICE_URL: { type: 'string', required: true }
27
+ }
28
+ });
29
+
30
+ console.log(result.parsed.PORT);
31
+ console.log(result.origins.SERVICE_URL);
32
+
33
+ File precedence
34
+ 1) .menv
35
+ 2) .menv.local
36
+ 3) .menv.{profile}
37
+ 4) .menv.{profile}.local
38
+
39
+ API
40
+ load(options)
41
+ - cwd: base directory (default: process.cwd())
42
+ - profile: profile name for .menv.{profile}
43
+ - files: custom file list (absolute or relative to cwd)
44
+ - schema: allowed keys, types, defaults, required
45
+ - strict: reject unknown keys, duplicates, and invalid lines
46
+ - cast: true | false | { boolean, number, json, date }
47
+ - export: write values to process.env
48
+ - override: override existing process.env values
49
+ - freeze: deep-freeze parsed config (default true)
50
+ - onWarning: function(info) for non-strict duplicates
51
+
52
+ parse(text, options)
53
+ - Parse a string of .menv content using the same typing rules
54
+
55
+ watch(options, onChange)
56
+ - Watch resolved files and reload on change
57
+ - onChange(err, result)
58
+
59
+ Schema example
60
+ const schema = {
61
+ PORT: { type: 'number', default: 3000 },
62
+ DEBUG: { type: 'boolean', default: false },
63
+ METADATA: { type: 'json' },
64
+ START_DATE: { type: 'date' }
65
+ };
66
+
67
+ Notes
68
+ - Use .menv.local for machine-specific values
69
+ - Use strict mode to detect surprises early
70
+
71
+ Example project
72
+ An example app is included in examples/basic.
73
+ Run it with:
74
+ - npm install (from examples/basic)
75
+ - npm start
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "molex-env",
3
+ "version": "0.1.0",
4
+ "description": "Native .menv loader with profiles, typing, and origin tracking.",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src",
8
+ "LICENSE",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "node --test"
13
+ },
14
+ "keywords": [
15
+ "env",
16
+ "dotenv",
17
+ "menv",
18
+ "config",
19
+ "profile",
20
+ "native"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/tonywied17/molex-env.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/tonywied17/molex-env/issues"
30
+ },
31
+ "homepage": "https://github.com/tonywied17/molex-env#readme"
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const { load, parse } = require('./lib/core');
4
+ const { watch } = require('./lib/watch');
5
+
6
+ module.exports = {
7
+ load,
8
+ parse,
9
+ /**
10
+ * Watch resolved .menv files and reload on change.
11
+ * @param {object} options
12
+ * @param {(err: Error|null, result?: {parsed: object, origins: object, files: string[]}) => void} onChange
13
+ * @returns {{close: () => void}}
14
+ */
15
+ watch(options, onChange)
16
+ {
17
+ return watch(options, onChange, load);
18
+ }
19
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { unknownKeyError, duplicateKeyError } = require('./errors');
4
+ const { coerceType, autoCast } = require('./cast');
5
+
6
+ /**
7
+ * Apply a parsed entry to the state with schema/type checks.
8
+ * @param {{values: object, origins: object, seen: Set<string>}} state
9
+ * @param {{key: string, raw: string, line: number}} entry
10
+ * @param {{schema: object|null, strict: boolean, cast: object, onWarning?: Function}} options
11
+ * @param {string} filePath
12
+ * @returns {void}
13
+ */
14
+ function applyEntry(state, entry, options, filePath)
15
+ {
16
+ const { schema, strict, cast, onWarning } = options;
17
+ const { key, raw, line } = entry;
18
+
19
+ if (schema && strict && !schema[key])
20
+ {
21
+ throw unknownKeyError(key, filePath, line);
22
+ }
23
+
24
+ if (state.seen.has(key))
25
+ {
26
+ if (strict)
27
+ {
28
+ throw duplicateKeyError(key, filePath, line);
29
+ }
30
+ if (typeof onWarning === 'function')
31
+ {
32
+ onWarning({
33
+ type: 'duplicate',
34
+ key,
35
+ file: filePath,
36
+ line
37
+ });
38
+ }
39
+ }
40
+
41
+ const def = schema ? schema[key] : null;
42
+ let value;
43
+ if (def && def.type)
44
+ {
45
+ value = coerceType(raw, def.type, filePath, line);
46
+ } else
47
+ {
48
+ value = autoCast(raw, cast);
49
+ }
50
+
51
+ state.values[key] = value;
52
+ state.origins[key] = { file: filePath, line, raw };
53
+ state.seen.add(key);
54
+ }
55
+
56
+ module.exports = {
57
+ applyEntry
58
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { invalidTypeError } = require('./errors');
4
+
5
+ /**
6
+ * Normalize cast options into explicit booleans.
7
+ * @param {boolean|{boolean?: boolean, number?: boolean, json?: boolean, date?: boolean}} cast
8
+ * @returns {{boolean: boolean, number: boolean, json: boolean, date: boolean}}
9
+ */
10
+ function normalizeCast(cast)
11
+ {
12
+ if (cast === true || cast === undefined)
13
+ {
14
+ return { boolean: true, number: true, json: true, date: true };
15
+ }
16
+ if (cast === false)
17
+ {
18
+ return { boolean: false, number: false, json: false, date: false };
19
+ }
20
+ return {
21
+ boolean: cast.boolean !== false,
22
+ number: cast.number !== false,
23
+ json: cast.json !== false,
24
+ date: cast.date !== false
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Check if a string is a number.
30
+ * @param {string} value
31
+ * @returns {boolean}
32
+ */
33
+ function isNumber(value)
34
+ {
35
+ return /^-?\d+(\.\d+)?$/.test(value);
36
+ }
37
+
38
+ /**
39
+ * Check if a string looks like an ISO date.
40
+ * @param {string} value
41
+ * @returns {boolean}
42
+ */
43
+ function isIsoDate(value)
44
+ {
45
+ return /^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(value);
46
+ }
47
+
48
+ /**
49
+ * Coerce a raw string to the requested type.
50
+ * @param {string} raw
51
+ * @param {string} type
52
+ * @param {string} file
53
+ * @param {number} line
54
+ * @returns {any}
55
+ */
56
+ function coerceType(raw, type, file, line)
57
+ {
58
+ if (type === 'string') return raw;
59
+ if (type === 'boolean')
60
+ {
61
+ if (raw.toLowerCase() === 'true') return true;
62
+ if (raw.toLowerCase() === 'false') return false;
63
+ throw invalidTypeError('boolean', raw, file, line);
64
+ }
65
+ if (type === 'number')
66
+ {
67
+ if (!isNumber(raw)) throw invalidTypeError('number', raw, file, line);
68
+ return Number(raw);
69
+ }
70
+ if (type === 'json')
71
+ {
72
+ return JSON.parse(raw);
73
+ }
74
+ if (type === 'date')
75
+ {
76
+ const date = new Date(raw);
77
+ if (Number.isNaN(date.getTime())) throw invalidTypeError('date', raw, file, line);
78
+ return date;
79
+ }
80
+ return raw;
81
+ }
82
+
83
+ /**
84
+ * Auto-cast a raw string based on enabled rules.
85
+ * @param {string} raw
86
+ * @param {{boolean: boolean, number: boolean, json: boolean, date: boolean}} cast
87
+ * @returns {any}
88
+ */
89
+ function autoCast(raw, cast)
90
+ {
91
+ const trimmed = raw.trim();
92
+ if (cast.boolean)
93
+ {
94
+ const lower = trimmed.toLowerCase();
95
+ if (lower === 'true') return true;
96
+ if (lower === 'false') return false;
97
+ }
98
+ if (cast.number && isNumber(trimmed))
99
+ {
100
+ return Number(trimmed);
101
+ }
102
+ if (cast.json && (trimmed.startsWith('{') || trimmed.startsWith('[')))
103
+ {
104
+ try
105
+ {
106
+ return JSON.parse(trimmed);
107
+ } catch (err)
108
+ {
109
+ return trimmed;
110
+ }
111
+ }
112
+ if (cast.date && isIsoDate(trimmed))
113
+ {
114
+ const date = new Date(trimmed);
115
+ if (!Number.isNaN(date.getTime())) return date;
116
+ }
117
+ return trimmed;
118
+ }
119
+
120
+ module.exports = {
121
+ normalizeCast,
122
+ coerceType,
123
+ autoCast
124
+ };
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ const { normalizeCast } = require('./cast');
6
+ const { normalizeSchema, applySchemaDefaults } = require('./schema');
7
+ const { parseEntries } = require('./parser');
8
+ const { applyEntry } = require('./apply');
9
+ const { resolveFiles } = require('./files');
10
+ const { deepFreeze } = require('./utils');
11
+
12
+ /**
13
+ * Build a new parsing state container.
14
+ * @returns {{values: object, origins: object, seen: Set<string>}}
15
+ */
16
+ function buildState()
17
+ {
18
+ return {
19
+ values: {},
20
+ origins: {},
21
+ seen: new Set()
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Export parsed values to process.env if enabled.
27
+ * @param {object} values
28
+ * @param {object} options
29
+ * @returns {void}
30
+ */
31
+ function exportToEnv(values, options)
32
+ {
33
+ if (!options.export) return;
34
+ for (const [key, value] of Object.entries(values))
35
+ {
36
+ if (!options.override && Object.prototype.hasOwnProperty.call(process.env, key))
37
+ {
38
+ continue;
39
+ }
40
+ process.env[key] = value === undefined ? '' : String(value);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Load .menv files and return parsed values with origins.
46
+ * @param {object} options
47
+ * @returns {{parsed: object, origins: object, files: string[]}}
48
+ */
49
+ function load(options = {})
50
+ {
51
+ const normalizedSchema = normalizeSchema(options.schema);
52
+ const cast = normalizeCast(options.cast);
53
+ const strict = Boolean(options.strict);
54
+
55
+ const state = buildState();
56
+ const files = resolveFiles(options);
57
+ const readFiles = [];
58
+
59
+ for (const filePath of files)
60
+ {
61
+ if (!fs.existsSync(filePath)) continue;
62
+ const text = fs.readFileSync(filePath, 'utf8');
63
+ const entries = parseEntries(text, { strict, filePath });
64
+ for (const entry of entries)
65
+ {
66
+ applyEntry(state, entry, {
67
+ schema: normalizedSchema,
68
+ strict,
69
+ cast,
70
+ onWarning: options.onWarning
71
+ }, filePath);
72
+ }
73
+ readFiles.push(filePath);
74
+ }
75
+
76
+ applySchemaDefaults(state.values, state.origins, normalizedSchema, strict);
77
+ exportToEnv(state.values, options);
78
+
79
+ if (options.freeze !== false)
80
+ {
81
+ deepFreeze(state.values);
82
+ }
83
+
84
+ return {
85
+ parsed: state.values,
86
+ origins: state.origins,
87
+ files: readFiles
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Parse a string containing .menv content.
93
+ * @param {string} text
94
+ * @param {object} options
95
+ * @returns {{parsed: object, origins: object, files: string[]}}
96
+ */
97
+ function parse(text, options = {})
98
+ {
99
+ const normalizedSchema = normalizeSchema(options.schema);
100
+ const cast = normalizeCast(options.cast);
101
+ const strict = Boolean(options.strict);
102
+ const state = buildState();
103
+ const filePath = options.filePath || '<inline>';
104
+
105
+ const entries = parseEntries(text, { strict, filePath });
106
+ for (const entry of entries)
107
+ {
108
+ applyEntry(state, entry, {
109
+ schema: normalizedSchema,
110
+ strict,
111
+ cast,
112
+ onWarning: options.onWarning
113
+ }, filePath);
114
+ }
115
+
116
+ applySchemaDefaults(state.values, state.origins, normalizedSchema, strict);
117
+
118
+ if (options.freeze !== false)
119
+ {
120
+ deepFreeze(state.values);
121
+ }
122
+
123
+ return {
124
+ parsed: state.values,
125
+ origins: state.origins,
126
+ files: options.filePath ? [options.filePath] : []
127
+ };
128
+ }
129
+
130
+ module.exports = {
131
+ load,
132
+ parse
133
+ };
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base error type for molex-env.
5
+ */
6
+ class MenvError extends Error
7
+ {
8
+ constructor(message, details)
9
+ {
10
+ super(message);
11
+ this.name = 'MenvError';
12
+ if (details)
13
+ {
14
+ this.details = details;
15
+ }
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Format a file/line suffix for error messages.
21
+ * @param {string} file
22
+ * @param {number} line
23
+ * @returns {string}
24
+ */
25
+ function formatLocation(file, line)
26
+ {
27
+ if (!file) return '';
28
+ if (!line) return ` (${file})`;
29
+ return ` (${file}:${line})`;
30
+ }
31
+
32
+ /**
33
+ * Create an invalid line error.
34
+ * @param {number} line
35
+ * @param {string} rawLine
36
+ * @param {string} file
37
+ * @returns {MenvError}
38
+ */
39
+ function invalidLineError(line, rawLine, file)
40
+ {
41
+ return new MenvError(`Invalid line ${line}: ${rawLine}${formatLocation(file)}`);
42
+ }
43
+
44
+ /**
45
+ * Create an unknown key error.
46
+ * @param {string} key
47
+ * @param {string} file
48
+ * @param {number} line
49
+ * @returns {MenvError}
50
+ */
51
+ function unknownKeyError(key, file, line)
52
+ {
53
+ return new MenvError(`Unknown key: ${key}${formatLocation(file, line)}`);
54
+ }
55
+
56
+ /**
57
+ * Create a duplicate key error.
58
+ * @param {string} key
59
+ * @param {string} file
60
+ * @param {number} line
61
+ * @returns {MenvError}
62
+ */
63
+ function duplicateKeyError(key, file, line)
64
+ {
65
+ return new MenvError(`Duplicate key: ${key}${formatLocation(file, line)}`);
66
+ }
67
+
68
+ /**
69
+ * Create a missing required key error.
70
+ * @param {string} key
71
+ * @returns {MenvError}
72
+ */
73
+ function missingRequiredError(key)
74
+ {
75
+ return new MenvError(`Missing required key: ${key}`);
76
+ }
77
+
78
+ /**
79
+ * Create an invalid type error.
80
+ * @param {string} type
81
+ * @param {string} raw
82
+ * @param {string} file
83
+ * @param {number} line
84
+ * @returns {MenvError}
85
+ */
86
+ function invalidTypeError(type, raw, file, line)
87
+ {
88
+ return new MenvError(`Invalid ${type}: ${raw}${formatLocation(file, line)}`);
89
+ }
90
+
91
+ module.exports = {
92
+ MenvError,
93
+ invalidLineError,
94
+ unknownKeyError,
95
+ duplicateKeyError,
96
+ missingRequiredError,
97
+ invalidTypeError
98
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Resolve .menv file paths based on options.
7
+ * @param {{cwd?: string, files?: string[], profile?: string}} options
8
+ * @returns {string[]}
9
+ */
10
+ function resolveFiles(options)
11
+ {
12
+ const cwd = options.cwd || process.cwd();
13
+ if (Array.isArray(options.files) && options.files.length > 0)
14
+ {
15
+ return options.files.map((file) => (path.isAbsolute(file) ? file : path.join(cwd, file)));
16
+ }
17
+
18
+ const files = [
19
+ path.join(cwd, '.menv'),
20
+ path.join(cwd, '.menv.local')
21
+ ];
22
+
23
+ if (options.profile)
24
+ {
25
+ files.push(
26
+ path.join(cwd, `.menv.${options.profile}`),
27
+ path.join(cwd, `.menv.${options.profile}.local`)
28
+ );
29
+ }
30
+
31
+ return files;
32
+ }
33
+
34
+ module.exports = {
35
+ resolveFiles
36
+ };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const { invalidLineError } = require('./errors');
4
+
5
+ /**
6
+ * Strip inline comments while preserving quoted values.
7
+ * @param {string} line
8
+ * @returns {string}
9
+ */
10
+ function stripInlineComment(line)
11
+ {
12
+ let inSingle = false;
13
+ let inDouble = false;
14
+ let escaped = false;
15
+ for (let i = 0; i < line.length; i += 1)
16
+ {
17
+ const ch = line[i];
18
+ if (escaped)
19
+ {
20
+ escaped = false;
21
+ continue;
22
+ }
23
+ if (ch === '\\')
24
+ {
25
+ escaped = true;
26
+ continue;
27
+ }
28
+ if (ch === '"' && !inSingle)
29
+ {
30
+ inDouble = !inDouble;
31
+ continue;
32
+ }
33
+ if (ch === "'" && !inDouble)
34
+ {
35
+ inSingle = !inSingle;
36
+ continue;
37
+ }
38
+ if (ch === '#' && !inSingle && !inDouble)
39
+ {
40
+ return line.slice(0, i);
41
+ }
42
+ }
43
+ return line;
44
+ }
45
+
46
+ /**
47
+ * Remove wrapping quotes and unescape common sequences.
48
+ * @param {string} value
49
+ * @returns {string}
50
+ */
51
+ function stripQuotes(value)
52
+ {
53
+ const trimmed = value.trim();
54
+ if (trimmed.length >= 2)
55
+ {
56
+ const first = trimmed[0];
57
+ const last = trimmed[trimmed.length - 1];
58
+ if ((first === '"' && last === '"') || (first === "'" && last === "'"))
59
+ {
60
+ return trimmed
61
+ .slice(1, -1)
62
+ .replace(/\\n/g, '\n')
63
+ .replace(/\\r/g, '\r')
64
+ .replace(/\\t/g, '\t')
65
+ .replace(/\\\\/g, '\\');
66
+ }
67
+ }
68
+ return trimmed;
69
+ }
70
+
71
+ /**
72
+ * Parse raw text into key/value entries.
73
+ * @param {string} text
74
+ * @param {{strict?: boolean, filePath?: string}} options
75
+ * @returns {{key: string, raw: string, line: number}[]}
76
+ */
77
+ function parseEntries(text, options)
78
+ {
79
+ const strict = Boolean(options.strict);
80
+ const filePath = options.filePath;
81
+ const lines = text.split(/\r?\n/);
82
+ const entries = [];
83
+
84
+ for (let i = 0; i < lines.length; i += 1)
85
+ {
86
+ const rawLine = lines[i];
87
+ const cleaned = stripInlineComment(rawLine);
88
+ if (!cleaned.trim()) continue;
89
+
90
+ const match = cleaned.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
91
+ if (!match)
92
+ {
93
+ if (strict)
94
+ {
95
+ throw invalidLineError(i + 1, rawLine, filePath);
96
+ }
97
+ continue;
98
+ }
99
+
100
+ const key = match[1];
101
+ const rawValue = stripQuotes(match[2] || '');
102
+
103
+ entries.push({
104
+ key,
105
+ raw: rawValue,
106
+ line: i + 1
107
+ });
108
+ }
109
+
110
+ return entries;
111
+ }
112
+
113
+ module.exports = {
114
+ parseEntries
115
+ };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const { missingRequiredError } = require('./errors');
4
+
5
+ /**
6
+ * Normalize schema definitions into objects.
7
+ * @param {object} schema
8
+ * @returns {object|null}
9
+ */
10
+ function normalizeSchema(schema)
11
+ {
12
+ if (!schema) return null;
13
+ const normalized = {};
14
+ for (const key of Object.keys(schema))
15
+ {
16
+ const def = schema[key];
17
+ if (typeof def === 'string')
18
+ {
19
+ normalized[key] = { type: def };
20
+ } else if (def && typeof def === 'object')
21
+ {
22
+ normalized[key] = { ...def };
23
+ }
24
+ }
25
+ return normalized;
26
+ }
27
+
28
+ /**
29
+ * Apply defaults and required checks from schema.
30
+ * @param {object} values
31
+ * @param {object} origins
32
+ * @param {object|null} schema
33
+ * @param {boolean} strict
34
+ * @returns {void}
35
+ */
36
+ function applySchemaDefaults(values, origins, schema, strict)
37
+ {
38
+ if (!schema) return;
39
+ for (const key of Object.keys(schema))
40
+ {
41
+ const def = schema[key];
42
+ if (values[key] === undefined)
43
+ {
44
+ if (def && Object.prototype.hasOwnProperty.call(def, 'default'))
45
+ {
46
+ values[key] = def.default;
47
+ origins[key] = { file: '<default>', line: 0, raw: undefined };
48
+ } else if (strict && def && def.required)
49
+ {
50
+ throw missingRequiredError(key);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ module.exports = {
57
+ normalizeSchema,
58
+ applySchemaDefaults
59
+ };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Recursively freeze an object graph.
5
+ * @param {object} obj
6
+ * @returns {object}
7
+ */
8
+ function deepFreeze(obj)
9
+ {
10
+ if (!obj || typeof obj !== 'object' || Object.isFrozen(obj)) return obj;
11
+ Object.freeze(obj);
12
+ for (const key of Object.keys(obj))
13
+ {
14
+ deepFreeze(obj[key]);
15
+ }
16
+ return obj;
17
+ }
18
+
19
+ module.exports = {
20
+ deepFreeze
21
+ };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { resolveFiles } = require('./files');
6
+
7
+ /**
8
+ * Watch resolved files and reload on changes.
9
+ * @param {object} options
10
+ * @param {(err: Error|null, result?: {parsed: object, origins: object, files: string[]}) => void} onChange
11
+ * @param {(options: object) => {parsed: object, origins: object, files: string[]}} load
12
+ * @returns {{close: () => void}}
13
+ */
14
+ function watch(options, onChange, load)
15
+ {
16
+ if (typeof onChange !== 'function')
17
+ {
18
+ throw new Error('onChange callback is required');
19
+ }
20
+ if (typeof load !== 'function')
21
+ {
22
+ throw new Error('load function is required');
23
+ }
24
+
25
+ const files = resolveFiles(options);
26
+ const cwd = options.cwd || process.cwd();
27
+ const basenames = new Set(files.map((file) => path.basename(file)));
28
+ const watchers = [];
29
+ let timer = null;
30
+
31
+ const trigger = () =>
32
+ {
33
+ if (timer) clearTimeout(timer);
34
+ timer = setTimeout(() =>
35
+ {
36
+ try
37
+ {
38
+ const result = load(options);
39
+ onChange(null, result);
40
+ } catch (err)
41
+ {
42
+ onChange(err);
43
+ }
44
+ }, 50);
45
+ };
46
+
47
+ for (const filePath of files)
48
+ {
49
+ if (!fs.existsSync(filePath)) continue;
50
+ watchers.push(fs.watch(filePath, trigger));
51
+ }
52
+
53
+ if (options.watchMissing !== false)
54
+ {
55
+ watchers.push(fs.watch(cwd, (event, filename) =>
56
+ {
57
+ if (!filename) return;
58
+ if (basenames.has(filename))
59
+ {
60
+ trigger();
61
+ }
62
+ }));
63
+ }
64
+
65
+ return {
66
+ close()
67
+ {
68
+ watchers.forEach((watcher) => watcher.close());
69
+ watchers.length = 0;
70
+ }
71
+ };
72
+ }
73
+
74
+ module.exports = {
75
+ watch
76
+ };