@o3co/ts.hocon 1.13.0 → 1.14.1
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 +253 -29
- package/README.md +161 -29
- package/dist/adapters/env.cjs +26 -2018
- package/dist/adapters/env.d.cts +1 -1
- package/dist/adapters/env.d.ts +1 -1
- package/dist/adapters/env.js +11 -1965
- package/dist/adapters/json5.cjs +15 -1969
- package/dist/adapters/json5.d.cts +1 -1
- package/dist/adapters/json5.d.ts +1 -1
- package/dist/adapters/json5.js +12 -1929
- package/dist/adapters/jsonc.cjs +17 -1972
- package/dist/adapters/jsonc.d.cts +1 -1
- package/dist/adapters/jsonc.d.ts +1 -1
- package/dist/adapters/jsonc.js +9 -1926
- package/dist/adapters/properties.cjs +7 -2113
- package/dist/adapters/properties.d.cts +1 -1
- package/dist/adapters/properties.d.ts +1 -1
- package/dist/adapters/properties.js +6 -2075
- package/dist/adapters/toml.cjs +12 -1966
- package/dist/adapters/toml.d.cts +1 -1
- package/dist/adapters/toml.d.ts +1 -1
- package/dist/adapters/toml.js +10 -1929
- package/dist/adapters/yaml.cjs +20 -1975
- package/dist/adapters/yaml.d.cts +1 -1
- package/dist/adapters/yaml.d.ts +1 -1
- package/dist/adapters/yaml.js +10 -1929
- package/dist/chunk-GOXMKYWS.js +3801 -0
- package/dist/chunk-JVABBNF5.js +158 -0
- package/dist/chunk-L3D2RE2U.cjs +3801 -0
- package/dist/chunk-OUPKYWL3.cjs +158 -0
- package/dist/chunk-PCAMDXJ4.cjs +83 -0
- package/dist/chunk-YRNDXYGQ.js +83 -0
- package/dist/{config-6sApTEMM.d.cts → config-OccaKqXu.d.cts} +126 -4
- package/dist/{config-6sApTEMM.d.ts → config-OccaKqXu.d.ts} +126 -4
- package/dist/index.cjs +34 -3793
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +25 -3728
- package/dist/zod.cjs +195 -3636
- package/dist/zod.d.cts +86 -3
- package/dist/zod.d.ts +86 -3
- package/dist/zod.js +182 -3589
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -10,9 +10,11 @@ A [Lightbend HOCON](https://github.com/lightbend/config/blob/main/HOCON.md) pars
|
|
|
10
10
|
> **Implemented by [Claude](https://claude.ai/) (Anthropic)** — designed and built end-to-end with Claude Code.
|
|
11
11
|
> Reviewed by [GitHub Copilot](https://github.com/features/copilot) and [OpenAI Codex](https://openai.com/index/openai-codex/).
|
|
12
12
|
|
|
13
|
+
**Docs:** [o3co.github.io/hocon](https://o3co.github.io/hocon/) — the cross-language hub for all four implementations, the hocon2 CLI and the shared conformance corpus.
|
|
14
|
+
|
|
13
15
|
[日本語](README.ja.md)
|
|
14
16
|
|
|
15
|
-
> **Library stance:** ts.hocon is a HOCON config loader — its purpose is reading `.hocon` config files and providing typed access via the Config API (`getString`, `getNumber`, `getBoolean`, `getDuration`, `getBytes`, `toObject`). It is not a low-level parser API. Internal types like `HoconValue` may change between minor versions.
|
|
17
|
+
> **Library stance:** ts.hocon is a HOCON config loader — its purpose is reading `.hocon` config files and providing typed access via the Config API (`getString`, `getNumber`, `getBoolean`, `getDuration`, `getBytes`, `getPeriod`, `toObject`). It is not a low-level parser API. Internal types like `HoconValue` may change between minor versions.
|
|
16
18
|
>
|
|
17
19
|
> **Cross-language conformance:** This implementation is tested against shared expected-JSON fixtures from [o3co/xx.hocon](https://github.com/o3co/xx.hocon) alongside [go.hocon](https://github.com/o3co/go.hocon), [rs.hocon](https://github.com/o3co/rs.hocon), and [py.hocon](https://github.com/o3co/py.hocon) to ensure all four implementations meet the same Lightbend HOCON specification.
|
|
18
20
|
|
|
@@ -36,12 +38,13 @@ import { parse } from '@o3co/ts.hocon' // ESM
|
|
|
36
38
|
const { parse } = require('@o3co/ts.hocon') // CJS
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
On every PR and every release, CI loads all
|
|
41
|
+
On every PR and every release, CI loads all eight exports both ways against the
|
|
40
42
|
built artifact, calls each one (including a package-scoped `include`, which
|
|
41
|
-
behaves differently per bundle format),
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
behaves differently per bundle format), checks that what a subpath returns or
|
|
44
|
+
throws is an instance of the classes the root exports, and checks that each
|
|
45
|
+
condition declares its own type declarations. That gate exists because 1.10.0
|
|
46
|
+
shipped with all seven of its CJS entrypoints throwing at load; it catches that
|
|
47
|
+
class of defect rather than promising none can exist.
|
|
45
48
|
|
|
46
49
|
### 2. Use
|
|
47
50
|
|
|
@@ -77,6 +80,49 @@ HOCON isn't just a serialization format — it's a **config-injection language**
|
|
|
77
80
|
|
|
78
81
|
On top of that, HOCON combines the readability of YAML with the structure of JSON — making it a strong fit for anything beyond flat key-value config.
|
|
79
82
|
|
|
83
|
+
## Configuration that stays inside its module
|
|
84
|
+
|
|
85
|
+
In a large codebase, configuration tends to leak: one big file that every team edits, defaults copied into each service, and modules that read each other's keys. HOCON lets each module own its configuration the way it owns its code.
|
|
86
|
+
|
|
87
|
+
- **Each module owns a namespace and ships its own defaults.** The `auth` module keeps `auth { … }` next to its code, with every default filled in. The application never copies those defaults; it brings them in with one `include`.
|
|
88
|
+
- **The application overrides only what differs.** Later layers deep-merge over earlier ones, so the application states two keys, not the whole block.
|
|
89
|
+
- **Each module reads only its own subtree, through its own schema.** The module asks for `auth` and validates it against the schema it defines. The shape of that section is the module's contract; nothing else in the file is visible to it.
|
|
90
|
+
- **Wiring between modules lives in the application.** The module only declares the keys it reads (`auth.db`) and leaves them without a default when the application must supply them. The application connects them to shared values (`auth.db = ${shared.db}`). Modules never reference each other or the application's keys, so dependencies run one way.
|
|
91
|
+
|
|
92
|
+
```hocon
|
|
93
|
+
# auth/reference.conf: shipped inside the auth package, owned by the auth team
|
|
94
|
+
auth {
|
|
95
|
+
session { lifetime = 30m, max-body = 64KiB }
|
|
96
|
+
# db: no default; the application supplies auth.db
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```hocon
|
|
101
|
+
# application.conf: owned by the application
|
|
102
|
+
include package("github.com/acme/auth", "reference.conf")
|
|
103
|
+
|
|
104
|
+
shared.db { host = db.internal, port = 5432 }
|
|
105
|
+
auth.db = ${shared.db} # wiring lives in the application
|
|
106
|
+
auth.session.lifetime = 2h # override what differs; the rest stays the module's default
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// inside the auth module
|
|
111
|
+
import { getValidated, hocon } from '@o3co/ts.hocon/zod'
|
|
112
|
+
import { z } from 'zod'
|
|
113
|
+
|
|
114
|
+
const Auth = z.object({
|
|
115
|
+
session: z.object({
|
|
116
|
+
lifetime: hocon.duration('s').pipe(z.number().int().positive()),
|
|
117
|
+
'max-body': hocon.bytes().pipe(z.number().int().positive()),
|
|
118
|
+
}),
|
|
119
|
+
db: z.object({ host: z.string(), port: z.number().int() }),
|
|
120
|
+
})
|
|
121
|
+
const auth = getValidated(config, 'auth', Auth)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`include package(...)` is not part of HOCON.md: it is a convention shared by the o3co implementations ([E11](https://github.com/o3co/xx.hocon/blob/main/docs/extra-spec-conventions.md#e11)), a non-JVM counterpart to `include classpath(...)`. It is how a module publishes its defaults without the application knowing where its files live (in ts.hocon, the default resolver hands `<identifier>/<file>` to Node module resolution from the including file, so the identifier there is the npm package name, such as `@acme/auth`, which E11 allows for TS-only config; a portable Go-style identifier like the one above needs a `packageResolver` that maps it to the package; see [`ParseOptions`](#parse-functions)). On the JVM, Lightbend's `reference.conf` convention plays the same role.
|
|
125
|
+
|
|
80
126
|
## Features
|
|
81
127
|
|
|
82
128
|
- Full HOCON parsing: objects, arrays, scalars, substitutions (`${path}`, `${?path}`)
|
|
@@ -85,12 +131,12 @@ On top of that, HOCON combines the readability of YAML with the structure of JSO
|
|
|
85
131
|
- `+=` append operator
|
|
86
132
|
- `include "file.conf"` and `include file("file.conf")` directives
|
|
87
133
|
- Triple-quoted strings (`"""..."""`)
|
|
88
|
-
- Duration
|
|
134
|
+
- Duration, byte size and period parsing (`getDuration()`, `getBytes()`, `getPeriod()`)
|
|
89
135
|
- HOCON emitter (`renderHocon()`) — renders a resolved `Config` back to HOCON text; re-parsing yields the same value tree (E18 round-trip contract)
|
|
90
136
|
- Sync and async API (`parse` / `parseAsync` / `parseFile` / `parseFileAsync`)
|
|
91
137
|
- ESM + CJS dual package (every entrypoint smoke-tested in both formats on each CI run)
|
|
92
138
|
- Optional [Zod](https://zod.dev/) integration for schema validation
|
|
93
|
-
-
|
|
139
|
+
- Runs in browsers (`parse`/`parseAsync`; the bundler must stub Node built-ins, see [Browser Compatibility](#browser-compatibility))
|
|
94
140
|
|
|
95
141
|
## API
|
|
96
142
|
|
|
@@ -115,28 +161,36 @@ parseFileAsync(path: string, opts?: ParseOptions): Promise<Config>
|
|
|
115
161
|
| `env` | `Record<string, string>` | Environment variables for substitution (default: `process.env`) |
|
|
116
162
|
| `readFileSync` | `(path: string) => string` | Custom file reader (sync) |
|
|
117
163
|
| `readFile` | `(path: string) => Promise<string>` | Custom file reader (async) |
|
|
164
|
+
| `resolveSubstitutions` | `boolean` | `false` returns an unresolved Config, to resolve later (default `true`; see [Deferred resolution](#deferred-resolution-api-e12)) |
|
|
165
|
+
| `originDescription` | `string` | Source name for resolve-error messages when there is no file path |
|
|
166
|
+
| `resolveFrom` | `string \| string[]` | Where `include package(...)` starts Node module resolution (default: `baseDir`, which for `parseFile` is the file's directory; else `process.cwd()`) |
|
|
167
|
+
| `packageResolver` | `PackageResolver` | Custom resolver for `include package(...)` (Yarn PnP, bundlers, edge runtimes, tests); `resolveFrom` is then ignored |
|
|
118
168
|
|
|
119
169
|
### Config methods
|
|
120
170
|
|
|
121
171
|
| Method | Returns | Throws if |
|
|
122
172
|
|--------|---------|-----------|
|
|
123
173
|
| `get(path)` | `unknown \| undefined` | — |
|
|
174
|
+
| `getValue(path)` (the raw value tree, for introspection) | `ReadonlyHoconValue \| undefined` | unresolved |
|
|
124
175
|
| `getString(path)` | `string` | missing, wrong type, or unresolved |
|
|
125
176
|
| `getNumber(path)` | `number` | missing, wrong type, or unresolved |
|
|
126
177
|
| `getBoolean(path)` | `boolean` | missing, wrong type, or unresolved |
|
|
127
178
|
| `getConfig(path)` | `Config` | missing, not an object, or unresolved |
|
|
128
179
|
| `getList(path)` | `unknown[]` | missing, not an array, or unresolved |
|
|
129
|
-
| `getDuration(path, unit?)` | `number` | missing, not a string
|
|
130
|
-
| `getBytes(path, unit?)` | `number` | missing, not a string
|
|
180
|
+
| `getDuration(path, unit?)` | `number` | missing, not a string or number, invalid duration format, or not a finite number (E21) |
|
|
181
|
+
| `getBytes(path, unit?)` | `number` | missing, not a string or number, invalid byte size format, negative, or beyond 2^53-1 bytes |
|
|
182
|
+
| `getPeriod(path)` | `Period` | missing, not a string or number, or invalid period format |
|
|
131
183
|
| `has(path)` | `boolean` | — |
|
|
132
184
|
| `keys()` | `string[]` | — |
|
|
133
185
|
| `withFallback(fallback)` | `Config` | — |
|
|
134
186
|
| `resolve(opts?)` | `Config` | unresolvable substitution (unless `allowUnresolved: true`) |
|
|
135
187
|
| `resolveWith(source, opts?)` | `Config` | source unresolved, or unresolvable substitution |
|
|
136
188
|
| `isResolved()` | `boolean` | — |
|
|
137
|
-
| `toObject()` | `unknown` | — |
|
|
189
|
+
| `toObject(opts?)` | `unknown` | — (`{ nullPrototype: true }` returns objects that inherit nothing; see [Security Considerations](#security-considerations)) |
|
|
138
190
|
| `renderHocon()` | `string` | unresolved |
|
|
139
191
|
|
|
192
|
+
Every "Throws if" above is a `ConfigError` with `.path`, with two exceptions. An unresolved value read by `getValue` or a getter, or an unresolved `resolveWith` source, throws `NotResolvedError` (a `ConfigError` subclass). An unresolvable substitution in `resolve` / `resolveWith` throws `ResolveError`.
|
|
193
|
+
|
|
140
194
|
### Deferred resolution API (E12)
|
|
141
195
|
|
|
142
196
|
Separate the parse, fallback-layering, and resolve steps for runtime config injection.
|
|
@@ -173,7 +227,7 @@ result.getString('r') // "val"
|
|
|
173
227
|
|
|
174
228
|
| Option | Type | Default | Description |
|
|
175
229
|
|--------|------|---------|-------------|
|
|
176
|
-
| `useSystemEnvironment` | `boolean` | `true` | Consult `process.env` for substitution fallback |
|
|
230
|
+
| `useSystemEnvironment` | `boolean` | `true` | Consult the environment (`ParseOptions.env`, default `process.env`) for substitution fallback |
|
|
177
231
|
| `allowUnresolved` | `boolean` | `false` | Leave unresolvable substitutions in place (no error) |
|
|
178
232
|
|
|
179
233
|
### Zod integration
|
|
@@ -196,21 +250,58 @@ const app = validate(cfg, Schema)
|
|
|
196
250
|
const port = getValidated(cfg, 'server.port', z.number().int())
|
|
197
251
|
```
|
|
198
252
|
|
|
253
|
+
A string is coerced where the schema expects a number or a boolean, so `port = ${?PORT}` with `PORT=8081` validates against `z.number()`. The coercion follows the schema through objects, arrays, records (each value), tuples, unions, pipes (by the input schema), lazy schemas and the optional / nullable / default / prefault / nonoptional / catch / readonly wrappers, and turns a string into a number or boolean literal (`"1"` for `z.literal(1)`). It does not walk an object's `catchall` or an intersection. A `z.discriminatedUnion` coerces by the option the discriminator picks. Any other union takes the value as it is when an option accepts it, so `z.union([z.number(), z.string()])` keeps `"5"` a string, and coerces it otherwise, by the first option that accepts the coerced value (`z.union([z.number(), z.literal('auto')])` reads `"8081"` as 8081). Finding that option tries the options in turn, so a deeply recursive plain union validates slowly; prefer `z.discriminatedUnion` there.
|
|
254
|
+
|
|
255
|
+
Durations and byte sizes are strings in HOCON (`30m`, `64KiB`), so `z.number()` refuses them. The `hocon.duration(unit?)` and `hocon.bytes(unit?)` markers read them with the same rules as `getDuration()` / `getBytes()` and give a number in the requested unit (default `ms` / `B`; a plain number keeps the getters' meaning). Pipe the marker into a number schema for range and integer checks:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { hocon, getValidated } from '@o3co/ts.hocon/zod'
|
|
259
|
+
|
|
260
|
+
const Session = z.object({
|
|
261
|
+
lifetime: hocon.duration('s').pipe(z.number().int().min(60).max(86_400)),
|
|
262
|
+
maxBody: hocon.bytes().pipe(z.number().int().positive()),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// session { lifetime = 30m, maxBody = 64KiB }
|
|
266
|
+
getValidated(cfg, 'session', Session) // { lifetime: 1800, maxBody: 65536 }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`hocon.period()` reads a period the way `getPeriod()` does and gives a `Period` (`{ years, months, days }`); refine it in the schema:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const Backup = z.object({
|
|
273
|
+
keep: hocon.period().refine((p) => p.years === 0, 'use months or days'),
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// backup { keep = 2w }
|
|
277
|
+
getValidated(cfg, 'backup', Backup) // { keep: { years: 0, months: 0, days: 14 } }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
An invalid duration, size or period fails validation with the key's path. The issue message names the problem, not the value (`not a duration`, `byte size must be non-negative`), so a secret set under the wrong key does not reach the logs; `params.problem` is `invalid`, `negative` or `overflow`, and the value is in the issue's `input` only when Zod is asked to report input. The markers are ordinary Zod schemas, so they work in `validate`, `getValidated`, `parseWithSchema`, `parseFileWithSchema` and anywhere else Zod runs them. Getters and markers both read exponent forms (`1e3`, `"1.5e2s"`). A marker reads a number through its JavaScript value, though, so a number literal with more significant digits than a double holds can read differently from the getters, which read the literal's text exactly (`1.99999999999999999` B is 1 through `getBytes()`, 2 through the marker); a period marker also reads a literal with a decimal point or an exponent (`7.0`, `1e2`) as an integer, which `getPeriod()` refuses. Write unit values with a unit (`30s`, `7d`) to avoid both.
|
|
281
|
+
|
|
282
|
+
On an unresolved Config (parsed with `resolveSubstitutions: false`, a fallback stack not yet `resolve()`d, or the result of `resolve({ allowUnresolved: true })` with something left), `validate` throws `NotResolvedError`, as the getters do, rather than validating a tree with the pending values missing; `getValidated` throws it for a path whose value is unresolved and validates a resolved one. Call `resolve()` first.
|
|
283
|
+
|
|
199
284
|
Install Zod as a peer dependency:
|
|
200
285
|
```bash
|
|
201
286
|
npm install zod
|
|
202
287
|
```
|
|
203
288
|
|
|
289
|
+
The Zod entry points (`validate`, `getValidated`, `parseWithSchema`, `parseFileWithSchema`) refuse an object key named `__proto__`, `constructor` or `prototype` anywhere in the value: Zod drops a `__proto__` key from a record without an error, and the three are refused alike so the result does not depend on the schema. The `ZodError` has one issue per key, at the object that holds it, with the key in the message and in `params.key` (`key "__proto__" names an object's prototype → at limits`), followed by the schema's issues for the rest of the value: the schema validates the value without the refused keys, so one run lists everything wrong. Rename the key, or read it with the plain getters (`cfg.getNumber('limits."constructor"')`), which are unaffected.
|
|
290
|
+
|
|
204
291
|
### Error types
|
|
205
292
|
|
|
206
293
|
```ts
|
|
207
|
-
import { ParseError, ResolveError, ConfigError } from '@o3co/ts.hocon'
|
|
294
|
+
import { ParseError, ResolveError, PackageLookupError, ConfigError, NotResolvedError } from '@o3co/ts.hocon'
|
|
208
295
|
|
|
209
|
-
// ParseError
|
|
210
|
-
// ResolveError
|
|
211
|
-
//
|
|
296
|
+
// ParseError — lexing/parsing failure: .line, .col, .file?
|
|
297
|
+
// ResolveError — substitution/include failure: .path, .line, .col, .file?
|
|
298
|
+
// PackageLookupError — include package(...) not found (a ResolveError): .identifier, .packageFile
|
|
299
|
+
// ConfigError — wrong type, missing path, invalid value: .path
|
|
300
|
+
// NotResolvedError — getter or Zod entry point on an unresolved value (a ConfigError): .path
|
|
212
301
|
```
|
|
213
302
|
|
|
303
|
+
Every entrypoint (`/zod`, `/adapters/*`) throws these same classes, so `instanceof` against the root's exports works across entrypoints within one module system. The Zod entry points throw `ZodError` for a value that does not match the schema.
|
|
304
|
+
|
|
214
305
|
## HOCON Examples
|
|
215
306
|
|
|
216
307
|
```hocon
|
|
@@ -262,8 +353,36 @@ c.getBytes('max-size') // 536870912 (bytes)
|
|
|
262
353
|
c.getBytes('max-size', 'MiB') // 512
|
|
263
354
|
```
|
|
264
355
|
|
|
265
|
-
Supported duration units: `ns`, `us`, `ms`, `s`, `m`, `h`, `d` (and long forms like `seconds`, `minutes`). Duration unit names are case-sensitive and must be lowercase (HOCON spec).
|
|
266
|
-
|
|
356
|
+
Supported duration units: `ns`, `us`, `ms`, `s`, `m`, `h`, `d` (and long forms like `seconds`, `minutes`). Duration unit names are case-sensitive and must be lowercase (HOCON spec). A number or a string with no unit is milliseconds for a duration and bytes for a byte size.
|
|
357
|
+
|
|
358
|
+
The number is read from its text as an exact decimal and multiplied by the unit exactly (E20). A duration is then rounded once in the requested unit: `1.001s` is `1001` ms (`1001000000` ns), where Lightbend's double arithmetic gives `1000999999` ns. A byte size is truncated toward zero to whole bytes (as Lightbend does) before it is converted: `1.0009kB` is `1000`. A duration whose value is not a finite number is an error (E21). A byte size must be non-negative and at most 2^53-1 bytes (the largest integer a JS number holds exactly), so 1 or more of any exa-, zetta- or yotta- unit (`EB`, `Ei`/`EiB`, `E`/`e`, `exabyte`, … through `Y`) is an error; use fractions (`0.001EiB`) or a smaller unit.
|
|
359
|
+
|
|
360
|
+
Supported byte units, the same set as Lightbend and case-sensitive like it:
|
|
361
|
+
|
|
362
|
+
- bytes: `B`, `b`, `byte`, `bytes`
|
|
363
|
+
- powers of 1000: `kB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, `YB`, and `kilobyte(s)` … `yottabyte(s)`
|
|
364
|
+
- powers of 1024: `Ki`/`KiB`, `Mi`/`MiB`, … `Yi`/`YiB`, and `kibibyte(s)` … `yobibyte(s)`
|
|
365
|
+
- single letters, powers of 1024 in either case (java `-Xmx` style): `K`/`k`, `M`/`m`, `G`/`g`, `T`/`t`, `P`/`p`, `E`/`e`, `Z`/`z`, `Y`/`y`
|
|
366
|
+
|
|
367
|
+
Other spellings are errors: `KB`, `kb`, `kib`, `Kilobytes`, `MEGABYTES`. (`'KB'` and the other names in `getBytes(path, unit)` are output units, a separate list.)
|
|
368
|
+
|
|
369
|
+
### Periods
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
const c = parse(`
|
|
373
|
+
retention = 2w
|
|
374
|
+
billing = "1 month"
|
|
375
|
+
archive = 1y
|
|
376
|
+
grace = 3 // a bare integer is days
|
|
377
|
+
`)
|
|
378
|
+
|
|
379
|
+
c.getPeriod('retention') // { years: 0, months: 0, days: 14 }
|
|
380
|
+
c.getPeriod('billing') // { years: 0, months: 1, days: 0 }
|
|
381
|
+
c.getPeriod('archive') // { years: 1, months: 0, days: 0 }
|
|
382
|
+
c.getPeriod('grace') // { years: 0, months: 0, days: 3 }
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Supported period units: `d`/`day`/`days`, `w`/`week`/`weeks` (folded into days), `m`/`mo`/`month`/`months`, `y`/`year`/`years`. Unit names are case-sensitive and must be lowercase (`m` is months here, not minutes). A period is integer only (`7.5` is an error, unlike durations and sizes), may be negative, and each component must fit in a 32-bit signed integer, as in Lightbend (`java.time.Period`), rs.hocon and py.hocon.
|
|
267
386
|
|
|
268
387
|
## Spec Compliance
|
|
269
388
|
|
|
@@ -271,7 +390,7 @@ Conformance against the [Lightbend HOCON specification](https://github.com/light
|
|
|
271
390
|
|
|
272
391
|
| Metric | Status |
|
|
273
392
|
| ------------------------------------- | ------------- |
|
|
274
|
-
| Spec total (incl. out-of-scope) | **
|
|
393
|
+
| Spec total (incl. out-of-scope) | **91.9%** |
|
|
275
394
|
| In-scope only | **100.0%** |
|
|
276
395
|
| Lightbend `test01`–`test13` suite | 13/13 passing |
|
|
277
396
|
|
|
@@ -279,15 +398,21 @@ Conformance against the [Lightbend HOCON specification](https://github.com/light
|
|
|
279
398
|
|
|
280
399
|
| Item | Description | Status |
|
|
281
400
|
|------|-------------|--------|
|
|
282
|
-
| E11 | `include package(...)` service-locator includes | ✅ v1.
|
|
401
|
+
| E11 | `include package(...)` service-locator includes | ✅ v1.4.0 |
|
|
283
402
|
| E12 | Deferred substitution resolution (`parse → withFallback → resolve()` lifecycle) | ✅ v1.4.0 |
|
|
403
|
+
| E18 | HOCON emitter round-trip contract (`renderHocon()`) | ✅ v1.13.0 |
|
|
404
|
+
| E19 | A float literal that overflows the double range (`1e999`) is a parse error | ✅ v1.14.0 |
|
|
405
|
+
| E20 | Unit arithmetic is exact decimal (durations: a deliberate divergence from Lightbend) | ✅ v1.14.0 |
|
|
406
|
+
| E21 | A duration the implementation cannot represent is an error | ✅ v1.14.0 |
|
|
407
|
+
|
|
408
|
+
The E-series items are conventions shared by the o3co implementations beyond HOCON.md; see [`xx.hocon/docs/extra-spec-conventions.md`](https://github.com/o3co/xx.hocon/blob/main/docs/extra-spec-conventions.md).
|
|
284
409
|
|
|
285
410
|
Not supported:
|
|
286
411
|
|
|
287
412
|
- `include url(...)`
|
|
288
413
|
- `include classpath(...)`
|
|
289
414
|
|
|
290
|
-
Supported since
|
|
415
|
+
Supported since v1.0.0 (P1):
|
|
291
416
|
|
|
292
417
|
- `.properties` file parsing
|
|
293
418
|
|
|
@@ -323,26 +448,33 @@ For typical application configs (loaded once at startup), the parsing cost is ne
|
|
|
323
448
|
|
|
324
449
|
### Feature Comparison with node-config
|
|
325
450
|
|
|
326
|
-
Feature-level comparison with [node-config](https://github.com/node-config/node-config) **
|
|
451
|
+
Feature-level comparison with [node-config](https://github.com/node-config/node-config) **v5.0.1** reading JSON config files, as of 2026-10-16:
|
|
327
452
|
|
|
328
453
|
| Feature | ts.hocon | node-config (JSON) |
|
|
329
454
|
|---|---|---|
|
|
330
|
-
| Comments | `//` `#` |
|
|
455
|
+
| Comments | `//` `#` | Yes (`//`, `/* */`; `.json` is parsed as JSON5) |
|
|
331
456
|
| Multi-line strings | `"""..."""` | No |
|
|
332
457
|
| Substitution (`${path}`) | Yes | No |
|
|
333
458
|
| Optional substitution (`${?path}`) | Yes | No |
|
|
334
459
|
| Environment variable reference | Yes (via substitution) | Partial (`custom-environment-variables` file) |
|
|
335
460
|
| Include | Yes | No |
|
|
336
|
-
| Deep merge | Yes (arrays
|
|
461
|
+
| Deep merge | Yes (objects; arrays replaced, `+=` / concatenation to extend) | Yes (objects; arrays replaced) |
|
|
337
462
|
| Append operator (`+=`) | Yes | No |
|
|
338
463
|
| Environment-based config | Configurable via HOCON | Yes (filename convention) |
|
|
339
464
|
| Schema validation | Zod integration | No |
|
|
340
465
|
| Programmatic API | `parse(string)` | File-based initialization, then `get()` |
|
|
341
|
-
| Typed getters | `getString`, `getNumber`, etc. | `get()` (
|
|
466
|
+
| Typed getters | `getString`, `getNumber`, etc. (checked at runtime) | `get<T>()` (type parameter only, no runtime check) |
|
|
342
467
|
|
|
343
468
|
## Browser Compatibility
|
|
344
469
|
|
|
345
|
-
`parse()` and `parseAsync()` work in browsers. `parseFile()` and `parseFileAsync()` require Node.js (or a custom `readFileSync`/`readFile` option).
|
|
470
|
+
`parse()` and `parseAsync()` work in browsers, as do the adapters and the Zod integration. `parseFile()` and `parseFileAsync()` require Node.js (or a custom `readFileSync`/`readFile` option).
|
|
471
|
+
|
|
472
|
+
The package still imports the Node built-ins `fs`, `path`, `module` and `url` (for file reading and `include package(...)`) and uses them only on those paths. A browser bundle needs them replaced by empty modules:
|
|
473
|
+
|
|
474
|
+
- **Vite** (7 and 8) does this by default, so there is nothing to configure.
|
|
475
|
+
- **Other bundlers:** map the four modules to an empty module (`export default {}`), for example with an esbuild plugin or webpack 5's `resolve.fallback: { fs: false, path: false, module: false, url: false }`. Marking them external is not enough, because the browser cannot load `fs`.
|
|
476
|
+
|
|
477
|
+
That covers parsing without file includes. Includes read through your own `readFile` / `readFileSync` (below) also resolve paths, so they need a browser implementation of `path` instead of the empty module, such as the `path-browserify` package. `include package(...)` needs a custom `packageResolver` there (see `ParseOptions`).
|
|
346
478
|
|
|
347
479
|
```ts
|
|
348
480
|
// Browser usage with custom file loader
|
|
@@ -409,7 +541,7 @@ const config = parseWithSchema(hoconInput, schema) // fails fast on startup
|
|
|
409
541
|
| Project | Language | Registry | Description |
|
|
410
542
|
|---------|----------|----------|-------------|
|
|
411
543
|
| [go.hocon](https://github.com/o3co/go.hocon) | Go | [pkg.go.dev](https://pkg.go.dev/github.com/o3co/go.hocon) | HOCON parser for Go |
|
|
412
|
-
| [rs.hocon](https://github.com/o3co/rs.hocon) | Rust | [crates.io](https://crates.io/crates/
|
|
544
|
+
| [rs.hocon](https://github.com/o3co/rs.hocon) | Rust | [crates.io](https://crates.io/crates/hocon-parser) | HOCON parser for Rust |
|
|
413
545
|
| [py.hocon](https://github.com/o3co/py.hocon) | Python | [PyPI](https://pypi.org/project/hocon-parser/) | HOCON parser for Python |
|
|
414
546
|
| [hocon2](https://github.com/o3co/hocon2) | Go | [pkg.go.dev](https://pkg.go.dev/github.com/o3co/hocon2) | HOCON → JSON/YAML/TOML/Properties CLI |
|
|
415
547
|
|
|
@@ -438,8 +570,8 @@ Deferring resolution matters: the plain `parse` resolves as it goes, so a
|
|
|
438
570
|
| `@o3co/ts.hocon/adapters/env` | — | Bulk-mounts a prefixed namespace; also reads `.env` |
|
|
439
571
|
| `@o3co/ts.hocon/adapters/jsonc` | — | JSON with comments and trailing commas |
|
|
440
572
|
| `@o3co/ts.hocon/adapters/json5` | — | JSON5 1.0.0, hand-rolled scanner, zero dependencies |
|
|
441
|
-
| `@o3co/ts.hocon/adapters/toml` | `smol-toml` | Optional peer dependency |
|
|
442
|
-
| `@o3co/ts.hocon/adapters/yaml` | `yaml` 2.
|
|
573
|
+
| `@o3co/ts.hocon/adapters/toml` | `smol-toml` (>= 1.0; >= 1.9.0 recommended, see [SECURITY.md](SECURITY.md#the-toml-adapter)) | Optional peer dependency |
|
|
574
|
+
| `@o3co/ts.hocon/adapters/yaml` | `yaml` (>= 2.0) | Optional peer dependency; scalar resolution is that library's answer, with `version: '1.2'` declared so it cannot drift |
|
|
443
575
|
|
|
444
576
|
The TOML and YAML libraries are **optional peer dependencies**, so installing
|
|
445
577
|
this package still pulls in nothing — you add the one you actually use. Plain
|