@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 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). The table below is a snapshot as of 2026-05-13; see [`xx.hocon/docs/compliance-matrix.md`](https://github.com/o3co/xx.hocon/blob/main/docs/compliance-matrix.md) for live cross-impl values.
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) | **74.2%** |
258
- | In-scope only | **83.3%** |
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
- ts.hocon provides significantly richer configuration capabilities compared to [node-config](https://github.com/node-config/node-config) (JSON):
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; YAML 1.2 core schema |
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
 
@@ -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 < b ? -1 : a > b ? 1 : 0);
558
- for (const [key, value] of sorted) {
559
- setNested(root, key.split("."), value);
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
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
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
- toObject() {
1303
- return hoconToJs(this.root);
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 obj = /* @__PURE__ */ Object.create(null);
1423
- for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
1424
- return Object.assign({}, obj);
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 = coerceObject(values, "");
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 > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER)) {
1640
+ if (v > INT64_MAX || v < INT64_MIN) {
1547
1641
  throw new ConfigError(
1548
- `fromMap: bigint ${v} exceeds Number.MAX_SAFE_INTEGER (not representable as HOCON number) at path "${atPath}"`,
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 prev = seen.get(path);
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(path, name);
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 = raw.startsWith("export ") ? raw.slice("export ".length) : raw;
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((s) => s.toLowerCase());
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
- return segs.join(".");
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,
@@ -1,4 +1,4 @@
1
- import { C as Config } from '../config-PdhVgeLS.cjs';
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