xenv-config 2.1.8 → 3.0.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/README.md CHANGED
@@ -45,7 +45,7 @@ expect(config.__$trace__).to.deep.equal({
45
45
  fooOption: { src: "default" },
46
46
  barOption: { src: "env", name: "BAR_OPTION" },
47
47
  zooOption: { src: "option" },
48
- jsonOption: { src: "env,option" }
48
+ jsonOption: { src: "env,option", name: "JSON_OPTION" }
49
49
  });
50
50
  ```
51
51
 
@@ -68,6 +68,7 @@ Returns `config` object.
68
68
 
69
69
  - `_env` - Object that represents environment instead of `process.env`
70
70
  - `merge` - function to merge `json` object instead of using `Object.assign`
71
+ - `sources` - ordered list of sources to check, from `env`, `option` and `default`. Default is `["env", "option"]`, and `default` is always checked last. An unknown name throws.
71
72
 
72
73
  ## Spec
73
74
 
@@ -113,8 +114,9 @@ When loading from `env`, in order to indicate what value to convert the string i
113
114
  - `boolean` - (boolean) convert with `x === "true" || x === "yes" || x === "1" || x === "on"`
114
115
  - `truthy` - (boolean from truthy check) convert with `!!x`
115
116
  - `json` - (JSON) parsed with `JSON.parse`
117
+ - `array` - (array) parsed with `JSON.parse`. The first source found wins, and nothing is merged.
116
118
 
117
- > If `type` is not specified, and `default` exist and not a function, then `typeof default` will be used.
119
+ > If `type` is not specified, and `default` exist and not a function, then `typeof default` will be used. A `null` default is `string`, and an array default is `array`: env is parsed with `JSON.parse`, and the first source found wins with no merging.
118
120
 
119
121
  ### Trace
120
122
 
@@ -132,6 +134,7 @@ The hidden field `__$trace__` contain data for each key to indicate where its va
132
134
  - Values from all three sources are combined in the following order `env`, `option`, `default`.
133
135
  - It uses `Object.assign` to combine values unless you pass in a `merge` function in options. For example, merge from `lodash`.
134
136
  - `trace.src` would be a comma separate list of them. For example, `"env,option"`, `"env,option,default"`, or `"option,default"`
137
+ - When env contributed, the trace also has `name`, the env var name used.
135
138
 
136
139
  ## Option Orders
137
140
 
package/dist/index.js CHANGED
@@ -9,7 +9,8 @@ const typeGetters = {
9
9
  return lower === "true" || lower === "yes" || lower === "1" || lower === "on";
10
10
  },
11
11
  truthy: x => !!x,
12
- json: x => JSON.parse(x)
12
+ json: x => JSON.parse(x),
13
+ array: x => JSON.parse(x)
13
14
  };
14
15
  const getEnvName = (optKey, opt, env) => {
15
16
  if (!Object.prototype.hasOwnProperty.call(opt, "env"))
@@ -40,9 +41,17 @@ const xenvConfig = (spec, userConfig, options) => {
40
41
  userConfig = userConfig || {};
41
42
  const trace = {};
42
43
  const getType = (opt) => {
43
- const tod = Object.prototype.hasOwnProperty.call(opt, "default") && typeof opt.default;
44
- const type = opt.type || (tod !== "function" && tod) || "string";
45
- return type === "object" ? "json" : type;
44
+ if (opt.type)
45
+ return opt.type === "object" ? "json" : opt.type;
46
+ if (!Object.prototype.hasOwnProperty.call(opt, "default"))
47
+ return "string";
48
+ // typeof says "object" for both, but neither is a json object to merge into
49
+ if (opt.default === null)
50
+ return "string";
51
+ if (Array.isArray(opt.default))
52
+ return "array";
53
+ const tod = typeof opt.default;
54
+ return tod === "object" ? "json" : tod === "function" ? "string" : tod;
46
55
  };
47
56
  const getGetter = (opt) => {
48
57
  const type = getType(opt);
@@ -79,7 +88,12 @@ const xenvConfig = (spec, userConfig, options) => {
79
88
  }
80
89
  };
81
90
  const sources = [...((options && options.sources) || ["env", "option"]), "default"];
91
+ const badSource = sources.find(s => !Object.prototype.hasOwnProperty.call(getters, s));
92
+ if (badSource !== undefined) {
93
+ throw new Error(`xenv-config: unknown source "${badSource}"; valid sources are ${Object.keys(getters).join(", ")}`);
94
+ }
82
95
  const config = Object.keys(spec).reduce((cfg, k) => {
96
+ var _a;
83
97
  const opt = spec[k];
84
98
  const type = getType(opt);
85
99
  let found;
@@ -89,8 +103,9 @@ const xenvConfig = (spec, userConfig, options) => {
89
103
  else {
90
104
  const mapped = sources.map(s => getters[s](opt, k)).filter((x) => Boolean(x));
91
105
  if (mapped.length > 0) {
106
+ const name = (_a = mapped.find(x => x.trace.name)) === null || _a === void 0 ? void 0 : _a.trace.name;
92
107
  found = {
93
- trace: { src: mapped.map(x => x.trace.src).join(",") },
108
+ trace: { src: mapped.map(x => x.trace.src).join(","), ...(name && { name }) },
94
109
  value: merge.apply(null, [{}].concat(mapped.map(x => x.value).reverse()))
95
110
  };
96
111
  }
package/docs/reference.md CHANGED
@@ -67,8 +67,8 @@ Reorder with `options.sources`. Example: `sources: ["option", "env"]` makes user
67
67
 
68
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
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.
70
+ - `options.sources` is not mutated. `"default"` is appended to a copy.
71
+ - A source name other than `"env"`, `"option"` or `"default"` throws before any key is resolved: `xenv-config: unknown source "<name>"; valid sources are env, option, default`.
72
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
73
 
74
74
  ### Env lookup
@@ -117,7 +117,7 @@ All fields are optional. A spec option with none of them resolves only from `use
117
117
  The type for a key is decided by `type` first, then inference:
