xenv-config 2.1.7 → 2.1.8

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
@@ -1,7 +1,16 @@
1
- [![NPM version][npm-image]][npm-url]
2
-
3
1
  # xenv-config
4
2
 
3
+ [![License][license-image]][license-url]
4
+ [![build][build-image]][build-url]
5
+
6
+ [![Downloads][downloads-image]][downloads-url]
7
+
8
+ [![npm badge][npm-badge-png]][package-url]
9
+
10
+ - [API reference](https://fynjs.pages.dev/api/modules/xenv-config) - TypeDoc
11
+ - [Markdown reference](docs/reference.md) - the full API and runtime behavior in one file. Also ships in the package.
12
+ - [GitHub](https://github.com/jchip/fynjs/tree/main/packages/xenv-config)
13
+
5
14
  Load config from env, user config, or default spec.
6
15
 
7
16
  ## Install
@@ -133,5 +142,11 @@ The order of source to check are:
133
142
  3. The default value from spec if it's declared
134
143
  4. Nothing
135
144
 
136
- [npm-image]: https://badge.fury.io/js/xenv-config.svg
137
- [npm-url]: https://npmjs.org/package/xenv-config
145
+ [license-image]: https://img.shields.io/npm/l/xenv-config.svg
146
+ [license-url]: LICENSE
147
+ [build-image]: https://github.com/jchip/fynjs/actions/workflows/ci.yml/badge.svg
148
+ [build-url]: https://github.com/jchip/fynjs/actions/workflows/ci.yml
149
+ [downloads-image]: https://img.shields.io/npm/dm/xenv-config.svg
150
+ [downloads-url]: https://npm-stat.com/charts.html?package=xenv-config
151
+ [npm-badge-png]: https://nodei.co/npm/xenv-config.png?downloads=true&stars=true
152
+ [package-url]: https://npmjs.com/package/xenv-config
package/dist/index.js CHANGED
@@ -78,8 +78,7 @@ const xenvConfig = (spec, userConfig, options) => {
78
78
  return undefined;
79
79
  }
80
80
  };
81
- const sources = (options && options.sources) || ["env", "option"];
82
- sources.push("default");
81
+ const sources = [...((options && options.sources) || ["env", "option"]), "default"];
83
82
  const config = Object.keys(spec).reduce((cfg, k) => {
84
83
  const opt = spec[k];
85
84
  const type = getType(opt);
@@ -0,0 +1,237 @@
1
+ # xenv-config reference
2
+
3
+ `xenv-config` builds a config object from a spec. Each key is resolved from an env var, a user config object, or a default, and every resolved key gets a trace record that says where it came from. No runtime dependencies. ESM only (`"type": "module"`).
4
+
5
+ ## Imports
6
+
7
+ ```js
8
+ import xenvConfig from "xenv-config";
9
+ ```
10
+
11
+ ```ts
12
+ import xenvConfig, {
13
+ type XenvConfigTrace,
14
+ type XenvConfigSpecOption,
15
+ type XenvConfigSpec,
16
+ type XenvConfigEnv,
17
+ type XenvConfigUserConfig,
18
+ type XenvConfigMergeFn,
19
+ type XenvConfigOptions,
20
+ type XenvConfigResult
21
+ } from "xenv-config";
22
+ ```
23
+
24
+ The only runtime export is the default export, the function `xenvConfig`. There are no named runtime exports and no subpaths (only `xenv-config/package.json`). The package has no CommonJS build, so `require("xenv-config")` as shown in the README does not apply to this version; use `import`.
25
+
26
+ Type-only exports: `XenvConfigTrace`, `XenvConfigSpecOption`, `XenvConfigSpec`, `XenvConfigEnv`, `XenvConfigUserConfig`, `XenvConfigMergeFn`, `XenvConfigOptions`, `XenvConfigResult`.
27
+
28
+ Not exported: the internal `typeGetters` table, `getEnvName` and `FoundValue`.
29
+
30
+ ## `xenvConfig(spec, userConfig?, options?)`
31
+
32
+ ```ts
33
+ const xenvConfig: <T = Record<string, unknown>>(
34
+ spec: XenvConfigSpec,
35
+ userConfig?: XenvConfigUserConfig,
36
+ options?: XenvConfigOptions
37
+ ) => XenvConfigResult<T>;
38
+ ```
39
+
40
+ Resolves every key of `spec` and returns a new plain object. It is synchronous. `T` is only a type cast of the result; nothing is checked against it.
41
+
42
+ | Parameter | Type | Default | Behavior |
43
+ | --- | --- | --- | --- |
44
+ | `spec` | `XenvConfigSpec` | required | Keys are iterated with `Object.keys(spec)`. Each value is a `XenvConfigSpecOption`. `undefined` or `null` `spec` throws a `TypeError`. |
45
+ | `userConfig` | `XenvConfigUserConfig` | `{}` | Values supplied by the caller. A falsy value is replaced with `{}`. |
46
+ | `options` | `XenvConfigOptions` | none | See `XenvConfigOptions`. |
47
+
48
+ For each spec key the function:
49
+
50
+ 1. Finds a value by walking the sources (see Source order).
51
+ 2. If nothing is found, the key is skipped. It is absent from the result and from `__$trace__`. `post` is not called.
52
+ 3. Otherwise records the trace, runs `post` if the spec has one, and stores the result.
53
+
54
+ ### Source order
55
+
56
+ Three sources exist, each named by a string:
57
+
58
+ | Source name | Looks in | Found when | Trace |
59
+ | --- | --- | --- | --- |
60
+ | `"env"` | env object (`options._env` or `process.env`) | the spec has an own `env` property that names a variable that is an own property of the env object | `{ src: "env", name }` |
61
+ | `"option"` | `userConfig` | `userConfig` has the key as an own property | `{ src: "option" }` |
62
+ | `"default"` | spec | the spec has an own `default` property | `{ src: "default" }` |
63
+
64
+ The default order is `["env", "option"]`, and `"default"` is appended last. For non-json types the first source that finds a value wins and the rest are not consulted.
65
+
66
+ Reorder with `options.sources`. Example: `sources: ["option", "env"]` makes user config beat env. A source left out of the list is never consulted, except `"default"`, which is always appended. Rules:
67
+
68
+ - Presence is tested with `Object.prototype.hasOwnProperty`. A present key whose value is `undefined`, `null`, `""` or `false` still counts as found. An empty string env var is found.
69
+ - Inherited properties are not seen, on `spec` entries, `userConfig` and the env object.
70
+ - `options.sources` is mutated. The function calls `push("default")` on the array you pass in. See Quirks.
71
+ - A source name other than `"env"`, `"option"` or `"default"` throws a `TypeError` (`getters[s] is not a function`) when that key is resolved.
72
+ - The `"default"` entry: if `default` is a function it is called with no arguments and its return value is used. A function is never type-coerced.
73
+
74
+ ### Env lookup
75
+
76
+ The env var name is chosen from `spec[key].env`, which is read only when `env` is an own property of the spec option.
77
+
78
+ | `env` value | Name used |
79
+ | --- | --- |
80
+ | absent, `false`, `""` | none, the env source finds nothing |
81
+ | `true` | the spec key itself |
82
+ | `string` | that exact name |
83
+ | `string[]` | the first name in the list that is an own property of the env object |
84
+
85
+ If the chosen name is not an own property of the env object, the env source finds nothing. For an array, the match requires own presence, so a present empty string wins over a later name. An empty array finds nothing.
86
+
87
+ When a name is found, the raw value is `env[name]`, then:
88
+
89
+ 1. If `envMap` is set and has the raw value as an own key, the mapped value is used as is. No type coercion is applied to it (`post` still runs). The raw value is used as a property key, so a non-string raw value is converted to a string for the lookup.
90
+ 2. Otherwise the raw value is coerced by the key's type (see Types).
91
+
92
+ ## `XenvConfigSpecOption`
93
+
94
+ ```ts
95
+ interface XenvConfigSpecOption {
96
+ env?: string | string[] | boolean;
97
+ envMap?: Record<string, unknown>;
98
+ type?: string;
99
+ default?: unknown;
100
+ post?: (value: unknown, trace: XenvConfigTrace) => unknown;
101
+ [extra: string]: unknown;
102
+ }
103
+ ```
104
+
105
+ All fields are optional. A spec option with none of them resolves only from `userConfig`. Extra properties are allowed by the type and ignored by the code.
106
+
107
+ | Field | Type | Default | Behavior |
108
+ | --- | --- | --- | --- |
109
+ | `env` | `string \| string[] \| boolean` | none | Which env var to read. See Env lookup. |
110
+ | `envMap` | `Record<string, unknown>` | none | Maps raw env strings to final values. Only applies to the env source. Checked before type coercion. A hit skips coercion. |
111
+ | `type` | `string` | inferred | How to coerce an env string. See Types. Applies only to env values, not to `userConfig` values or defaults, except that `json` also changes how all sources combine. |
112
+ | `default` | `unknown` | none | Fallback value, or a function that returns it. Presence is tested as an own property, so `default: undefined` counts as a default and yields `undefined`. A function default is called each time `xenvConfig` runs. Not coerced. |
113
+ | `post` | `(value, trace) => unknown` | none | Runs on the resolved value after a source is found. Its return value is stored in the result. Receives the same trace object that is stored in `__$trace__`, so mutating it changes the stored trace. Not called when the key is not found. Runs for every source, including `option` and `default`. |
114
+
115
+ ## Types
116
+
117
+ The type for a key is decided by `type` first, then inference:
118
+
119
+ 1. `opt.type` if truthy.
120
+ 2. Else `typeof opt.default` when `default` is an own property and its `typeof` is not `"function"`.
121
+ 3. Else `"string"`.
122
+
123
+ Then `"object"` is rewritten to `"json"`. A type name not listed below falls back to the `string` converter. Only `"json"` (after the rewrite) triggers merge behavior.
124
+
125
+ | Type | Converter for an env value `x` | Notes |
126
+ | --- | --- | --- |
127
+ | `string` | `` `${x}` `` | Also the fallback for any unknown type name. |
128
+ | `number` | `parseInt(x, 10)` | Integer. A non-numeric string gives `NaN` (JSON serializes it as `null`). `"1.9"` gives `1`. |
129
+ | `float` | `parseFloat(x)` | |
130
+ | `boolean` | lower-cased `x` is `"true"`, `"yes"`, `"1"` or `"on"` | Case-insensitive. Any other string gives `false`. A non-string raw value throws (see Errors). |
131
+ | `truthy` | `!!x` | An empty string gives `false`. Any non-empty string, including `"false"` and `"0"`, gives `true`. |
132
+ | `json` | `JSON.parse(x)` | Needs strict JSON. `{b:90}` is invalid. See json merging. |
133
+
134
+ Inference from `default`:
135
+
136
+ | `typeof default` | Inferred type |
137
+ | --- | --- |
138
+ | `"string"` | `string` |
139
+ | `"number"` | `number` (integer, `parseInt`) |
140
+ | `"boolean"` | `boolean` |
141
+ | `"object"` | `json` (this includes `null` and arrays) |
142
+ | `"undefined"` (own property set to `undefined`) | no converter of that name, so `string` conversion |
143
+ | `"function"`, or no `default` | `string` |
144
+
145
+ An explicit `type` always overrides inference. `type: "object"` is accepted as an alias of `json`.
146
+
147
+ ### json merging
148
+
149
+ When the resolved type is `json`, sources are not first-wins. Every source in `sources` (plus `"default"`) is consulted. All found values are merged.
150
+
151
+ - The env value is parsed with `JSON.parse`, or taken from `envMap` when a mapping hits.
152
+ - Merge call: `merge({}, ...values)` where values are ordered lowest priority first. With the default order the call is `merge({}, defaultValue, optionValue, envValue)`, so env wins, then option, then default. With a custom `sources`, the earlier a source is in the list, the higher its priority.
153
+ - `merge` is `options.merge` or `Object.assign` (shallow). Pass a deep merge such as lodash `merge` for nested objects.
154
+ - The result is always a new object, so defaults and user config are not mutated by the merge.
155
+ - If no source finds a value, the key is skipped.
156
+ - Values are merged as given. With `Object.assign`, a string merges as indexed characters, an array as indexed keys, and `null` or `undefined` is ignored.
157
+ - The trace is `{ src: "<names joined by comma>" }`, in `sources` order, only for the sources that found a value, for example `"env,option,default"` or `"option,default"`. It has no `name` field, even when env contributed.
158
+
159
+ ## `XenvConfigOptions`
160
+
161
+ ```ts
162
+ interface XenvConfigOptions {
163
+ _env?: XenvConfigEnv;
164
+ merge?: XenvConfigMergeFn;
165
+ sources?: string[];
166
+ }
167
+ ```
168
+
169
+ | Option | Type | Default | Behavior |
170
+ | --- | --- | --- | --- |
171
+ | `_env` | `XenvConfigEnv` (`Record<string, unknown>`) | `process.env` | Env-like object to read. Used when truthy. Values need not be strings, but `boolean` rejects non-strings. |
172
+ | `merge` | `XenvConfigMergeFn` | `Object.assign` | Called as `merge.apply(null, [{}, ...values])` for json keys. Must return the merged object. Its return value is used as the key's value. |
173
+ | `sources` | `string[]` | `["env", "option"]` | Ordered source names to consult. `"default"` is always appended, after your list. The array is mutated. |
174
+
175
+ ## Result
176
+
177
+ ```ts
178
+ type XenvConfigResult<T = Record<string, unknown>> = T & {
179
+ readonly __$trace__: Record<string, XenvConfigTrace>;
180
+ };
181
+
182
+ interface XenvConfigTrace {
183
+ src: string;
184
+ name?: string;
185
+ }
186
+ ```
187
+
188
+ The result is a plain `{}` with one property per resolved spec key, in spec key order.
189
+
190
+ - `__$trace__` is defined with `Object.defineProperty`: `enumerable: false`, `writable: false`, `configurable: false`. It is skipped by `Object.keys`, spread, `JSON.stringify` and deep-equal checks, but is read as `config.__$trace__`. Copying the object with spread or `Object.assign` drops it.
191
+ - `__$trace__[key]` exists exactly when `key` is in the result.
192
+ - `trace.src` is `"env"`, `"option"`, `"default"`, or for json keys a comma-joined list of those.
193
+ - `trace.name` is set only for non-json keys resolved from env. It holds the env var name actually used, which is useful with an `env` array.
194
+ - Values from `userConfig` and `default` are stored by reference, not cloned (non-json keys).
195
+
196
+ ```js
197
+ const config = xenvConfig(
198
+ {
199
+ port: { env: ["APP_PORT", "PORT"], type: "number", default: 8080 },
200
+ debug: { env: true, type: "boolean" },
201
+ opts: { env: "OPTS", type: "json", default: { a: 1 } }
202
+ },
203
+ { opts: { b: 2 } },
204
+ { _env: { PORT: "9000", OPTS: '{"c":3}' } }
205
+ );
206
+ // config: { port: 9000, opts: { a: 1, b: 2, c: 3 } } (debug is absent)
207
+ // config.__$trace__:
208
+ // port: { src: "env", name: "PORT" }
209
+ // opts: { src: "env,option,default" }
210
+ ```
211
+
212
+ ## Errors
213
+
214
+ Nothing is caught or wrapped. All errors are thrown synchronously from `xenvConfig`.
215
+
216
+ | Condition | Error |
217
+ | --- | --- |
218
+ | `boolean` type and the env value (or `_env` value) is not a string | `AssertionError` from `node:assert`, message `xenv-config: [<key>] trying to convert non-string env value <value> to boolean.` |
219
+ | `json` type and the env string is not valid JSON | `SyntaxError` from `JSON.parse` |
220
+ | `sources` contains an unknown name | `TypeError` (`getters[s] is not a function`) |
221
+ | `spec` is `undefined` or `null` | `TypeError` from `Object.keys` |
222
+ | A function `default`, `post`, or `merge` throws | the error propagates unchanged |
223
+ | `type: "json"` where `x` is not a string, such as a non-string `_env` value | `JSON.parse` coerces it to a string first, so it may parse or throw `SyntaxError` |
224
+
225
+ Absence of a value is not an error. Missing keys are left out of the result.
226
+
227
+ ## Quirks
228
+
229
+ - `options.sources` is mutated by `push("default")`. Reusing the same array in a later call adds another `"default"`. For json keys this repeats the default in the merge and in the trace (`"option,default,default"`). Pass a fresh array each call.
230
+ - `type: "json"` needs strict JSON in env. The README example `JSON_OPTION = "{b:90}"` throws `SyntaxError`; use `'{"b":90}'`.
231
+ - `default: null` or an array default infers type `json`, so the key resolves to a new merged object (`{}` for `null`, `{0: ...}` for an array), not to `null` or the array.
232
+ - `number` env values that are not integers give `NaN` rather than an error. Empty env strings give `NaN`.
233
+ - `envMap` hits are returned as is, with no coercion by `type`.
234
+ - `userConfig` values and defaults are never coerced, only env values are.
235
+ - A key present in the env object with value `undefined` counts as found. For `string` type it yields the string `"undefined"`.
236
+ - The README says the order is env, option, default. The code matches this by default, but only `"default"` is forced last. The README does not document `options.sources`.
237
+ - The README says `boolean` is `x === "true" || ...`. The code lower-cases first, so `"TRUE"` is `true`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xenv-config",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "description": "Load config from env, user config, or default spec",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -16,7 +16,7 @@
16
16
  "engines": {
17
17
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
18
18
  },
19
- "homepage": "https://github.com/jchip/fynjs/tree/main/packages/xenv-config",
19
+ "homepage": "https://fynjs.pages.dev/api/modules/xenv-config",
20
20
  "bugs": {
21
21
  "url": "https://github.com/jchip/fynjs/issues"
22
22
  },
@@ -43,7 +43,8 @@
43
43
  "author": "Joel Chen",
44
44
  "license": "Apache-2.0",
45
45
  "files": [
46
- "dist"
46
+ "dist",
47
+ "docs/reference.md"
47
48
  ],
48
49
  "devDependencies": {
49
50
  "@types/lodash-es": "^4.17.12",
@@ -51,7 +52,7 @@
51
52
  "@vitest/coverage-v8": "^5.0.0",
52
53
  "lodash-es": "^4.18.1",
53
54
  "prettier": "^3.5.3",
54
- "publish-util": "^3.1.7",
55
+ "publish-util": "^3.1.8",
55
56
  "typescript": "^7.0.2",
56
57
  "vite": "^8.2.2",
57
58
  "vitest": "^5.0.0"