@o3co/ts.hocon 1.10.0 → 1.12.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.ja.md +129 -1
- package/README.md +103 -10
- package/dist/adapters/env.cjs +150 -31
- package/dist/adapters/env.d.cts +16 -1
- package/dist/adapters/env.d.ts +16 -1
- package/dist/adapters/env.js +150 -30
- package/dist/adapters/jsonc.cjs +148 -18
- package/dist/adapters/jsonc.d.cts +36 -5
- package/dist/adapters/jsonc.d.ts +36 -5
- package/dist/adapters/jsonc.js +148 -17
- package/dist/adapters/properties.cjs +124 -25
- package/dist/adapters/properties.d.cts +6 -1
- package/dist/adapters/properties.d.ts +6 -1
- package/dist/adapters/properties.js +124 -24
- package/dist/adapters/toml.cjs +111 -22
- package/dist/adapters/toml.d.cts +3 -2
- package/dist/adapters/toml.d.ts +3 -2
- package/dist/adapters/toml.js +111 -21
- package/dist/adapters/yaml.cjs +142 -27
- package/dist/adapters/yaml.d.cts +12 -3
- package/dist/adapters/yaml.d.ts +12 -3
- package/dist/adapters/yaml.js +142 -26
- package/dist/{config-PdhVgeLS.d.cts → config-Dlq6rF3w.d.cts} +47 -1
- package/dist/{config-PdhVgeLS.d.ts → config-Dlq6rF3w.d.ts} +47 -1
- package/dist/index.cjs +162 -31
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +169 -31
- package/dist/zod.cjs +153 -23
- package/dist/zod.d.cts +1 -1
- package/dist/zod.d.ts +1 -1
- package/dist/zod.js +160 -23
- package/package.json +58 -22
package/README.ja.md
CHANGED
|
@@ -24,6 +24,20 @@ npm install @o3co/ts.hocon
|
|
|
24
24
|
|
|
25
25
|
Node.js 22 以上が必要です。
|
|
26
26
|
|
|
27
|
+
ESM / CJS の両方に対応しています。`import` でも `require` でも同じエントリポイント
|
|
28
|
+
(`adapters/*` のサブパスを含む)に到達します:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { parse } from '@o3co/ts.hocon' // ESM
|
|
32
|
+
const { parse } = require('@o3co/ts.hocon') // CJS
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
CI では PR ごと・リリースごとに、ビルド成果物に対して 7 つの export すべてを両形式で
|
|
36
|
+
ロードし、実際に呼び出し(バンドル形式で挙動が変わる package include を含む)、各条件が
|
|
37
|
+
自前の型定義を宣言しているかまで検証します。1.10.0 で CJS の 7 エントリポイントすべてが
|
|
38
|
+
ロード時に例外を投げた実績があるためのゲートであり、この種の欠陥を検出するものです
|
|
39
|
+
(あらゆる欠陥が起こり得ないという保証ではありません)。
|
|
40
|
+
|
|
27
41
|
### 2. 使い方
|
|
28
42
|
|
|
29
43
|
```ts
|
|
@@ -67,7 +81,7 @@ HOCON は単なるシリアライズ形式ではなく、**プログラムに注
|
|
|
67
81
|
- `include "file.conf"` および `include file("file.conf")` ディレクティブ
|
|
68
82
|
- トリプルクォート文字列(`"""..."""`)
|
|
69
83
|
- 同期・非同期 API(`parse` / `parseAsync` / `parseFile` / `parseFileAsync`)
|
|
70
|
-
- ESM + CJS
|
|
84
|
+
- ESM + CJS デュアルパッケージ(全エントリポイントを CI で両形式ロード検証)
|
|
71
85
|
- [Zod](https://zod.dev/) スキーマバリデーション統合(オプション)
|
|
72
86
|
- ブラウザ対応(`parse`/`parseAsync` — Node.js 不要)
|
|
73
87
|
|
|
@@ -268,6 +282,16 @@ const cfg = await parseAsync(hoconString, {
|
|
|
268
282
|
- **`${ENV}` の使用を最小限に**: 設定ファイル自体にデフォルト値を定義し、`${?ENV}`(オプショナル)を使いましょう
|
|
269
283
|
- **ローカル開発で環境変数を必須にしない**: デフォルトだけで動くようにしましょう
|
|
270
284
|
- **必須の環境変数を文書化**: プロジェクトの README や `.env.example` にリストしましょう
|
|
285
|
+
- **変数名の階層は `.` ではなく `__` で**: `loadEnv`(および `parseDotEnv`)で一括マウント
|
|
286
|
+
するとき、階層を作るのは `__` **だけ** です。`.` は通常のキー文字なので、次の 2 つは
|
|
287
|
+
別々のキーであり、どちらも失われません:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
APP_FOO__BAR=nested # -> foo.bar : cfg.getString('foo.bar')
|
|
291
|
+
APP_FOO.BAR=dotted # -> "foo.bar" : cfg.getString('"foo.bar"')
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
単独の `_` もセグメントの一部のままです(`APP_DB__MAX_CONN` → `db.max_conn`)。
|
|
271
295
|
|
|
272
296
|
### 開発 / 本番の分離
|
|
273
297
|
|
|
@@ -304,6 +328,110 @@ const config = parseWithSchema(hoconInput, schema) // 起動時に即座に失
|
|
|
304
328
|
|
|
305
329
|
3 つのパーサー実装([ts.hocon](https://github.com/o3co/ts.hocon)、[rs.hocon](https://github.com/o3co/rs.hocon)、[go.hocon](https://github.com/o3co/go.hocon))はすべて同じ Lightbend HOCON 仕様で追跡されています — 実装ごとの準拠率は [横断ロールアップ](https://github.com/o3co/xx.hocon/blob/main/docs/compliance-matrix.md) を参照してください。
|
|
306
330
|
|
|
331
|
+
## フォーマットアダプタ
|
|
332
|
+
|
|
333
|
+
*他のプログラム* が所有する設定ファイルを HOCON としてマウントできます。自分の
|
|
334
|
+
ドキュメント内の `${...}` からその内容を参照できるようになります:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { parseStringWithOptions } from '@o3co/ts.hocon'
|
|
338
|
+
import { loadEnv } from '@o3co/ts.hocon/adapters/env'
|
|
339
|
+
|
|
340
|
+
const base = loadEnv({ prefix: 'APP_' }) // APP_DB__HOST -> db.host
|
|
341
|
+
const cfg = parseStringWithOptions(src, { resolveSubstitutions: false })
|
|
342
|
+
const merged = cfg.withFallback(base).resolve()
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
解決を遅延させることが重要です。素の `parse` はパースしながら解決するため、
|
|
346
|
+
フォールバックを付ける前に `${...}` の解決が失敗してしまいます。
|
|
347
|
+
|
|
348
|
+
| サブパス | 必要な依存 | 備考 |
|
|
349
|
+
| --- | --- | --- |
|
|
350
|
+
| `@o3co/ts.hocon/adapters/properties` | — | `java.util.Properties`。`include` と構文レイヤーを共有 |
|
|
351
|
+
| `@o3co/ts.hocon/adapters/env` | — | プレフィックス付き名前空間の一括マウント。`.env` の読み込みも可能 |
|
|
352
|
+
| `@o3co/ts.hocon/adapters/jsonc` | — | コメントと末尾カンマを許す JSON |
|
|
353
|
+
| `@o3co/ts.hocon/adapters/toml` | `smol-toml` | オプショナルなピア依存 |
|
|
354
|
+
| `@o3co/ts.hocon/adapters/yaml` | `yaml` 2.9.x | オプショナルなピア依存。スカラー解決はそのライブラリの答えで、`version: '1.2'` を明示してドリフトを防止 |
|
|
355
|
+
|
|
356
|
+
TOML と YAML のライブラリは **オプショナルなピア依存** なので、本パッケージを入れても
|
|
357
|
+
何も追加でインストールされません — 実際に使うものだけを足してください。プレーンな JSON に
|
|
358
|
+
アダプタは不要です(HOCON は JSON のスーパーセットです)。
|
|
359
|
+
|
|
360
|
+
外部のデータはあくまでデータです。マウントされた値の中の `${a.b}` は参照ではなく
|
|
361
|
+
リテラルなテキストとして扱われます。そのファイルは HOCON の構文に同意していない
|
|
362
|
+
プログラムのものだからです。
|
|
363
|
+
|
|
364
|
+
### 環境変数名の付け方
|
|
365
|
+
|
|
366
|
+
パス区切りは `__` だけです。変数名の中の `.` は境界ではなくキー文字なので、
|
|
367
|
+
`APP_FOO.BAR` と `APP_FOO__BAR` は別のキーであり、同時に設定できます:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
const cfg = loadEnv({ prefix: 'APP_', env: { 'APP_FOO.BAR': 'dotted', APP_FOO__BAR: 'nested' } })
|
|
371
|
+
cfg.getString('"foo.bar"') // "dotted" — 名前にドットを含む 1 つのキー
|
|
372
|
+
cfg.getString('foo.bar') // "nested" — foo -> bar
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### JSONC / YAML の数値
|
|
376
|
+
|
|
377
|
+
整数は精度を落とさずに取り込まれます。JS の `number` で表せない幅の整数リテラルも
|
|
378
|
+
桁がそのまま保持され、int64 の範囲を超える値は黙って壊れる代わりにエラーになります。
|
|
379
|
+
取得結果はゲッターによって変わります:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const cfg = parseJsonc('{"id": 9007199254740993}')
|
|
383
|
+
cfg.getString('id') // "9007199254740993" — ソースそのままの正確な値
|
|
384
|
+
cfg.getNumber('id') // 9007199254740992 — JS の数値モデルで丸められる
|
|
385
|
+
cfg.toObject() // { id: 9007199254740992 }
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
したがって大きな識別子(snowflake ID、台帳の連番など)は `getString` で読んでください。
|
|
389
|
+
`getNumber` と `toObject` は JavaScript の数値セマンティクスをそのまま適用します
|
|
390
|
+
(HOCON テキストに同じリテラルを書いた場合と同じ挙動です)。保証されるのは
|
|
391
|
+
**取り込み時に情報が失われない** ことです。
|
|
392
|
+
|
|
393
|
+
YAML のスカラー解決はそれ以外の点では `yaml` ライブラリのものです。アダプタは
|
|
394
|
+
デフォルト任せにせず `version: '1.2'` を明示し、`fromYamlValue` はデコード済みの
|
|
395
|
+
ツリーを受け取るため、別のライブラリやスキーマを使うこともできます。
|
|
396
|
+
|
|
397
|
+
## 既知の制限
|
|
398
|
+
|
|
399
|
+
- **`include url(...)`** は未対応です。リモート設定の取得はこのパーサーの範囲外です。アプリの HTTP クライアントで取得してから `parse()` に渡してください。
|
|
400
|
+
- **`include classpath(...)`** は未対応です。JVM 固有の include 形式で、Java ランタイム以外に対応物がありません。
|
|
401
|
+
- **watch / reload なし** — ライブラリはロード時にパースするだけです。ライブリロードが必要なら変更時に `parse()` / `parseFile()` を呼び直してください。
|
|
402
|
+
- **ストリーミングパーサーなし** — 入力全体をメモリに載せます。非常に大きな設定はパース前にサイズを検証してください(セキュリティ上の考慮事項を参照)。
|
|
403
|
+
|
|
404
|
+
## セキュリティ上の考慮事項
|
|
405
|
+
|
|
406
|
+
信頼できない HOCON 入力をパースする際は次に注意してください:
|
|
407
|
+
|
|
408
|
+
- **include のパストラバーサル:** 相対 `include` パスは `baseDir` から解決され、`..` セグメントで外に出て `/etc/passwd` のような機密ファイルに到達し得ます。信頼できない入力をパースするなら、パスを検証するカスタム `readFileSync`/`readFile` を渡してください。
|
|
409
|
+
- **入力サイズ:** パーサーに入力サイズ上限はありません。信頼できない入力は `parse()` の前にサイズを検証してください。
|
|
410
|
+
- **include の深さ:** 深い include 連鎖によるスタックオーバーフローを防ぐため 50 段に制限されています。
|
|
411
|
+
- **プロトタイプ汚染:** `__proto__` / `constructor` / `prototype` という名前のキーは
|
|
412
|
+
**通常のキーとして保持されます**。`.properties` ファイルや環境変数がそれらを正当に
|
|
413
|
+
使っていることがあり、落とせば黙ったデータ欠落になるからです。ライブラリ内部の安全性は
|
|
414
|
+
拒否リストではなく構造で担保しています。ネスト構築には `__proto__` セッターを継承しない
|
|
415
|
+
null プロトタイプのオブジェクトを使い、各アダプタと `toObject()` はキーを own データ
|
|
416
|
+
プロパティとして定義します。パース自体は何も汚染せず、返るオブジェクトのプロトタイプは
|
|
417
|
+
常に `Object.prototype` です。
|
|
418
|
+
|
|
419
|
+
**重要なのは結果をどう扱うか** です。設定データが own な `__proto__` キーを持ち得ます:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
const data = cfg.toObject()
|
|
423
|
+
{ ...data } // 安全
|
|
424
|
+
structuredClone(data) // 安全
|
|
425
|
+
Object.assign({}, data) // 危険 — キーが消え、設定データがコピー先のプロトタイプになる
|
|
426
|
+
deepMerge({}, data) // 危険 — 素朴な再帰マージはグローバルな Object.prototype に書き込み得る
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
いずれも **コピー先** オブジェクトのプロトタイプに起因するため、避けるのは利用側の責務です。
|
|
430
|
+
コピーはスプレッドか `structuredClone`、走査は `Object.entries()` / `Object.keys()` を使い、
|
|
431
|
+
`__proto__` を通常のキー名として扱ってください。制御下にない場所へ渡す場合は
|
|
432
|
+
`cfg.toObject({ nullPrototype: true })` が何も継承しないツリーを返します(ただし利用側が
|
|
433
|
+
素の `{}` にコピーする場合の問題までは解消できません)。
|
|
434
|
+
|
|
307
435
|
## ライセンス
|
|
308
436
|
|
|
309
437
|
Apache License 2.0 — [LICENSE](LICENSE) を参照。
|
package/README.md
CHANGED
|
@@ -28,6 +28,21 @@ npm install @o3co/ts.hocon
|
|
|
28
28
|
|
|
29
29
|
Requires Node.js 22+.
|
|
30
30
|
|
|
31
|
+
Both module systems are supported — `import` and `require` reach the same
|
|
32
|
+
entrypoints, including every `adapters/*` subpath:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { parse } from '@o3co/ts.hocon' // ESM
|
|
36
|
+
const { parse } = require('@o3co/ts.hocon') // CJS
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
On every PR and every release, CI loads all seven exports both ways against the
|
|
40
|
+
built artifact, calls each one (including a package-scoped `include`, which
|
|
41
|
+
behaves differently per bundle format), and checks that each condition declares
|
|
42
|
+
its own type declarations. That gate exists because 1.10.0 shipped with all
|
|
43
|
+
seven CJS entrypoints throwing at load; it catches that class of defect rather
|
|
44
|
+
than promising none can exist.
|
|
45
|
+
|
|
31
46
|
### 2. Use
|
|
32
47
|
|
|
33
48
|
```ts
|
|
@@ -72,7 +87,7 @@ On top of that, HOCON combines the readability of YAML with the structure of JSO
|
|
|
72
87
|
- Triple-quoted strings (`"""..."""`)
|
|
73
88
|
- Duration and byte size parsing (`getDuration()`, `getBytes()`)
|
|
74
89
|
- Sync and async API (`parse` / `parseAsync` / `parseFile` / `parseFileAsync`)
|
|
75
|
-
- ESM + CJS dual package
|
|
90
|
+
- ESM + CJS dual package (every entrypoint smoke-tested in both formats on each CI run)
|
|
76
91
|
- Optional [Zod](https://zod.dev/) integration for schema validation
|
|
77
92
|
- Browser compatible (`parse`/`parseAsync` — no Node.js required)
|
|
78
93
|
|
|
@@ -250,12 +265,12 @@ Supported byte units: `B`, `KB`/`KiB`, `MB`/`MiB`, `GB`/`GiB`, `TB`/`TiB` (and l
|
|
|
250
265
|
|
|
251
266
|
## Spec Compliance
|
|
252
267
|
|
|
253
|
-
Conformance against the [Lightbend HOCON specification](https://github.com/lightbend/config/blob/main/HOCON.md) is tracked at item granularity in [`docs/spec-compliance.md`](docs/spec-compliance.md).
|
|
268
|
+
Conformance against the [Lightbend HOCON specification](https://github.com/lightbend/config/blob/main/HOCON.md) is tracked at item granularity in [`docs/spec-compliance.md`](docs/spec-compliance.md), which is the source these rates are computed from — `tests/docs.test.ts` recomputes them and fails the build if this table drifts. See [`xx.hocon/docs/compliance-matrix.md`](https://github.com/o3co/xx.hocon/blob/main/docs/compliance-matrix.md) for the cross-implementation roll-up.
|
|
254
269
|
|
|
255
270
|
| Metric | Status |
|
|
256
271
|
| ------------------------------------- | ------------- |
|
|
257
|
-
| Spec total (incl. out-of-scope) | **
|
|
258
|
-
| In-scope only | **
|
|
272
|
+
| Spec total (incl. out-of-scope) | **89.3%** |
|
|
273
|
+
| In-scope only | **99.2%** |
|
|
259
274
|
| Lightbend `test01`–`test13` suite | 13/13 passing |
|
|
260
275
|
|
|
261
276
|
**Extra-spec conventions (E-series) — implementation status:**
|
|
@@ -274,10 +289,6 @@ Supported since v0.2.0 (P1):
|
|
|
274
289
|
|
|
275
290
|
- `.properties` file parsing
|
|
276
291
|
|
|
277
|
-
### Stricter than Lightbend
|
|
278
|
-
|
|
279
|
-
- **S8.6 leading-hyphen rejection** (Unreleased): `a = -foo`, `a = -bar`, `a = -` etc. now raise a lex error per HOCON.md L270–276, where Lightbend silently falls back to unquoted strings. Mitigation: quote the value (`a = "-foo"`). See [CHANGELOG](CHANGELOG.md#unreleased) and [`docs/spec-compliance.md`](docs/spec-compliance.md) §S8.6.
|
|
280
|
-
|
|
281
292
|
## Performance
|
|
282
293
|
|
|
283
294
|
### ts.hocon Parsing Cost
|
|
@@ -310,7 +321,7 @@ For typical application configs (loaded once at startup), the parsing cost is ne
|
|
|
310
321
|
|
|
311
322
|
### Feature Comparison with node-config
|
|
312
323
|
|
|
313
|
-
|
|
324
|
+
Feature-level comparison with [node-config](https://github.com/node-config/node-config) **v4.4.2** reading JSON config files, as of 2026-07-26:
|
|
314
325
|
|
|
315
326
|
| Feature | ts.hocon | node-config (JSON) |
|
|
316
327
|
|---|---|---|
|
|
@@ -354,6 +365,17 @@ const cfg = await parseAsync(hoconString, {
|
|
|
354
365
|
- **Minimize `${ENV}` usage**: Prefer `${?ENV}` (optional) with sensible defaults defined in the config itself
|
|
355
366
|
- **Never require env vars for local development**: Defaults should work out of the box
|
|
356
367
|
- **Document required env vars**: List them in your project's README or a `.env.example`
|
|
368
|
+
- **Name variables with `__`, never `.`**: when bulk-mounting with `loadEnv` (or
|
|
369
|
+
`parseDotEnv`), `__` is the *only* thing that creates hierarchy. A dot is
|
|
370
|
+
ordinary key text, so the two spellings below are different keys and both
|
|
371
|
+
survive:
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
APP_FOO__BAR=nested # -> foo.bar : cfg.getString('foo.bar')
|
|
375
|
+
APP_FOO.BAR=dotted # -> "foo.bar" : cfg.getString('"foo.bar"')
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
A single `_` also stays part of its segment (`APP_DB__MAX_CONN` → `db.max_conn`).
|
|
357
379
|
|
|
358
380
|
### Dev / Prod Separation
|
|
359
381
|
|
|
@@ -414,7 +436,7 @@ Deferring resolution matters: the plain `parse` resolves as it goes, so a
|
|
|
414
436
|
| `@o3co/ts.hocon/adapters/env` | — | Bulk-mounts a prefixed namespace; also reads `.env` |
|
|
415
437
|
| `@o3co/ts.hocon/adapters/jsonc` | — | JSON with comments and trailing commas |
|
|
416
438
|
| `@o3co/ts.hocon/adapters/toml` | `smol-toml` | Optional peer dependency |
|
|
417
|
-
| `@o3co/ts.hocon/adapters/yaml` | `yaml` | Optional peer dependency;
|
|
439
|
+
| `@o3co/ts.hocon/adapters/yaml` | `yaml` 2.9.x | Optional peer dependency; scalar resolution is that library's answer, with `version: '1.2'` declared so it cannot drift |
|
|
418
440
|
|
|
419
441
|
The TOML and YAML libraries are **optional peer dependencies**, so installing
|
|
420
442
|
this package still pulls in nothing — you add the one you actually use. Plain
|
|
@@ -424,6 +446,40 @@ Foreign data stays data: a `${a.b}` in a mounted value is literal text, never a
|
|
|
424
446
|
reference, because the file belongs to a program that never agreed to HOCON's
|
|
425
447
|
syntax.
|
|
426
448
|
|
|
449
|
+
### Environment variable names
|
|
450
|
+
|
|
451
|
+
`__` is the only path separator, so a literal `.` in a variable name is key
|
|
452
|
+
text rather than a boundary — `APP_FOO.BAR` and `APP_FOO__BAR` are two
|
|
453
|
+
different keys and can be set at the same time:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
const cfg = loadEnv({ prefix: 'APP_', env: { 'APP_FOO.BAR': 'dotted', APP_FOO__BAR: 'nested' } })
|
|
457
|
+
cfg.getString('"foo.bar"') // "dotted" — one key whose name contains a dot
|
|
458
|
+
cfg.getString('foo.bar') // "nested" — foo -> bar
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Numbers from JSONC and YAML
|
|
462
|
+
|
|
463
|
+
Integers are ingested losslessly: an integer literal too wide for a JS `number`
|
|
464
|
+
keeps its digits instead of being rounded, and one outside the int64 range is
|
|
465
|
+
refused rather than silently mangled. What you get back depends on the getter:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
const cfg = parseJsonc('{"id": 9007199254740993}')
|
|
469
|
+
cfg.getString('id') // "9007199254740993" — exact, the source's own text
|
|
470
|
+
cfg.getNumber('id') // 9007199254740992 — the JS number model rounds
|
|
471
|
+
cfg.toObject() // { id: 9007199254740992 }
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
So read large identifiers (snowflake IDs, ledger sequence numbers) with
|
|
475
|
+
`getString`. `getNumber` and `toObject` apply JavaScript's own number
|
|
476
|
+
semantics, exactly as they do for the same literal written in HOCON text — the
|
|
477
|
+
guarantee is that nothing is lost *on the way in*.
|
|
478
|
+
|
|
479
|
+
YAML scalar resolution otherwise belongs to the `yaml` library; the adapter
|
|
480
|
+
declares `version: '1.2'` rather than trusting a default, and `fromYamlValue`
|
|
481
|
+
takes an already-decoded tree so you can use a different library or schema.
|
|
482
|
+
|
|
427
483
|
## Known Limitations
|
|
428
484
|
|
|
429
485
|
- **`include url(...)`** is not supported. Fetching remote configuration is outside the scope of this parser. Use your application's HTTP client to fetch the content, then pass it to `parse()`.
|
|
@@ -438,6 +494,43 @@ When parsing untrusted HOCON input, be aware of:
|
|
|
438
494
|
- **Path traversal in includes:** a relative `include` path resolves against `baseDir` and can climb out of it with `..` segments to reach sensitive files such as `/etc/passwd`. Use a custom `readFileSync`/`readFile` that validates paths if parsing untrusted input.
|
|
439
495
|
- **Input size:** The parser has no built-in input size limit. For untrusted input, validate size before calling `parse()`.
|
|
440
496
|
- **Include depth:** Limited to 50 levels to prevent stack overflow from deep include chains.
|
|
497
|
+
- **Mapped path depth:** an environment variable's `__` segments and a
|
|
498
|
+
`.properties` dotted key are limited to 64 segments. One name produces one
|
|
499
|
+
arbitrarily deep chain, so without the cap a single long variable name was
|
|
500
|
+
enough to exhaust the stack. rs.hocon and py.hocon cap the same mapping at 64.
|
|
501
|
+
- **Document nesting depth:** not capped. A document nested past what the
|
|
502
|
+
engine's stack holds fails as `ParseError` or `ConfigError` — never as a bare
|
|
503
|
+
`RangeError` — but the depth at which that happens depends on how deep the
|
|
504
|
+
calling code already is, so validate size before parsing untrusted input (see
|
|
505
|
+
above) rather than relying on a fixed level.
|
|
506
|
+
- **Prototype pollution:** keys named `__proto__`, `constructor` or `prototype`
|
|
507
|
+
are **kept as ordinary keys** — a `.properties` file or environment variable
|
|
508
|
+
may legitimately use them, and dropping them would be silent data loss.
|
|
509
|
+
Safety inside this library is structural rather than a denylist: the nesting
|
|
510
|
+
step builds null-prototype objects, which inherit no `__proto__` setter, every
|
|
511
|
+
adapter materializes objects by defining own data properties, and `toObject()`
|
|
512
|
+
does the same. Parsing pollutes nothing, and the returned object's prototype is
|
|
513
|
+
always `Object.prototype`.
|
|
514
|
+
|
|
515
|
+
**What you do with the result matters**, because config data can now hand you
|
|
516
|
+
an own `__proto__` key:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const data = cfg.toObject()
|
|
520
|
+
{ ...data } // safe
|
|
521
|
+
structuredClone(data) // safe
|
|
522
|
+
Object.assign({}, data) // NOT safe — the key vanishes and config data becomes
|
|
523
|
+
// the copy's prototype
|
|
524
|
+
deepMerge({}, data) // NOT safe — a naive recursive merge can write onto
|
|
525
|
+
// the global Object.prototype
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Both hazards come from the *destination* object's prototype, so they are the
|
|
529
|
+
consumer's to avoid: copy with spread or `structuredClone`, iterate with
|
|
530
|
+
`Object.entries()` / `Object.keys()`, and treat `__proto__` as an ordinary
|
|
531
|
+
key name. `cfg.toObject({ nullPrototype: true })` returns a tree that inherits
|
|
532
|
+
nothing — useful when the data is going somewhere you do not control, though it
|
|
533
|
+
cannot fix a consumer that copies into a plain `{}`.
|
|
441
534
|
|
|
442
535
|
## License
|
|
443
536
|
|
package/dist/adapters/env.cjs
CHANGED
|
@@ -550,37 +550,73 @@ function deepMergeHoconValues(base, overlay) {
|
|
|
550
550
|
// src/internal/resolver/include-loader.ts
|
|
551
551
|
var import_node_module = require("module");
|
|
552
552
|
var nodePath = __toESM(require("path"), 1);
|
|
553
|
+
var import_node_url = require("url");
|
|
554
|
+
|
|
555
|
+
// src/internal/strip-bom.ts
|
|
556
|
+
function stripBom(input) {
|
|
557
|
+
return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/internal/depth.ts
|
|
561
|
+
var MAX_PATH_SEGMENTS = 64;
|
|
562
|
+
function tooDeep(segments) {
|
|
563
|
+
return segments > MAX_PATH_SEGMENTS;
|
|
564
|
+
}
|
|
565
|
+
function guardStackDepth(fn, wrap) {
|
|
566
|
+
try {
|
|
567
|
+
return fn();
|
|
568
|
+
} catch (e) {
|
|
569
|
+
if (!isStackOverflow(e)) throw e;
|
|
570
|
+
throw wrap(STACK_MESSAGE);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
|
|
574
|
+
function isStackOverflow(e) {
|
|
575
|
+
if (!(e instanceof RangeError)) return false;
|
|
576
|
+
const m = e.message.toLowerCase();
|
|
577
|
+
return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
|
|
578
|
+
m.includes("recursion") || // SpiderMonkey: "too much recursion"
|
|
579
|
+
m.includes("stack overflow");
|
|
580
|
+
}
|
|
581
|
+
function depthError(message) {
|
|
582
|
+
return new ConfigError(message, "");
|
|
583
|
+
}
|
|
553
584
|
|
|
554
585
|
// src/internal/properties/properties.ts
|
|
555
586
|
function nestPairs(pairs) {
|
|
556
587
|
const root = /* @__PURE__ */ Object.create(null);
|
|
557
|
-
const sorted = [...pairs].sort(([a], [b]) => a
|
|
558
|
-
for (const [
|
|
559
|
-
setNested(root,
|
|
588
|
+
const sorted = [...pairs].sort(([a], [b]) => compareSegments(a, b));
|
|
589
|
+
for (const [segments, value] of sorted) {
|
|
590
|
+
setNested(root, segments, value);
|
|
560
591
|
}
|
|
561
592
|
return root;
|
|
562
593
|
}
|
|
563
|
-
|
|
594
|
+
function compareSegments(a, b) {
|
|
595
|
+
const n = Math.min(a.length, b.length);
|
|
596
|
+
for (let i = 0; i < n; i++) {
|
|
597
|
+
const x = a[i];
|
|
598
|
+
const y = b[i];
|
|
599
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
600
|
+
}
|
|
601
|
+
return a.length - b.length;
|
|
602
|
+
}
|
|
603
|
+
function pathKey(segments) {
|
|
604
|
+
return JSON.stringify(segments);
|
|
605
|
+
}
|
|
564
606
|
function setNested(obj, segments, value) {
|
|
565
607
|
let current = obj;
|
|
566
608
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
567
609
|
const seg = segments[i];
|
|
568
|
-
if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
|
|
569
610
|
if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
|
|
570
611
|
current[seg] = /* @__PURE__ */ Object.create(null);
|
|
571
612
|
}
|
|
572
613
|
current = current[seg];
|
|
573
614
|
}
|
|
574
615
|
const last = segments[segments.length - 1];
|
|
575
|
-
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
576
616
|
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
577
617
|
current[last] = value;
|
|
578
618
|
}
|
|
579
619
|
|
|
580
|
-
// src/internal/resolver/include-loader.ts
|
|
581
|
-
var import_meta = {};
|
|
582
|
-
var _require = (0, import_node_module.createRequire)(import_meta.url);
|
|
583
|
-
|
|
584
620
|
// src/value/numeric-array.ts
|
|
585
621
|
function numericObjectToArray(value) {
|
|
586
622
|
if (value.kind !== "object") return null;
|
|
@@ -1168,10 +1204,10 @@ var Config = class _Config {
|
|
|
1168
1204
|
}
|
|
1169
1205
|
if (v.kind === "object") {
|
|
1170
1206
|
const converted = numericObjectToArray(v);
|
|
1171
|
-
if (converted !== null) return converted.map(hoconToJs);
|
|
1207
|
+
if (converted !== null) return converted.map((item) => hoconToJs(item));
|
|
1172
1208
|
}
|
|
1173
1209
|
if (v.kind !== "array") throw new ConfigError(`expected array at ${path}`, path);
|
|
1174
|
-
return v.items.map(hoconToJs);
|
|
1210
|
+
return v.items.map((item) => hoconToJs(item));
|
|
1175
1211
|
}
|
|
1176
1212
|
has(path) {
|
|
1177
1213
|
return this.lookupNode(path) !== void 0;
|
|
@@ -1299,8 +1335,52 @@ var Config = class _Config {
|
|
|
1299
1335
|
originDescription: this._originDescription
|
|
1300
1336
|
});
|
|
1301
1337
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1338
|
+
/**
|
|
1339
|
+
* The whole config as plain JS data: objects, arrays, strings, numbers,
|
|
1340
|
+
* booleans and `null`.
|
|
1341
|
+
*
|
|
1342
|
+
* Keys are reproduced exactly as the config carries them, `__proto__`,
|
|
1343
|
+
* `constructor` and `prototype` included: each is defined as an **own data
|
|
1344
|
+
* property**, so the returned object's prototype is always
|
|
1345
|
+
* `Object.prototype`, and this call writes nothing to the global one.
|
|
1346
|
+
*
|
|
1347
|
+
* ### Passing the result to other code
|
|
1348
|
+
*
|
|
1349
|
+
* A config file — especially a `.properties` file or an env namespace you
|
|
1350
|
+
* merged in — can therefore hand you an own `__proto__` key, and what happens
|
|
1351
|
+
* next depends entirely on how the consumer copies it:
|
|
1352
|
+
*
|
|
1353
|
+
* ```ts
|
|
1354
|
+
* const data = cfg.toObject()
|
|
1355
|
+
* { ...data } // safe — own properties are copied as data
|
|
1356
|
+
* structuredClone(data) // safe
|
|
1357
|
+
* Object.assign({}, data) // NOT safe — [[Set]] on the {} target hits
|
|
1358
|
+
* // Object.prototype's __proto__ setter: the key
|
|
1359
|
+
* // vanishes and config data becomes the result's
|
|
1360
|
+
* // prototype
|
|
1361
|
+
* deepMerge({}, data) // NOT safe — a naive recursive merge reads
|
|
1362
|
+
* // target['__proto__'], gets Object.prototype,
|
|
1363
|
+
* // and writes to it: global pollution
|
|
1364
|
+
* for (const k in data) … // prefer Object.entries / Object.keys
|
|
1365
|
+
* ```
|
|
1366
|
+
*
|
|
1367
|
+
* Those two hazards come from the **destination** object's prototype, so no
|
|
1368
|
+
* option here can disarm them — copy with spread or `structuredClone` instead.
|
|
1369
|
+
*
|
|
1370
|
+
* What `{ nullPrototype: true }` does give you is a result that inherits
|
|
1371
|
+
* nothing: every object comes back with a `null` prototype, so reading
|
|
1372
|
+
* `x.__proto__`, `x.constructor` or `x.toString` yields config data or
|
|
1373
|
+
* `undefined` rather than something from `Object.prototype`, and code that
|
|
1374
|
+
* merges *into* this object cannot climb out of it. Keys are all still
|
|
1375
|
+
* present. The trade-off is the usual one for prototype-less objects: not
|
|
1376
|
+
* `instanceof Object`, and no `hasOwnProperty` of their own.
|
|
1377
|
+
*
|
|
1378
|
+
* Numbers follow the JS number model, so an integer wider than 2^53 rounds
|
|
1379
|
+
* here even when the config holds it exactly — read those with
|
|
1380
|
+
* {@link Config.getString} instead.
|
|
1381
|
+
*/
|
|
1382
|
+
toObject(opts) {
|
|
1383
|
+
return hoconToJs(this.root, opts?.nullPrototype === true);
|
|
1304
1384
|
}
|
|
1305
1385
|
/**
|
|
1306
1386
|
* @internal Test-only: renders this resolved Config's value tree as canonical
|
|
@@ -1412,16 +1492,25 @@ function scalarToJs(raw, valueType) {
|
|
|
1412
1492
|
return raw;
|
|
1413
1493
|
}
|
|
1414
1494
|
}
|
|
1415
|
-
function hoconToJs(v) {
|
|
1495
|
+
function hoconToJs(v, nullPrototype = false) {
|
|
1416
1496
|
switch (v.kind) {
|
|
1417
1497
|
case "scalar":
|
|
1418
1498
|
return scalarToJs(v.raw, v.valueType);
|
|
1419
1499
|
case "array":
|
|
1420
|
-
return v.items.map(hoconToJs);
|
|
1500
|
+
return v.items.map((item) => hoconToJs(item, nullPrototype));
|
|
1421
1501
|
case "object": {
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1502
|
+
const entries = Array.from(v.fields, ([k, val]) => [k, hoconToJs(val, nullPrototype)]);
|
|
1503
|
+
if (nullPrototype) {
|
|
1504
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
1505
|
+
for (const [k, val] of entries) Object.defineProperty(obj, k, {
|
|
1506
|
+
value: val,
|
|
1507
|
+
writable: true,
|
|
1508
|
+
enumerable: true,
|
|
1509
|
+
configurable: true
|
|
1510
|
+
});
|
|
1511
|
+
return obj;
|
|
1512
|
+
}
|
|
1513
|
+
return Object.fromEntries(entries);
|
|
1425
1514
|
}
|
|
1426
1515
|
}
|
|
1427
1516
|
}
|
|
@@ -1506,10 +1595,15 @@ function normalizeLeadingZeroNumber(raw) {
|
|
|
1506
1595
|
|
|
1507
1596
|
// src/value-factory.ts
|
|
1508
1597
|
function fromMap(values, originDescription) {
|
|
1509
|
-
const fields =
|
|
1598
|
+
const fields = guardStackDepth(
|
|
1599
|
+
() => coerceObject(values, ""),
|
|
1600
|
+
(msg) => depthError(`fromMap: ${msg}`)
|
|
1601
|
+
);
|
|
1510
1602
|
const root = { kind: "object", fields };
|
|
1511
1603
|
return Config._fromResolvedValue(root, { originDescription });
|
|
1512
1604
|
}
|
|
1605
|
+
var INT64_MAX = 9223372036854775807n;
|
|
1606
|
+
var INT64_MIN = -9223372036854775808n;
|
|
1513
1607
|
function coerceObject(obj, atPath) {
|
|
1514
1608
|
const fields = /* @__PURE__ */ new Map();
|
|
1515
1609
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -1538,14 +1632,14 @@ function coerceValue(v, atPath) {
|
|
|
1538
1632
|
atPath
|
|
1539
1633
|
);
|
|
1540
1634
|
}
|
|
1541
|
-
return { kind: "scalar", raw: String(v), valueType: "number" };
|
|
1635
|
+
return { kind: "scalar", raw: Object.is(v, -0) ? "-0" : String(v), valueType: "number" };
|
|
1542
1636
|
}
|
|
1543
1637
|
case "boolean":
|
|
1544
1638
|
return { kind: "scalar", raw: v ? "true" : "false", valueType: "boolean" };
|
|
1545
1639
|
case "bigint": {
|
|
1546
|
-
if (v >
|
|
1640
|
+
if (v > INT64_MAX || v < INT64_MIN) {
|
|
1547
1641
|
throw new ConfigError(
|
|
1548
|
-
`fromMap:
|
|
1642
|
+
`fromMap: integer ${v} is out of int64 range (HOCON integers are int64; spec F0.5) at path "${atPath}"`,
|
|
1549
1643
|
atPath
|
|
1550
1644
|
);
|
|
1551
1645
|
}
|
|
@@ -1586,11 +1680,12 @@ function loadEnv(opts = {}) {
|
|
|
1586
1680
|
const value = source[name];
|
|
1587
1681
|
if (value === void 0) continue;
|
|
1588
1682
|
const path = toPath(name.slice(prefix.length));
|
|
1589
|
-
const
|
|
1683
|
+
const key = pathKey(path);
|
|
1684
|
+
const prev = seen.get(key);
|
|
1590
1685
|
if (prev !== void 0) {
|
|
1591
|
-
throw new ConfigError(`loadEnv: ${prev} and ${name} both map to "${path}"`, path);
|
|
1686
|
+
throw new ConfigError(`loadEnv: ${prev} and ${name} both map to "${path.join(".")}"`, path.join("."));
|
|
1592
1687
|
}
|
|
1593
|
-
seen.set(
|
|
1688
|
+
seen.set(key, name);
|
|
1594
1689
|
pairs.push([path, value]);
|
|
1595
1690
|
}
|
|
1596
1691
|
return fromMap(nestPairs(pairs), opts.originDescription ?? "environment variables");
|
|
@@ -1599,27 +1694,37 @@ function parseDotEnv(input, opts = {}) {
|
|
|
1599
1694
|
const origin = opts.originDescription ?? ".env";
|
|
1600
1695
|
const prefix = opts.prefix ?? "";
|
|
1601
1696
|
const pairs = [];
|
|
1602
|
-
const lines = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
1697
|
+
const lines = stripBom(input).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
1603
1698
|
for (let i = 0; i < lines.length; i++) {
|
|
1604
1699
|
const raw = (lines[i] ?? "").trim();
|
|
1605
1700
|
if (raw === "" || raw.startsWith("#")) continue;
|
|
1606
|
-
const line =
|
|
1701
|
+
const line = stripExport(raw);
|
|
1607
1702
|
const eq = line.indexOf("=");
|
|
1608
1703
|
if (eq === -1) throw new ConfigError(`${origin}:${i + 1}: expected NAME=value`, "");
|
|
1609
1704
|
const name = line.slice(0, eq).trim();
|
|
1610
1705
|
if (name === "") throw new ConfigError(`${origin}:${i + 1}: empty variable name`, "");
|
|
1611
|
-
const value = dotEnvValue(line.slice(eq + 1).replace(/^[ \t]+/, ""), origin, i + 1, name);
|
|
1612
1706
|
if (!name.startsWith(prefix)) continue;
|
|
1707
|
+
checkName(name, origin, i + 1);
|
|
1708
|
+
const value = dotEnvValue(line.slice(eq + 1).replace(/^[ \t]+/, ""), origin, i + 1, name);
|
|
1613
1709
|
pairs.push([toPath(name.slice(prefix.length)), value]);
|
|
1614
1710
|
}
|
|
1615
1711
|
return fromMap(nestPairs(pairs), origin);
|
|
1616
1712
|
}
|
|
1617
1713
|
function toPath(name) {
|
|
1618
|
-
const segs = name.split(SEPARATOR).map(
|
|
1714
|
+
const segs = name.split(SEPARATOR).map(asciiLower);
|
|
1619
1715
|
if (segs.some((s) => s === "")) {
|
|
1620
1716
|
throw new ConfigError(`env: "${name}" produces an empty path segment`, name);
|
|
1621
1717
|
}
|
|
1622
|
-
|
|
1718
|
+
if (tooDeep(segs.length)) {
|
|
1719
|
+
throw new ConfigError(
|
|
1720
|
+
`env: "${name}" maps to a path ${segs.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
|
|
1721
|
+
name
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return segs;
|
|
1725
|
+
}
|
|
1726
|
+
function asciiLower(s) {
|
|
1727
|
+
return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
|
|
1623
1728
|
}
|
|
1624
1729
|
function dotEnvValue(v, origin, line, name) {
|
|
1625
1730
|
const fail = (msg) => {
|
|
@@ -1661,6 +1766,20 @@ function dotEnvValue(v, origin, line, name) {
|
|
|
1661
1766
|
}
|
|
1662
1767
|
return trimmed;
|
|
1663
1768
|
}
|
|
1769
|
+
function stripExport(line) {
|
|
1770
|
+
const m = /^export[ \t]+/.exec(line);
|
|
1771
|
+
return m ? line.slice(m[0].length) : line;
|
|
1772
|
+
}
|
|
1773
|
+
function checkName(name, origin, line) {
|
|
1774
|
+
const bad = /\p{White_Space}|#/u.exec(name);
|
|
1775
|
+
if (bad) {
|
|
1776
|
+
const what = bad[0] === "#" ? "'#'" : "whitespace";
|
|
1777
|
+
throw new ConfigError(
|
|
1778
|
+
`${origin}:${line}: variable name "${name}" contains ${what}; the line is not NAME=value (spec F1.7)`,
|
|
1779
|
+
name
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1664
1783
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1665
1784
|
0 && (module.exports = {
|
|
1666
1785
|
loadEnv,
|
package/dist/adapters/env.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Config } from '../config-
|
|
1
|
+
import { C as Config } from '../config-Dlq6rF3w.cjs';
|
|
2
2
|
|
|
3
3
|
type EnvOptions = {
|
|
4
4
|
/**
|
|
@@ -22,6 +22,16 @@ type EnvOptions = {
|
|
|
22
22
|
* Reading a single variable needs nothing from here — HOCON's own `${?VAR}`
|
|
23
23
|
* already does that. This is for mounting a whole namespace as a subtree.
|
|
24
24
|
*
|
|
25
|
+
* `__` is the only path separator: a single `_` stays part of its segment
|
|
26
|
+
* (`APP_DB__MAX_CONN` → `db.max_conn`), and a literal `.` in a variable name is
|
|
27
|
+
* key text rather than a boundary, so `APP_FOO.BAR` becomes the single
|
|
28
|
+
* top-level key `foo.bar` — addressable as the quoted path `"foo.bar"` — and
|
|
29
|
+
* coexists with `APP_FOO__BAR` instead of colliding with it (F1.2).
|
|
30
|
+
*
|
|
31
|
+
* Two names that do map to one path are an error, the environment having no
|
|
32
|
+
* order to break the tie with (F1.6). Keys named `__proto__`, `constructor` or
|
|
33
|
+
* `prototype` are kept like any other (F2.9).
|
|
34
|
+
*
|
|
25
35
|
* Values are always strings, and a `${...}` inside one stays literal (F0.2, F1.4).
|
|
26
36
|
*/
|
|
27
37
|
declare function loadEnv(opts?: EnvOptions & {
|
|
@@ -35,6 +45,11 @@ declare function loadEnv(opts?: EnvOptions & {
|
|
|
35
45
|
* double quotes with `\n \r \t \\ \"`. Multi-line values and trailing comments
|
|
36
46
|
* are not supported — an unquoted value containing ` #` is an error rather than
|
|
37
47
|
* a guess about whether a comment was meant. No `${...}` expansion.
|
|
48
|
+
*
|
|
49
|
+
* Names map to paths exactly as in {@link loadEnv}: `__` is the only separator,
|
|
50
|
+
* so a literal `.` stays key text (`FOO.BAR` → the single key `foo.bar`). A
|
|
51
|
+
* file has a definite line order, so a repeated name is last-wins (F0.7)
|
|
52
|
+
* rather than the collision error the process environment gets.
|
|
38
53
|
*/
|
|
39
54
|
declare function parseDotEnv(input: string, opts?: EnvOptions): Config;
|
|
40
55
|
|