@o3co/ts.hocon 1.10.0 → 1.11.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
 
@@ -354,6 +369,17 @@ const cfg = await parseAsync(hoconString, {
354
369
  - **Minimize `${ENV}` usage**: Prefer `${?ENV}` (optional) with sensible defaults defined in the config itself
355
370
  - **Never require env vars for local development**: Defaults should work out of the box
356
371
  - **Document required env vars**: List them in your project's README or a `.env.example`
372
+ - **Name variables with `__`, never `.`**: when bulk-mounting with `loadEnv` (or
373
+ `parseDotEnv`), `__` is the *only* thing that creates hierarchy. A dot is
374
+ ordinary key text, so the two spellings below are different keys and both
375
+ survive:
376
+
377
+ ```text
378
+ APP_FOO__BAR=nested # -> foo.bar : cfg.getString('foo.bar')
379
+ APP_FOO.BAR=dotted # -> "foo.bar" : cfg.getString('"foo.bar"')
380
+ ```
381
+
382
+ A single `_` also stays part of its segment (`APP_DB__MAX_CONN` → `db.max_conn`).
357
383
 
358
384
  ### Dev / Prod Separation
359
385
 
@@ -414,7 +440,7 @@ Deferring resolution matters: the plain `parse` resolves as it goes, so a
414
440
  | `@o3co/ts.hocon/adapters/env` | — | Bulk-mounts a prefixed namespace; also reads `.env` |
415
441
  | `@o3co/ts.hocon/adapters/jsonc` | — | JSON with comments and trailing commas |
416
442
  | `@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 |
443
+ | `@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
444
 
419
445
  The TOML and YAML libraries are **optional peer dependencies**, so installing
420
446
  this package still pulls in nothing — you add the one you actually use. Plain
@@ -424,6 +450,40 @@ Foreign data stays data: a `${a.b}` in a mounted value is literal text, never a
424
450
  reference, because the file belongs to a program that never agreed to HOCON's
425
451
  syntax.
426
452
 
453
+ ### Environment variable names
454
+
455
+ `__` is the only path separator, so a literal `.` in a variable name is key
456
+ text rather than a boundary — `APP_FOO.BAR` and `APP_FOO__BAR` are two
457
+ different keys and can be set at the same time:
458
+
459
+ ```ts
460
+ const cfg = loadEnv({ prefix: 'APP_', env: { 'APP_FOO.BAR': 'dotted', APP_FOO__BAR: 'nested' } })
461
+ cfg.getString('"foo.bar"') // "dotted" — one key whose name contains a dot
462
+ cfg.getString('foo.bar') // "nested" — foo -> bar
463
+ ```
464
+
465
+ ### Numbers from JSONC and YAML
466
+
467
+ Integers are ingested losslessly: an integer literal too wide for a JS `number`
468
+ keeps its digits instead of being rounded, and one outside the int64 range is
469
+ refused rather than silently mangled. What you get back depends on the getter:
470
+
471
+ ```ts
472
+ const cfg = parseJsonc('{"id": 9007199254740993}')
473
+ cfg.getString('id') // "9007199254740993" — exact, the source's own text
474
+ cfg.getNumber('id') // 9007199254740992 — the JS number model rounds
475
+ cfg.toObject() // { id: 9007199254740992 }
476
+ ```
477
+
478
+ So read large identifiers (snowflake IDs, ledger sequence numbers) with
479
+ `getString`. `getNumber` and `toObject` apply JavaScript's own number
480
+ semantics, exactly as they do for the same literal written in HOCON text — the
481
+ guarantee is that nothing is lost *on the way in*.
482
+
483
+ YAML scalar resolution otherwise belongs to the `yaml` library; the adapter
484
+ declares `version: '1.2'` rather than trusting a default, and `fromYamlValue`
485
+ takes an already-decoded tree so you can use a different library or schema.
486
+
427
487
  ## Known Limitations
428
488
 
429
489
  - **`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 +498,34 @@ When parsing untrusted HOCON input, be aware of:
438
498
  - **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
499
  - **Input size:** The parser has no built-in input size limit. For untrusted input, validate size before calling `parse()`.
440
500
  - **Include depth:** Limited to 50 levels to prevent stack overflow from deep include chains.
501
+ - **Prototype pollution:** keys named `__proto__`, `constructor` or `prototype`
502
+ are **kept as ordinary keys** — a `.properties` file or environment variable
503
+ may legitimately use them, and dropping them would be silent data loss.
504
+ Safety inside this library is structural rather than a denylist: the nesting
505
+ step builds null-prototype objects, which inherit no `__proto__` setter, every
506
+ adapter materializes objects by defining own data properties, and `toObject()`
507
+ does the same. Parsing pollutes nothing, and the returned object's prototype is
508
+ always `Object.prototype`.
509
+
510
+ **What you do with the result matters**, because config data can now hand you
511
+ an own `__proto__` key:
512
+
513
+ ```ts
514
+ const data = cfg.toObject()
515
+ { ...data } // safe
516
+ structuredClone(data) // safe
517
+ Object.assign({}, data) // NOT safe — the key vanishes and config data becomes
518
+ // the copy's prototype
519
+ deepMerge({}, data) // NOT safe — a naive recursive merge can write onto
520
+ // the global Object.prototype
521
+ ```
522
+
523
+ Both hazards come from the *destination* object's prototype, so they are the
524
+ consumer's to avoid: copy with spread or `structuredClone`, iterate with
525
+ `Object.entries()` / `Object.keys()`, and treat `__proto__` as an ordinary
526
+ key name. `cfg.toObject({ nullPrototype: true })` returns a tree that inherits
527
+ nothing — useful when the data is going somewhere you do not control, though it
528
+ cannot fix a consumer that copies into a plain `{}`.
441
529
 
442
530
  ## License
443
531
 
@@ -550,37 +550,48 @@ 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
+ }
553
559
 
554
560
  // src/internal/properties/properties.ts
555
561
  function nestPairs(pairs) {
556
562
  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);
563
+ const sorted = [...pairs].sort(([a], [b]) => compareSegments(a, b));
564
+ for (const [segments, value] of sorted) {
565
+ setNested(root, segments, value);
560
566
  }
561
567
  return root;
562
568
  }
563
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
569
+ function compareSegments(a, b) {
570
+ const n = Math.min(a.length, b.length);
571
+ for (let i = 0; i < n; i++) {
572
+ const x = a[i];
573
+ const y = b[i];
574
+ if (x !== y) return x < y ? -1 : 1;
575
+ }
576
+ return a.length - b.length;
577
+ }
578
+ function pathKey(segments) {
579
+ return JSON.stringify(segments);
580
+ }
564
581
  function setNested(obj, segments, value) {
565
582
  let current = obj;
566
583
  for (let i = 0; i < segments.length - 1; i++) {
567
584
  const seg = segments[i];
568
- if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
569
585
  if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
570
586
  current[seg] = /* @__PURE__ */ Object.create(null);
571
587
  }
572
588
  current = current[seg];
573
589
  }
574
590
  const last = segments[segments.length - 1];
575
- if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
576
591
  if (typeof current[last] === "object" && current[last] !== null) return;
577
592
  current[last] = value;
578
593
  }
579
594
 
580
- // src/internal/resolver/include-loader.ts
581
- var import_meta = {};
582
- var _require = (0, import_node_module.createRequire)(import_meta.url);
583
-
584
595
  // src/value/numeric-array.ts
585
596
  function numericObjectToArray(value) {
586
597
  if (value.kind !== "object") return null;
@@ -1168,10 +1179,10 @@ var Config = class _Config {
1168
1179
  }
1169
1180
  if (v.kind === "object") {
1170
1181
  const converted = numericObjectToArray(v);
1171
- if (converted !== null) return converted.map(hoconToJs);
1182
+ if (converted !== null) return converted.map((item) => hoconToJs(item));
1172
1183
  }
1173
1184
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path}`, path);
1174
- return v.items.map(hoconToJs);
1185
+ return v.items.map((item) => hoconToJs(item));
1175
1186
  }
