@o3co/ts.hocon 1.9.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
 
@@ -391,13 +417,79 @@ const config = parseWithSchema(hoconInput, schema) // fails fast on startup
391
417
 
392
418
  The four parser implementations ([ts.hocon](https://github.com/o3co/ts.hocon), [rs.hocon](https://github.com/o3co/rs.hocon), [go.hocon](https://github.com/o3co/go.hocon), [py.hocon](https://github.com/o3co/py.hocon)) are all tracked against the same Lightbend HOCON spec — see the [cross-impl roll-up](https://github.com/o3co/xx.hocon/blob/main/docs/compliance-matrix.md) for per-impl conformance rates.
393
419
 
420
+ ## Format adapters
421
+
422
+ Config files that belong to *other* programs can be mounted as HOCON, so a
423
+ `${...}` in your document can reach into them:
424
+
425
+ ```ts
426
+ import { parseStringWithOptions } from '@o3co/ts.hocon'
427
+ import { loadEnv } from '@o3co/ts.hocon/adapters/env'
428
+
429
+ const base = loadEnv({ prefix: 'APP_' }) // APP_DB__HOST -> db.host
430
+ const cfg = parseStringWithOptions(src, { resolveSubstitutions: false })
431
+ const merged = cfg.withFallback(base).resolve()
432
+ ```
433
+
434
+ Deferring resolution matters: the plain `parse` resolves as it goes, so a
435
+ `${...}` aimed at the fallback would fail before the fallback is attached.
436
+
437
+ | Subpath | Needs | Notes |
438
+ | --- | --- | --- |
439
+ | `@o3co/ts.hocon/adapters/properties` | — | `java.util.Properties`, sharing the `include` syntax layer |
440
+ | `@o3co/ts.hocon/adapters/env` | — | Bulk-mounts a prefixed namespace; also reads `.env` |
441
+ | `@o3co/ts.hocon/adapters/jsonc` | — | JSON with comments and trailing commas |
442
+ | `@o3co/ts.hocon/adapters/toml` | `smol-toml` | Optional peer dependency |
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 |
444
+
445
+ The TOML and YAML libraries are **optional peer dependencies**, so installing
446
+ this package still pulls in nothing — you add the one you actually use. Plain
447
+ JSON needs no adapter at all, HOCON being a JSON superset.
448
+
449
+ Foreign data stays data: a `${a.b}` in a mounted value is literal text, never a
450
+ reference, because the file belongs to a program that never agreed to HOCON's
451
+ syntax.
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
+
394
487
  ## Known Limitations
395
488
 
396
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()`.
397
490
  - **`include classpath(...)`** is not supported. This is a JVM-specific include form with no equivalent outside Java runtimes.
398
491
  - **No watch/reload** — the library parses config at load time. For live-reloading, re-call `parse()` or `parseFile()` on change.
399
492
  - **No streaming parser** — the entire input is loaded into memory. For very large configs, validate input size before parsing (see Security Considerations).
400
- - **`.properties` include** — supports basic `key=value` / `key:value` syntax. Does not support multiline values (backslash continuation), Unicode escapes, or key escaping from the full Java .properties specification.
401
493
 
402
494
  ## Security Considerations
403
495
 
@@ -406,6 +498,34 @@ When parsing untrusted HOCON input, be aware of:
406
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.
407
499
  - **Input size:** The parser has no built-in input size limit. For untrusted input, validate size before calling `parse()`.
408
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 `{}`.
409
529
 
410
530
  ## License
411
531