molex-env 0.3.2 → 0.3.3

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/src/lib/cast.js CHANGED
@@ -1,124 +1,133 @@
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
- };
1
+ 'use strict';
2
+
3
+ const { invalidTypeError } = require('./errors');
4
+
5
+ /* ------------------------------------------------------------------ */
6
+ /* Constants */
7
+ /* ------------------------------------------------------------------ */
8
+
9
+ const CAST_ALL_ON = { boolean: true, number: true, json: true, date: true };
10
+ const CAST_ALL_OFF = { boolean: false, number: false, json: false, date: false };
11
+ const NUMBER_RE = /^-?\d+(\.\d+)?$/;
12
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/;
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Helpers */
16
+ /* ------------------------------------------------------------------ */
17
+
18
+ /** @returns {boolean} */
19
+ function isNumber(value)
20
+ {
21
+ return NUMBER_RE.test(value);
22
+ }
23
+
24
+ /** @returns {boolean} */
25
+ function isIsoDate(value)
26
+ {
27
+ return ISO_DATE_RE.test(value);
28
+ }
29
+
30
+ /* ------------------------------------------------------------------ */
31
+ /* Public API */
32
+ /* ------------------------------------------------------------------ */
33
+
34
+ /**
35
+ * Normalize cast options into explicit booleans.
36
+ * @param {boolean|{boolean?: boolean, number?: boolean, json?: boolean, date?: boolean}} cast
37
+ * @returns {{boolean: boolean, number: boolean, json: boolean, date: boolean}}
38
+ */
39
+ function normalizeCast(cast)
40
+ {
41
+ if (cast === true || cast === undefined) return { ...CAST_ALL_ON };
42
+ if (cast === false) return { ...CAST_ALL_OFF };
43
+ return {
44
+ boolean: cast.boolean !== false,
45
+ number: cast.number !== false,
46
+ json: cast.json !== false,
47
+ date: cast.date !== false,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Coerce a raw string to the requested schema type.
53
+ * @param {string} raw
54
+ * @param {string} type
55
+ * @param {string} file
56
+ * @param {number} line
57
+ * @returns {any}
58
+ */
59
+ function coerceType(raw, type, file, line)
60
+ {
61
+ switch (type)
62
+ {
63
+ case 'string':
64
+ return raw;
65
+
66
+ case 'boolean': {
67
+ const lower = raw.toLowerCase();
68
+ if (lower === 'true') return true;
69
+ if (lower === 'false') return false;
70
+ throw invalidTypeError('boolean', raw, file, line);
71
+ }
72
+
73
+ case 'number':
74
+ if (!isNumber(raw)) throw invalidTypeError('number', raw, file, line);
75
+ return Number(raw);
76
+
77
+ case 'json':
78
+ return JSON.parse(raw);
79
+
80
+ case 'date': {
81
+ const date = new Date(raw);
82
+ if (Number.isNaN(date.getTime())) throw invalidTypeError('date', raw, file, line);
83
+ return date;
84
+ }
85
+
86
+ default:
87
+ return raw;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Auto-cast a raw string based on enabled casting rules.
93
+ * @param {string} raw
94
+ * @param {{boolean: boolean, number: boolean, json: boolean, date: boolean}} cast
95
+ * @returns {any}
96
+ */
97
+ function autoCast(raw, cast)
98
+ {
99
+ const trimmed = raw.trim();
100
+
101
+ if (cast.boolean)
102
+ {
103
+ const lower = trimmed.toLowerCase();
104
+ if (lower === 'true') return true;
105
+ if (lower === 'false') return false;
106
+ }
107
+
108
+ if (cast.number && isNumber(trimmed))
109
+ {
110
+ return Number(trimmed);
111
+ }
112
+
113
+ if (cast.json && (trimmed.startsWith('{') || trimmed.startsWith('[')))
114
+ {
115
+ try
116
+ {
117
+ return JSON.parse(trimmed);
118
+ } catch (_err)
119
+ {
120
+ return trimmed;
121
+ }
122
+ }
123
+
124
+ if (cast.date && isIsoDate(trimmed))
125
+ {
126
+ const date = new Date(trimmed);
127
+ if (!Number.isNaN(date.getTime())) return date;
128
+ }
129
+
130
+ return trimmed;
131
+ }
132
+
133
+ module.exports = { normalizeCast, coerceType, autoCast };
package/src/lib/core.js CHANGED
@@ -1,148 +1,161 @@
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, seenPerFile: Map<string, Set<string>>}}
15
- */
16
- function buildState()
17
- {
18
- return {
19
- values: {},
20
- origins: {},
21
- seenPerFile: new Map()
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.exportEnv) 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
- * Attach parsed values to process.menv unless disabled.
46
- * @param {object} values
47
- * @param {object} options
48
- * @returns {void}
49
- */
50
- function attachToProcess(values, options)
51
- {
52
- if (options.attach === false) return;
53
- process.menv = values;
54
- }
55
-
56
- /**
57
- * Load .menv files and return parsed values with origins.
58
- * @param {object} options
59
- * @returns {{parsed: object, origins: object, files: string[]}}
60
- */
61
- function load(options = {})
62
- {
63
- const normalizedSchema = normalizeSchema(options.schema);
64
- const cast = normalizeCast(options.cast);
65
- const strict = Boolean(options.strict);
66
-
67
- const state = buildState();
68
- const files = resolveFiles(options);
69
- const readFiles = [];
70
-
71
- for (const filePath of files)
72
- {
73
- if (!fs.existsSync(filePath)) continue;
74
- const text = fs.readFileSync(filePath, 'utf8');
75
- const entries = parseEntries(text, { strict, filePath });
76
- for (const entry of entries)
77
- {
78
- applyEntry(state, entry, {
79
- schema: normalizedSchema,
80
- strict,
81
- cast,
82
- onWarning: options.onWarning,
83
- debug: options.debug
84
- }, filePath);
85
- }
86
- readFiles.push(filePath);
87
- }
88
-
89
- applySchemaDefaults(state.values, state.origins, normalizedSchema, strict);
90
- exportToEnv(state.values, options);
91
-
92
- if (options.freeze !== false)
93
- {
94
- deepFreeze(state.values);
95
- }
96
-
97
- attachToProcess(state.values, options);
98
-
99
- return {
100
- parsed: state.values,
101
- origins: state.origins,
102
- files: readFiles
103
- };
104
- }
105
-
106
- /**
107
- * Parse a string containing .menv content.
108
- * @param {string} text
109
- * @param {object} options
110
- * @returns {{parsed: object, origins: object, files: string[]}}
111
- */
112
- function parse(text, options = {})
113
- {
114
- const normalizedSchema = normalizeSchema(options.schema);
115
- const cast = normalizeCast(options.cast);
116
- const strict = Boolean(options.strict);
117
- const state = buildState();
118
- const filePath = options.filePath || '<inline>';
119
-
120
- const entries = parseEntries(text, { strict, filePath });
121
- for (const entry of entries)
122
- {
123
- applyEntry(state, entry, {
124
- schema: normalizedSchema,
125
- strict,
126
- cast,
127
- onWarning: options.onWarning
128
- }, filePath);
129
- }
130
-
131
- applySchemaDefaults(state.values, state.origins, normalizedSchema, strict);
132
-
133
- if (options.freeze !== false)
134
- {
135
- deepFreeze(state.values);
136
- }
137
-
138
- return {
139
- parsed: state.values,
140
- origins: state.origins,
141
- files: options.filePath ? [options.filePath] : []
142
- };
143
- }
144
-
145
- module.exports = {
146
- load,
147
- parse
148
- };
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
+ /* Internal helpers (shared pipeline) */
14
+ /* ------------------------------------------------------------------ */
15
+
16
+ /**
17
+ * Create a fresh processing state container.
18
+ * @returns {{values: object, raw: object, origins: object, seenPerFile: Map}}
19
+ */
20
+ function createState()
21
+ {
22
+ return {
23
+ values: {},
24
+ raw: {},
25
+ origins: {},
26
+ seenPerFile: new Map(),
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Normalize user options into a consistent internal format.
32
+ * @param {object} options - Raw user options.
33
+ * @returns {object} Normalized processing options.
34
+ */
35
+ function normalizeOptions(options)
36
+ {
37
+ return {
38
+ schema: normalizeSchema(options.schema),
39
+ cast: normalizeCast(options.cast),
40
+ strict: Boolean(options.strict),
41
+ freeze: options.freeze !== false,
42
+ onWarning: options.onWarning,
43
+ debug: options.debug,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Process a block of .menv text into the state.
49
+ * @param {object} state
50
+ * @param {string} text
51
+ * @param {string} filePath
52
+ * @param {object} opts - Normalized options.
53
+ */
54
+ function processText(state, text, filePath, opts)
55
+ {
56
+ const entries = parseEntries(text, { strict: opts.strict, filePath });
57
+ for (const entry of entries)
58
+ {
59
+ applyEntry(state, entry, opts, filePath);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Apply schema defaults and optionally deep-freeze the result.
65
+ * @param {object} state
66
+ * @param {object} opts - Normalized options.
67
+ */
68
+ function finalizeState(state, opts)
69
+ {
70
+ applySchemaDefaults(state.values, state.origins, opts.schema, opts.strict);
71
+ if (opts.freeze) deepFreeze(state.values);
72
+ }
73
+
74
+ /* ------------------------------------------------------------------ */
75
+ /* Side-effect helpers */
76
+ /* ------------------------------------------------------------------ */
77
+
78
+ /**
79
+ * Write parsed values to process.env when enabled.
80
+ * @param {object} values
81
+ * @param {object} options - Raw user options.
82
+ */
83
+ function exportToEnv(values, options)
84
+ {
85
+ if (!options.exportEnv) return;
86
+ for (const [key, value] of Object.entries(values))
87
+ {
88
+ if (!options.override && Object.prototype.hasOwnProperty.call(process.env, key)) continue;
89
+ process.env[key] = value === undefined ? '' : String(value);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Attach parsed values to process.menv unless disabled.
95
+ * @param {object} values
96
+ * @param {object} options - Raw user options.
97
+ */
98
+ function attachToProcess(values, options)
99
+ {
100
+ if (options.attach !== false) process.menv = values;
101
+ }
102
+
103
+ /* ------------------------------------------------------------------ */
104
+ /* Public API */
105
+ /* ------------------------------------------------------------------ */
106
+
107
+ /**
108
+ * Load .menv files, merge, parse, and validate.
109
+ * @param {object} [options]
110
+ * @returns {{parsed: object, raw: object, origins: object, files: string[]}}
111
+ */
112
+ function load(options = {})
113
+ {
114
+ const opts = normalizeOptions(options);
115
+ const state = createState();
116
+ const filePaths = resolveFiles(options);
117
+ const readFiles = [];
118
+
119
+ for (const filePath of filePaths)
120
+ {
121
+ if (!fs.existsSync(filePath)) continue;
122
+ processText(state, fs.readFileSync(filePath, 'utf8'), filePath, opts);
123
+ readFiles.push(filePath);
124
+ }
125
+
126
+ finalizeState(state, opts);
127
+ exportToEnv(state.values, options);
128
+ attachToProcess(state.values, options);
129
+
130
+ return {
131
+ parsed: state.values,
132
+ raw: state.raw,
133
+ origins: state.origins,
134
+ files: readFiles,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Parse a string of .menv content without loading files.
140
+ * @param {string} text
141
+ * @param {object} [options]
142
+ * @returns {{parsed: object, raw: object, origins: object, files: string[]}}
143
+ */
144
+ function parse(text, options = {})
145
+ {
146
+ const opts = normalizeOptions(options);
147
+ const state = createState();
148
+ const filePath = options.filePath || '<inline>';
149
+
150
+ processText(state, text, filePath, opts);
151
+ finalizeState(state, opts);
152
+
153
+ return {
154
+ parsed: state.values,
155
+ raw: state.raw,
156
+ origins: state.origins,
157
+ files: options.filePath ? [options.filePath] : [],
158
+ };
159
+ }
160
+
161
+ module.exports = { load, parse };