1176
1187
  has(path) {
1177
1188
  return this.lookupNode(path) !== void 0;
@@ -1299,8 +1310,52 @@ var Config = class _Config {
1299
1310
  originDescription: this._originDescription
1300
1311
  });
1301
1312
  }
1302
- toObject() {
1303
- return hoconToJs(this.root);
1313
+ /**
1314
+ * The whole config as plain JS data: objects, arrays, strings, numbers,
1315
+ * booleans and `null`.
1316
+ *
1317
+ * Keys are reproduced exactly as the config carries them, `__proto__`,
1318
+ * `constructor` and `prototype` included: each is defined as an **own data
1319
+ * property**, so the returned object's prototype is always
1320
+ * `Object.prototype`, and this call writes nothing to the global one.
1321
+ *
1322
+ * ### Passing the result to other code
1323
+ *
1324
+ * A config file — especially a `.properties` file or an env namespace you
1325
+ * merged in — can therefore hand you an own `__proto__` key, and what happens
1326
+ * next depends entirely on how the consumer copies it:
1327
+ *
1328
+ * ```ts
1329
+ * const data = cfg.toObject()
1330
+ * { ...data } // safe — own properties are copied as data
1331
+ * structuredClone(data) // safe
1332
+ * Object.assign({}, data) // NOT safe — [[Set]] on the {} target hits
1333
+ * // Object.prototype's __proto__ setter: the key
1334
+ * // vanishes and config data becomes the result's
1335
+ * // prototype
1336
+ * deepMerge({}, data) // NOT safe — a naive recursive merge reads
1337
+ * // target['__proto__'], gets Object.prototype,
1338
+ * // and writes to it: global pollution
1339
+ * for (const k in data) … // prefer Object.entries / Object.keys
1340
+ * ```
1341
+ *
1342
+ * Those two hazards come from the **destination** object's prototype, so no
1343
+ * option here can disarm them — copy with spread or `structuredClone` instead.
1344
+ *
1345
+ * What `{ nullPrototype: true }` does give you is a result that inherits
1346
+ * nothing: every object comes back with a `null` prototype, so reading
1347
+ * `x.__proto__`, `x.constructor` or `x.toString` yields config data or
1348
+ * `undefined` rather than something from `Object.prototype`, and code that
1349
+ * merges *into* this object cannot climb out of it. Keys are all still
1350
+ * present. The trade-off is the usual one for prototype-less objects: not
1351
+ * `instanceof Object`, and no `hasOwnProperty` of their own.
1352
+ *
1353
+ * Numbers follow the JS number model, so an integer wider than 2^53 rounds
1354
+ * here even when the config holds it exactly — read those with
1355
+ * {@link Config.getString} instead.
1356
+ */
1357
+ toObject(opts) {
1358
+ return hoconToJs(this.root, opts?.nullPrototype === true);
1304
1359
  }
1305
1360
  /**
1306
1361
  * @internal Test-only: renders this resolved Config's value tree as canonical
@@ -1412,16 +1467,25 @@ function scalarToJs(raw, valueType) {
1412
1467
  return raw;
1413
1468
  }
1414
1469
  }
1415
- function hoconToJs(v) {
1470
+ function hoconToJs(v, nullPrototype = false) {
1416
1471
  switch (v.kind) {
1417
1472
  case "scalar":
1418
1473
  return scalarToJs(v.raw, v.valueType);
1419
1474
  case "array":
1420
- return v.items.map(hoconToJs);
1475
+ return v.items.map((item) => hoconToJs(item, nullPrototype));
1421
1476
  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);
1477
+ const entries = Array.from(v.fields, ([k, val]) => [k, hoconToJs(val, nullPrototype)]);
1478
+ if (nullPrototype) {
1479
+ const obj = /* @__PURE__ */ Object.create(null);
1480
+ for (const [k, val] of entries) Object.defineProperty(obj, k, {
1481
+ value: val,
1482
+ writable: true,
1483
+ enumerable: true,
1484
+ configurable: true
1485
+ });
1486
+ return obj;
1487
+ }
1488
+ return Object.fromEntries(entries);
1425
1489
  }