118
118
 
119
119
  1. `opt.type` if truthy.
120
- 2. Else `typeof opt.default` when `default` is an own property and its `typeof` is not `"function"`.
120
+ 2. Else, when `default` is an own property, inferred from it (see the table below).
121
121
  3. Else `"string"`.
122
122
 
123
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.
@@ -130,17 +130,20 @@ Then `"object"` is rewritten to `"json"`. A type name not listed below falls bac
130
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
131
  | `truthy` | `!!x` | An empty string gives `false`. Any non-empty string, including `"false"` and `"0"`, gives `true`. |
132
132
  | `json` | `JSON.parse(x)` | Needs strict JSON. `{b:90}` is invalid. See json merging. |
133
+ | `array` | `JSON.parse(x)` | Same parse as `json`, but first-wins like other types. Nothing is merged. |
133
134
 
134
135
  Inference from `default`:
135
136
 
136
- | `typeof default` | Inferred type |
137
+ | `default` | Inferred type |
137
138
  | --- | --- |
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` |
139
+ | a string | `string` |
140
+ | a number | `number` (integer, `parseInt`) |
141
+ | a boolean | `boolean` |
142
+ | `null` | `string`. With no other source the value stays `null`. |
143
+ | an array | `array`. The first source found wins and its value is returned as is. |
144
+ | any other object | `json` |
145
+ | `undefined` (own property set to `undefined`) | no converter of that name, so `string` conversion |
146
+ | a function, or no `default` | `string` |
144
147
 
145
148
  An explicit `type` always overrides inference. `type: "object"` is accepted as an alias of `json`.
146
149
 
@@ -154,7 +157,7 @@ When the resolved type is `json`, sources are not first-wins. Every source in `s
154
157
  - The result is always a new object, so defaults and user config are not mutated by the merge.
155
158
  - If no source finds a value, the key is skipped.
156
159
  - 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.
160
+ - 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"`. When env contributed, the trace also has `name`, the env var name used.
158
161
 
159
162
  ## `XenvConfigOptions`
160
163
 
@@ -170,7 +173,7 @@ interface XenvConfigOptions {
170
173
  | --- | --- | --- | --- |
171
174
  | `_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
175
  | `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. |
176
+ | `sources` | `string[]` | `["env", "option"]` | Ordered source names to consult. `"default"` is always appended, after your list. The array you pass is not changed. An unknown name throws. |
174
177
 
175
178
  ## Result
176
179
 
@@ -190,7 +193,7 @@ The result is a plain `{}` with one property per resolved spec key, in spec key
190
193
  - `__$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
194
  - `__$trace__[key]` exists exactly when `key` is in the result.
192
195
  - `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.
196
+ - `trace.name` is set when env provided the value, or contributed to a json value. It holds the env var name actually used, which is useful with an `env` array.
194
197
  - Values from `userConfig` and `default` are stored by reference, not cloned (non-json keys).
195
198
 
196
199
  ```js
@@ -206,7 +209,7 @@ const config = xenvConfig(
206
209
  // config: { port: 9000, opts: { a: 1, b: 2, c: 3 } } (debug is absent)
207
210
  // config.__$trace__:
208
211
  // port: { src: "env", name: "PORT" }
209
- // opts: { src: "env,option,default" }
212
+ // opts: { src: "env,option,default", name: "OPTS" }
210
213
  ```
211
214
 
212
215
  ## Errors
@@ -217,7 +220,7 @@ Nothing is caught or wrapped. All errors are thrown synchronously from `xenvConf
217
220
  | --- | --- |
218
221
  | `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
222
  | `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`) |
223
+ | `sources` contains an unknown name | `Error`, message `xenv-config: unknown source "<name>"; valid sources are env, option, default`. Thrown before any key is resolved. |
221
224
  | `spec` is `undefined` or `null` | `TypeError` from `Object.keys` |
222
225
  | A function `default`, `post`, or `merge` throws | the error propagates unchanged |
223
226
  | `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` |
@@ -226,9 +229,7 @@ Absence of a value is not an error. Missing keys are left out of the result.
226
229
 
227
230
  ## Quirks
228
231
 
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
232
  - `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
233
  - `number` env values that are not integers give `NaN` rather than an error. Empty env strings give `NaN`.
233
234
  - `envMap` hits are returned as is, with no coercion by `type`.
234
235
  - `userConfig` values and defaults are never coerced, only env values are.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xenv-config",
3
- "version": "2.1.8",
3
+ "version": "3.0.0",
4
4
  "description": "Load config from env, user config, or default spec",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",