@o3co/ts.hocon 1.8.0 → 1.9.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.md +5 -4
- package/dist/index.cjs +32 -26
- package/dist/index.js +32 -26
- package/dist/zod.cjs +32 -26
- package/dist/zod.js +32 -26
- package/package.json +19 -18
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A [Lightbend HOCON](https://github.com/lightbend/config/blob/main/HOCON.md) pars
|
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
>
|
|
17
|
-
> **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)
|
|
17
|
+
> **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
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -245,7 +245,7 @@ c.getBytes('max-size') // 536870912 (bytes)
|
|
|
245
245
|
c.getBytes('max-size', 'MiB') // 512
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
Supported duration units: `ns`, `us`, `ms`, `s`, `m`, `h`, `d` (and long forms like `seconds`, `minutes`).
|
|
248
|
+
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). Byte units are more case-tolerant: the canonical forms below plus lowercase aliases (`kb`, `kib`, …), any-case long forms (`megabytes`), and single-letter powers-of-two in both cases (`K`/`k`, per Lightbend).
|
|
249
249
|
Supported byte units: `B`, `KB`/`KiB`, `MB`/`MiB`, `GB`/`GiB`, `TB`/`TiB` (and long forms like `megabytes`, `mebibytes`).
|
|
250
250
|
|
|
251
251
|
## Spec Compliance
|
|
@@ -386,9 +386,10 @@ const config = parseWithSchema(hoconInput, schema) // fails fast on startup
|
|
|
386
386
|
|---------|----------|----------|-------------|
|
|
387
387
|
| [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 |
|
|
388
388
|
| [rs.hocon](https://github.com/o3co/rs.hocon) | Rust | [crates.io](https://crates.io/crates/o3co-hocon) | HOCON parser for Rust |
|
|
389
|
+
| [py.hocon](https://github.com/o3co/py.hocon) | Python | [PyPI](https://pypi.org/project/hocon-parser/) | HOCON parser for Python |
|
|
389
390
|
| [hocon2](https://github.com/o3co/hocon2) | Go | [pkg.go.dev](https://pkg.go.dev/github.com/o3co/hocon2) | HOCON → JSON/YAML/TOML/Properties CLI |
|
|
390
391
|
|
|
391
|
-
The
|
|
392
|
+
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.
|
|
392
393
|
|
|
393
394
|
## Known Limitations
|
|
394
395
|
|
|
@@ -402,7 +403,7 @@ The three parser implementations ([ts.hocon](https://github.com/o3co/ts.hocon),
|
|
|
402
403
|
|
|
403
404
|
When parsing untrusted HOCON input, be aware of:
|
|
404
405
|
|
|
405
|
-
- **Path traversal in includes:** `include
|
|
406
|
+
- **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.
|
|
406
407
|
- **Input size:** The parser has no built-in input size limit. For untrusted input, validate size before calling `parse()`.
|
|
407
408
|
- **Include depth:** Limited to 50 levels to prevent stack overflow from deep include chains.
|
|
408
409
|
|
package/dist/index.cjs
CHANGED
|
@@ -141,7 +141,7 @@ function parseDuration(value, outputUnit = "ms") {
|
|
|
141
141
|
if (i === 0) return NaN;
|
|
142
142
|
const num = Number(trimmed.slice(0, i));
|
|
143
143
|
if (Number.isNaN(num)) return NaN;
|
|
144
|
-
const unit = trimHoconWs(trimmed.slice(i))
|
|
144
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
145
145
|
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
146
146
|
if (divisor === void 0) return NaN;
|
|
147
147
|
if (unit === "") {
|
|
@@ -701,20 +701,6 @@ function isFileNotFoundError(e) {
|
|
|
701
701
|
var import_node_module = require("module");
|
|
702
702
|
var nodePath = __toESM(require("path"), 1);
|
|
703
703
|
|
|
704
|
-
// src/internal/parser/empty-check.ts
|
|
705
|
-
function hasContentTokens(tokens) {
|
|
706
|
-
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
707
|
-
}
|
|
708
|
-
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
709
|
-
if (!hasContentTokens(tokens)) {
|
|
710
|
-
throw new ParseError(
|
|
711
|
-
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
712
|
-
1,
|
|
713
|
-
1
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
704
|
// src/internal/lexer/lexer.ts
|
|
719
705
|
function isHoconWhitespace(ch) {
|
|
720
706
|
const cp = ch.codePointAt(0) ?? -1;
|
|
@@ -1160,6 +1146,16 @@ var Parser = class {
|
|
|
1160
1146
|
}
|
|
1161
1147
|
return { kind: "object", fields: allFields, pos: first.pos };
|
|
1162
1148
|
}
|
|
1149
|
+
if (t.kind === "lbracket") {
|
|
1150
|
+
this.advance();
|
|
1151
|
+
const arr = this.parseArray();
|
|
1152
|
+
this.skip("newline");
|
|
1153
|
+
const remaining = this.peek();
|
|
1154
|
+
if (remaining.kind !== "eof") {
|
|
1155
|
+
throw new ParseError(`Unexpected token '${remaining.value}' after root array`, remaining.line, remaining.col);
|
|
1156
|
+
}
|
|
1157
|
+
return { ...arr, pos: { line: t.line, col: t.col } };
|
|
1158
|
+
}
|
|
1163
1159
|
return this.parseObject(false);
|
|
1164
1160
|
}
|
|
1165
1161
|
peek(offset = 0) {
|
|
@@ -1599,6 +1595,16 @@ var _require = (0, import_node_module.createRequire)(import_meta.url);
|
|
|
1599
1595
|
function isPnpPath(p) {
|
|
1600
1596
|
return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
|
|
1601
1597
|
}
|
|
1598
|
+
function assertObjectRootedInclude(ast, sourcePath) {
|
|
1599
|
+
if (ast.kind === "array") {
|
|
1600
|
+
throw new ResolveError(
|
|
1601
|
+
`included file has array at file root \u2014 an included file must contain an object, not an array (HOCON.md L993-994): ${sourcePath}`,
|
|
1602
|
+
sourcePath,
|
|
1603
|
+
ast.pos.line,
|
|
1604
|
+
ast.pos.col
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1602
1608
|
function validatePackageFile(file, identifier) {
|
|
1603
1609
|
if (file.length === 0) {
|
|
1604
1610
|
throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
|
|
@@ -1778,10 +1784,9 @@ var IncludeLoader = class {
|
|
|
1778
1784
|
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1779
1785
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1780
1786
|
const content = readFileSync2(resolvedPath);
|
|
1781
|
-
if (content.length === 0) return makeResObj();
|
|
1782
1787
|
const tokens = tokenize(content);
|
|
1783
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1784
1788
|
const ast = parseTokens(tokens);
|
|
1789
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1785
1790
|
return this.onBuildResObj(ast, {
|
|
1786
1791
|
...this.opts,
|
|
1787
1792
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1811,10 +1816,9 @@ var IncludeLoader = class {
|
|
|
1811
1816
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1812
1817
|
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1813
1818
|
const content = await read(resolvedPath);
|
|
1814
|
-
if (content.length === 0) return makeResObj();
|
|
1815
1819
|
const tokens = tokenize(content);
|
|
1816
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1817
1820
|
const ast = parseTokens(tokens);
|
|
1821
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1818
1822
|
return this.onBuildResObjAsync(ast, {
|
|
1819
1823
|
...this.opts,
|
|
1820
1824
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1837,10 +1841,8 @@ var IncludeLoader = class {
|
|
|
1837
1841
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1838
1842
|
}
|
|
1839
1843
|
const tokens = tokenize(content);
|
|
1840
|
-
if (!hasContentTokens(tokens)) {
|
|
1841
|
-
return makeResObj();
|
|
1842
|
-
}
|
|
1843
1844
|
const ast = parseTokens(tokens);
|
|
1845
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1844
1846
|
return this.onBuildResObj(ast, {
|
|
1845
1847
|
...this.opts,
|
|
1846
1848
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1864,10 +1866,8 @@ var IncludeLoader = class {
|
|
|
1864
1866
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1865
1867
|
}
|
|
1866
1868
|
const tokens = tokenize(content);
|
|
1867
|
-
if (!hasContentTokens(tokens)) {
|
|
1868
|
-
return makeResObj();
|
|
1869
|
-
}
|
|
1870
1869
|
const ast = parseTokens(tokens);
|
|
1870
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1871
1871
|
return this.onBuildResObjAsync(ast, {
|
|
1872
1872
|
...this.opts,
|
|
1873
1873
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -3059,8 +3059,14 @@ async function defaultReadFile(filePath) {
|
|
|
3059
3059
|
}
|
|
3060
3060
|
function buildResolveContext(input, opts) {
|
|
3061
3061
|
const tokens = tokenize(input);
|
|
3062
|
-
assertNonEmptyDocument(tokens, "input");
|
|
3063
3062
|
const ast = parseTokens(tokens);
|
|
3063
|
+
if (ast.kind === "array") {
|
|
3064
|
+
const origin = opts.originDescription ?? "input";
|
|
3065
|
+
throw new ConfigError(
|
|
3066
|
+
`${origin}: ${ast.pos.line}:${ast.pos.col}: document has type array rather than object at file root (HOCON.md L989-991); the Config API requires an object root`,
|
|
3067
|
+
""
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3064
3070
|
const resolveOpts = {
|
|
3065
3071
|
env: getEnv(opts),
|
|
3066
3072
|
baseDir: opts.baseDir,
|
package/dist/index.js
CHANGED
|
@@ -80,7 +80,7 @@ function parseDuration(value, outputUnit = "ms") {
|
|
|
80
80
|
if (i === 0) return NaN;
|
|
81
81
|
const num = Number(trimmed.slice(0, i));
|
|
82
82
|
if (Number.isNaN(num)) return NaN;
|
|
83
|
-
const unit = trimHoconWs(trimmed.slice(i))
|
|
83
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
84
84
|
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
85
85
|
if (divisor === void 0) return NaN;
|
|
86
86
|
if (unit === "") {
|
|
@@ -640,20 +640,6 @@ function isFileNotFoundError(e) {
|
|
|
640
640
|
import { createRequire } from "module";
|
|
641
641
|
import * as nodePath from "path";
|
|
642
642
|
|
|
643
|
-
// src/internal/parser/empty-check.ts
|
|
644
|
-
function hasContentTokens(tokens) {
|
|
645
|
-
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
646
|
-
}
|
|
647
|
-
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
648
|
-
if (!hasContentTokens(tokens)) {
|
|
649
|
-
throw new ParseError(
|
|
650
|
-
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
651
|
-
1,
|
|
652
|
-
1
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
643
|
// src/internal/lexer/lexer.ts
|
|
658
644
|
function isHoconWhitespace(ch) {
|
|
659
645
|
const cp = ch.codePointAt(0) ?? -1;
|
|
@@ -1099,6 +1085,16 @@ var Parser = class {
|
|
|
1099
1085
|
}
|
|
1100
1086
|
return { kind: "object", fields: allFields, pos: first.pos };
|
|
1101
1087
|
}
|
|
1088
|
+
if (t.kind === "lbracket") {
|
|
1089
|
+
this.advance();
|
|
1090
|
+
const arr = this.parseArray();
|
|
1091
|
+
this.skip("newline");
|
|
1092
|
+
const remaining = this.peek();
|
|
1093
|
+
if (remaining.kind !== "eof") {
|
|
1094
|
+
throw new ParseError(`Unexpected token '${remaining.value}' after root array`, remaining.line, remaining.col);
|
|
1095
|
+
}
|
|
1096
|
+
return { ...arr, pos: { line: t.line, col: t.col } };
|
|
1097
|
+
}
|
|
1102
1098
|
return this.parseObject(false);
|
|
1103
1099
|
}
|
|
1104
1100
|
peek(offset = 0) {
|
|
@@ -1537,6 +1533,16 @@ var _require = createRequire(import.meta.url);
|
|
|
1537
1533
|
function isPnpPath(p) {
|
|
1538
1534
|
return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
|
|
1539
1535
|
}
|
|
1536
|
+
function assertObjectRootedInclude(ast, sourcePath) {
|
|
1537
|
+
if (ast.kind === "array") {
|
|
1538
|
+
throw new ResolveError(
|
|
1539
|
+
`included file has array at file root \u2014 an included file must contain an object, not an array (HOCON.md L993-994): ${sourcePath}`,
|
|
1540
|
+
sourcePath,
|
|
1541
|
+
ast.pos.line,
|
|
1542
|
+
ast.pos.col
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1540
1546
|
function validatePackageFile(file, identifier) {
|
|
1541
1547
|
if (file.length === 0) {
|
|
1542
1548
|
throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
|
|
@@ -1716,10 +1722,9 @@ var IncludeLoader = class {
|
|
|
1716
1722
|
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1717
1723
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1718
1724
|
const content = readFileSync2(resolvedPath);
|
|
1719
|
-
if (content.length === 0) return makeResObj();
|
|
1720
1725
|
const tokens = tokenize(content);
|
|
1721
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1722
1726
|
const ast = parseTokens(tokens);
|
|
1727
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1723
1728
|
return this.onBuildResObj(ast, {
|
|
1724
1729
|
...this.opts,
|
|
1725
1730
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1749,10 +1754,9 @@ var IncludeLoader = class {
|
|
|
1749
1754
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1750
1755
|
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1751
1756
|
const content = await read(resolvedPath);
|
|
1752
|
-
if (content.length === 0) return makeResObj();
|
|
1753
1757
|
const tokens = tokenize(content);
|
|
1754
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1755
1758
|
const ast = parseTokens(tokens);
|
|
1759
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1756
1760
|
return this.onBuildResObjAsync(ast, {
|
|
1757
1761
|
...this.opts,
|
|
1758
1762
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1775,10 +1779,8 @@ var IncludeLoader = class {
|
|
|
1775
1779
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1776
1780
|
}
|
|
1777
1781
|
const tokens = tokenize(content);
|
|
1778
|
-
if (!hasContentTokens(tokens)) {
|
|
1779
|
-
return makeResObj();
|
|
1780
|
-
}
|
|
1781
1782
|
const ast = parseTokens(tokens);
|
|
1783
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1782
1784
|
return this.onBuildResObj(ast, {
|
|
1783
1785
|
...this.opts,
|
|
1784
1786
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1802,10 +1804,8 @@ var IncludeLoader = class {
|
|
|
1802
1804
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1803
1805
|
}
|
|
1804
1806
|
const tokens = tokenize(content);
|
|
1805
|
-
if (!hasContentTokens(tokens)) {
|
|
1806
|
-
return makeResObj();
|
|
1807
|
-
}
|
|
1808
1807
|
const ast = parseTokens(tokens);
|
|
1808
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1809
1809
|
return this.onBuildResObjAsync(ast, {
|
|
1810
1810
|
...this.opts,
|
|
1811
1811
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -2997,8 +2997,14 @@ async function defaultReadFile(filePath) {
|
|
|
2997
2997
|
}
|
|
2998
2998
|
function buildResolveContext(input, opts) {
|
|
2999
2999
|
const tokens = tokenize(input);
|
|
3000
|
-
assertNonEmptyDocument(tokens, "input");
|
|
3001
3000
|
const ast = parseTokens(tokens);
|
|
3001
|
+
if (ast.kind === "array") {
|
|
3002
|
+
const origin = opts.originDescription ?? "input";
|
|
3003
|
+
throw new ConfigError(
|
|
3004
|
+
`${origin}: ${ast.pos.line}:${ast.pos.col}: document has type array rather than object at file root (HOCON.md L989-991); the Config API requires an object root`,
|
|
3005
|
+
""
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3002
3008
|
const resolveOpts = {
|
|
3003
3009
|
env: getEnv(opts),
|
|
3004
3010
|
baseDir: opts.baseDir,
|
package/dist/zod.cjs
CHANGED
|
@@ -119,7 +119,7 @@ function parseDuration(value, outputUnit = "ms") {
|
|
|
119
119
|
if (i === 0) return NaN;
|
|
120
120
|
const num = Number(trimmed.slice(0, i));
|
|
121
121
|
if (Number.isNaN(num)) return NaN;
|
|
122
|
-
const unit = trimHoconWs(trimmed.slice(i))
|
|
122
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
123
123
|
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
124
124
|
if (divisor === void 0) return NaN;
|
|
125
125
|
if (unit === "") {
|
|
@@ -683,20 +683,6 @@ function isFileNotFoundError(e) {
|
|
|
683
683
|
var import_node_module = require("module");
|
|
684
684
|
var nodePath = __toESM(require("path"), 1);
|
|
685
685
|
|
|
686
|
-
// src/internal/parser/empty-check.ts
|
|
687
|
-
function hasContentTokens(tokens) {
|
|
688
|
-
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
689
|
-
}
|
|
690
|
-
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
691
|
-
if (!hasContentTokens(tokens)) {
|
|
692
|
-
throw new ParseError(
|
|
693
|
-
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
694
|
-
1,
|
|
695
|
-
1
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
686
|
// src/internal/lexer/lexer.ts
|
|
701
687
|
function isHoconWhitespace(ch) {
|
|
702
688
|
const cp = ch.codePointAt(0) ?? -1;
|
|
@@ -1142,6 +1128,16 @@ var Parser = class {
|
|
|
1142
1128
|
}
|
|
1143
1129
|
return { kind: "object", fields: allFields, pos: first.pos };
|
|
1144
1130
|
}
|
|
1131
|
+
if (t.kind === "lbracket") {
|
|
1132
|
+
this.advance();
|
|
1133
|
+
const arr = this.parseArray();
|
|
1134
|
+
this.skip("newline");
|
|
1135
|
+
const remaining = this.peek();
|
|
1136
|
+
if (remaining.kind !== "eof") {
|
|
1137
|
+
throw new ParseError(`Unexpected token '${remaining.value}' after root array`, remaining.line, remaining.col);
|
|
1138
|
+
}
|
|
1139
|
+
return { ...arr, pos: { line: t.line, col: t.col } };
|
|
1140
|
+
}
|
|
1145
1141
|
return this.parseObject(false);
|
|
1146
1142
|
}
|
|
1147
1143
|
peek(offset = 0) {
|
|
@@ -1581,6 +1577,16 @@ var _require = (0, import_node_module.createRequire)(import_meta.url);
|
|
|
1581
1577
|
function isPnpPath(p) {
|
|
1582
1578
|
return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
|
|
1583
1579
|
}
|
|
1580
|
+
function assertObjectRootedInclude(ast, sourcePath) {
|
|
1581
|
+
if (ast.kind === "array") {
|
|
1582
|
+
throw new ResolveError(
|
|
1583
|
+
`included file has array at file root \u2014 an included file must contain an object, not an array (HOCON.md L993-994): ${sourcePath}`,
|
|
1584
|
+
sourcePath,
|
|
1585
|
+
ast.pos.line,
|
|
1586
|
+
ast.pos.col
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1584
1590
|
function validatePackageFile(file, identifier) {
|
|
1585
1591
|
if (file.length === 0) {
|
|
1586
1592
|
throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
|
|
@@ -1760,10 +1766,9 @@ var IncludeLoader = class {
|
|
|
1760
1766
|
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1761
1767
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1762
1768
|
const content = readFileSync2(resolvedPath);
|
|
1763
|
-
if (content.length === 0) return makeResObj();
|
|
1764
1769
|
const tokens = tokenize(content);
|
|
1765
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1766
1770
|
const ast = parseTokens(tokens);
|
|
1771
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1767
1772
|
return this.onBuildResObj(ast, {
|
|
1768
1773
|
...this.opts,
|
|
1769
1774
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1793,10 +1798,9 @@ var IncludeLoader = class {
|
|
|
1793
1798
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1794
1799
|
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1795
1800
|
const content = await read(resolvedPath);
|
|
1796
|
-
if (content.length === 0) return makeResObj();
|
|
1797
1801
|
const tokens = tokenize(content);
|
|
1798
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1799
1802
|
const ast = parseTokens(tokens);
|
|
1803
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1800
1804
|
return this.onBuildResObjAsync(ast, {
|
|
1801
1805
|
...this.opts,
|
|
1802
1806
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1819,10 +1823,8 @@ var IncludeLoader = class {
|
|
|
1819
1823
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1820
1824
|
}
|
|
1821
1825
|
const tokens = tokenize(content);
|
|
1822
|
-
if (!hasContentTokens(tokens)) {
|
|
1823
|
-
return makeResObj();
|
|
1824
|
-
}
|
|
1825
1826
|
const ast = parseTokens(tokens);
|
|
1827
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1826
1828
|
return this.onBuildResObj(ast, {
|
|
1827
1829
|
...this.opts,
|
|
1828
1830
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1846,10 +1848,8 @@ var IncludeLoader = class {
|
|
|
1846
1848
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1847
1849
|
}
|
|
1848
1850
|
const tokens = tokenize(content);
|
|
1849
|
-
if (!hasContentTokens(tokens)) {
|
|
1850
|
-
return makeResObj();
|
|
1851
|
-
}
|
|
1852
1851
|
const ast = parseTokens(tokens);
|
|
1852
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1853
1853
|
return this.onBuildResObjAsync(ast, {
|
|
1854
1854
|
...this.opts,
|
|
1855
1855
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -3026,8 +3026,14 @@ function defaultReadFileSync(filePath) {
|
|
|
3026
3026
|
}
|
|
3027
3027
|
function buildResolveContext(input, opts) {
|
|
3028
3028
|
const tokens = tokenize(input);
|
|
3029
|
-
assertNonEmptyDocument(tokens, "input");
|
|
3030
3029
|
const ast = parseTokens(tokens);
|
|
3030
|
+
if (ast.kind === "array") {
|
|
3031
|
+
const origin = opts.originDescription ?? "input";
|
|
3032
|
+
throw new ConfigError(
|
|
3033
|
+
`${origin}: ${ast.pos.line}:${ast.pos.col}: document has type array rather than object at file root (HOCON.md L989-991); the Config API requires an object root`,
|
|
3034
|
+
""
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3031
3037
|
const resolveOpts = {
|
|
3032
3038
|
env: getEnv(opts),
|
|
3033
3039
|
baseDir: opts.baseDir,
|
package/dist/zod.js
CHANGED
|
@@ -80,7 +80,7 @@ function parseDuration(value, outputUnit = "ms") {
|
|
|
80
80
|
if (i === 0) return NaN;
|
|
81
81
|
const num = Number(trimmed.slice(0, i));
|
|
82
82
|
if (Number.isNaN(num)) return NaN;
|
|
83
|
-
const unit = trimHoconWs(trimmed.slice(i))
|
|
83
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
84
84
|
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
85
85
|
if (divisor === void 0) return NaN;
|
|
86
86
|
if (unit === "") {
|
|
@@ -644,20 +644,6 @@ function isFileNotFoundError(e) {
|
|
|
644
644
|
import { createRequire } from "module";
|
|
645
645
|
import * as nodePath from "path";
|
|
646
646
|
|
|
647
|
-
// src/internal/parser/empty-check.ts
|
|
648
|
-
function hasContentTokens(tokens) {
|
|
649
|
-
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
650
|
-
}
|
|
651
|
-
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
652
|
-
if (!hasContentTokens(tokens)) {
|
|
653
|
-
throw new ParseError(
|
|
654
|
-
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
655
|
-
1,
|
|
656
|
-
1
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
647
|
// src/internal/lexer/lexer.ts
|
|
662
648
|
function isHoconWhitespace(ch) {
|
|
663
649
|
const cp = ch.codePointAt(0) ?? -1;
|
|
@@ -1103,6 +1089,16 @@ var Parser = class {
|
|
|
1103
1089
|
}
|
|
1104
1090
|
return { kind: "object", fields: allFields, pos: first.pos };
|
|
1105
1091
|
}
|
|
1092
|
+
if (t.kind === "lbracket") {
|
|
1093
|
+
this.advance();
|
|
1094
|
+
const arr = this.parseArray();
|
|
1095
|
+
this.skip("newline");
|
|
1096
|
+
const remaining = this.peek();
|
|
1097
|
+
if (remaining.kind !== "eof") {
|
|
1098
|
+
throw new ParseError(`Unexpected token '${remaining.value}' after root array`, remaining.line, remaining.col);
|
|
1099
|
+
}
|
|
1100
|
+
return { ...arr, pos: { line: t.line, col: t.col } };
|
|
1101
|
+
}
|
|
1106
1102
|
return this.parseObject(false);
|
|
1107
1103
|
}
|
|
1108
1104
|
peek(offset = 0) {
|
|
@@ -1541,6 +1537,16 @@ var _require = createRequire(import.meta.url);
|
|
|
1541
1537
|
function isPnpPath(p) {
|
|
1542
1538
|
return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
|
|
1543
1539
|
}
|
|
1540
|
+
function assertObjectRootedInclude(ast, sourcePath) {
|
|
1541
|
+
if (ast.kind === "array") {
|
|
1542
|
+
throw new ResolveError(
|
|
1543
|
+
`included file has array at file root \u2014 an included file must contain an object, not an array (HOCON.md L993-994): ${sourcePath}`,
|
|
1544
|
+
sourcePath,
|
|
1545
|
+
ast.pos.line,
|
|
1546
|
+
ast.pos.col
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1544
1550
|
function validatePackageFile(file, identifier) {
|
|
1545
1551
|
if (file.length === 0) {
|
|
1546
1552
|
throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
|
|
@@ -1720,10 +1726,9 @@ var IncludeLoader = class {
|
|
|
1720
1726
|
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1721
1727
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1722
1728
|
const content = readFileSync2(resolvedPath);
|
|
1723
|
-
if (content.length === 0) return makeResObj();
|
|
1724
1729
|
const tokens = tokenize(content);
|
|
1725
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1726
1730
|
const ast = parseTokens(tokens);
|
|
1731
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1727
1732
|
return this.onBuildResObj(ast, {
|
|
1728
1733
|
...this.opts,
|
|
1729
1734
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1753,10 +1758,9 @@ var IncludeLoader = class {
|
|
|
1753
1758
|
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1754
1759
|
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1755
1760
|
const content = await read(resolvedPath);
|
|
1756
|
-
if (content.length === 0) return makeResObj();
|
|
1757
1761
|
const tokens = tokenize(content);
|
|
1758
|
-
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1759
1762
|
const ast = parseTokens(tokens);
|
|
1763
|
+
assertObjectRootedInclude(ast, resolvedPath);
|
|
1760
1764
|
return this.onBuildResObjAsync(ast, {
|
|
1761
1765
|
...this.opts,
|
|
1762
1766
|
baseDir: nodePath.dirname(resolvedPath),
|
|
@@ -1779,10 +1783,8 @@ var IncludeLoader = class {
|
|
|
1779
1783
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1780
1784
|
}
|
|
1781
1785
|
const tokens = tokenize(content);
|
|
1782
|
-
if (!hasContentTokens(tokens)) {
|
|
1783
|
-
return makeResObj();
|
|
1784
|
-
}
|
|
1785
1786
|
const ast = parseTokens(tokens);
|
|
1787
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1786
1788
|
return this.onBuildResObj(ast, {
|
|
1787
1789
|
...this.opts,
|
|
1788
1790
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -1806,10 +1808,8 @@ var IncludeLoader = class {
|
|
|
1806
1808
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1807
1809
|
}
|
|
1808
1810
|
const tokens = tokenize(content);
|
|
1809
|
-
if (!hasContentTokens(tokens)) {
|
|
1810
|
-
return makeResObj();
|
|
1811
|
-
}
|
|
1812
1811
|
const ast = parseTokens(tokens);
|
|
1812
|
+
assertObjectRootedInclude(ast, candidate);
|
|
1813
1813
|
return this.onBuildResObjAsync(ast, {
|
|
1814
1814
|
...this.opts,
|
|
1815
1815
|
baseDir: nodePath.dirname(candidate),
|
|
@@ -2986,8 +2986,14 @@ function defaultReadFileSync(filePath) {
|
|
|
2986
2986
|
}
|
|
2987
2987
|
function buildResolveContext(input, opts) {
|
|
2988
2988
|
const tokens = tokenize(input);
|
|
2989
|
-
assertNonEmptyDocument(tokens, "input");
|
|
2990
2989
|
const ast = parseTokens(tokens);
|
|
2990
|
+
if (ast.kind === "array") {
|
|
2991
|
+
const origin = opts.originDescription ?? "input";
|
|
2992
|
+
throw new ConfigError(
|
|
2993
|
+
`${origin}: ${ast.pos.line}:${ast.pos.col}: document has type array rather than object at file root (HOCON.md L989-991); the Config API requires an object root`,
|
|
2994
|
+
""
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2991
2997
|
const resolveOpts = {
|
|
2992
2998
|
env: getEnv(opts),
|
|
2993
2999
|
baseDir: opts.baseDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/ts.hocon",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Full Lightbend HOCON spec-compliant parser and config library for TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hocon",
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"coverage": "vitest run --coverage",
|
|
38
|
+
"bench": "vitest bench",
|
|
39
|
+
"docs": "typedoc"
|
|
40
|
+
},
|
|
32
41
|
"peerDependencies": {
|
|
33
42
|
"zod": ">=4.0.0"
|
|
34
43
|
},
|
|
@@ -39,18 +48,18 @@
|
|
|
39
48
|
},
|
|
40
49
|
"devDependencies": {
|
|
41
50
|
"@types/js-yaml": "^4.0.9",
|
|
42
|
-
"@types/node": "^25.5
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
44
|
-
"js-yaml": "^4.
|
|
51
|
+
"@types/node": "^25.9.5",
|
|
52
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
53
|
+
"js-yaml": "^4.3.0",
|
|
45
54
|
"tsup": "^8.5.1",
|
|
46
|
-
"typedoc": "^0.28.
|
|
55
|
+
"typedoc": "^0.28.20",
|
|
47
56
|
"typescript": "^5.9.3",
|
|
48
|
-
"vitest": "4.1.
|
|
49
|
-
"zod": "^4.3
|
|
57
|
+
"vitest": "4.1.10",
|
|
58
|
+
"zod": "^4.4.3"
|
|
50
59
|
},
|
|
51
60
|
"repository": {
|
|
52
61
|
"type": "git",
|
|
53
|
-
"url": "https://github.com/o3co/ts.hocon"
|
|
62
|
+
"url": "git+https://github.com/o3co/ts.hocon.git"
|
|
54
63
|
},
|
|
55
64
|
"license": "Apache-2.0",
|
|
56
65
|
"engines": {
|
|
@@ -58,13 +67,5 @@
|
|
|
58
67
|
},
|
|
59
68
|
"homepage": "https://o3co.github.io/ts.hocon/",
|
|
60
69
|
"bugs": "https://github.com/o3co/ts.hocon/issues",
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
"test": "vitest run",
|
|
64
|
-
"test:watch": "vitest",
|
|
65
|
-
"typecheck": "tsc --noEmit",
|
|
66
|
-
"coverage": "vitest run --coverage",
|
|
67
|
-
"bench": "vitest bench",
|
|
68
|
-
"docs": "typedoc"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
70
|
+
"packageManager": "pnpm@10.30.2"
|
|
71
|
+
}
|