1426
1490
  }
1427
1491
  }
@@ -1510,6 +1574,8 @@ function fromMap(values, originDescription) {
1510
1574
  const root = { kind: "object", fields };
1511
1575
  return Config._fromResolvedValue(root, { originDescription });
1512
1576
  }
1577
+ var INT64_MAX = 9223372036854775807n;
1578
+ var INT64_MIN = -9223372036854775808n;
1513
1579
  function coerceObject(obj, atPath) {
1514
1580
  const fields = /* @__PURE__ */ new Map();
1515
1581
  for (const [k, v] of Object.entries(obj)) {
@@ -1538,14 +1604,14 @@ function coerceValue(v, atPath) {
1538
1604
  atPath
1539
1605
  );
1540
1606
  }
1541
- return { kind: "scalar", raw: String(v), valueType: "number" };
1607
+ return { kind: "scalar", raw: Object.is(v, -0) ? "-0" : String(v), valueType: "number" };
1542
1608
  }
1543
1609
  case "boolean":
1544
1610
  return { kind: "scalar", raw: v ? "true" : "false", valueType: "boolean" };
1545
1611
  case "bigint": {
1546
- if (v > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER)) {
1612
+ if (v > INT64_MAX || v < INT64_MIN) {
1547
1613
  throw new ConfigError(
1548
- `fromMap: bigint ${v} exceeds Number.MAX_SAFE_INTEGER (not representable as HOCON number) at path "${atPath}"`,
1614
+ `fromMap: integer ${v} is out of int64 range (HOCON integers are int64; spec F0.5) at path "${atPath}"`,
1549
1615
  atPath
1550
1616
  );
1551
1617
  }
@@ -1586,11 +1652,12 @@ function loadEnv(opts = {}) {
1586
1652
  const value = source[name];
1587
1653
  if (value === void 0) continue;
1588
1654
  const path = toPath(name.slice(prefix.length));
1589
- const prev = seen.get(path);
1655
+ const key = pathKey(path);
1656
+ const prev = seen.get(key);
1590
1657
  if (prev !== void 0) {
1591
- throw new ConfigError(`loadEnv: ${prev} and ${name} both map to "${path}"`, path);
1658
+ throw new ConfigError(`loadEnv: ${prev} and ${name} both map to "${path.join(".")}"`, path.join("."));
1592
1659
  }
1593
- seen.set(path, name);
1660
+ seen.set(key, name);
1594
1661
  pairs.push([path, value]);
1595
1662
  }
1596
1663
  return fromMap(nestPairs(pairs), opts.originDescription ?? "environment variables");
@@ -1599,7 +1666,7 @@ function parseDotEnv(input, opts = {}) {
1599
1666
  const origin = opts.originDescription ?? ".env";
1600
1667
  const prefix = opts.prefix ?? "";
1601
1668
  const pairs = [];
1602
- const lines = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
1669
+ const lines = stripBom(input).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
1603
1670
  for (let i = 0; i < lines.length; i++) {
1604
1671
  const raw = (lines[i] ?? "").trim();
1605
1672
  if (raw === "" || raw.startsWith("#")) continue;
@@ -1615,11 +1682,14 @@ function parseDotEnv(input, opts = {}) {
1615
1682
  return fromMap(nestPairs(pairs), origin);
1616
1683
  }
1617
1684
  function toPath(name) {
1618
- const segs = name.split(SEPARATOR).map((s) => s.toLowerCase());
1685
+ const segs = name.split(SEPARATOR).map(asciiLower);
1619
1686
  if (segs.some((s) => s === "")) {
1620
1687
  throw new ConfigError(`env: "${name}" produces an empty path segment`, name);
1621
1688
  }
1622
- return segs.join(".");
1689
+ return segs;
1690
+ }
1691
+ function asciiLower(s) {
1692
+ return s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
1623
1693
  }
1624
1694
  function dotEnvValue(v, origin, line, name) {
1625
1695
  const fail = (msg) => {
@@ -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
 
@@ -1,4 +1,4 @@
1
- import { C as Config } from '../config-PdhVgeLS.js';
1
+ import { C as Config } from '../config-Dlq6rF3w.js';
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