@o3co/ts.hocon 1.7.1 → 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 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) and [rs.hocon](https://github.com/o3co/rs.hocon) to ensure all three implementations meet the same Lightbend HOCON specification.
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 three 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)) 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
+ 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 "../../../etc/passwd"` will resolve relative to `baseDir`. Use a custom `readFileSync`/`readFile` that validates paths if parsing untrusted input.
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
 
@@ -13,6 +13,59 @@ type HoconValue = {
13
13
  raw: string;
14
14
  valueType: ScalarValueType;
15
15
  };
16
+ /**
17
+ * Deeply-immutable view of a {@link HoconValue}. `Config.getValue` returns this
18
+ * so callers cannot mutate the parsed configuration tree (the live nodes are
19
+ * shared, not cloned). `Map`/array containers become `ReadonlyMap`/`readonly[]`
20
+ * recursively, so `.set` / `.push` / element assignment are compile errors.
21
+ *
22
+ * `HoconValue` is structurally assignable to `ReadonlyHoconValue`, so internal
23
+ * code holding a mutable node can return it here, and the accessor functions
24
+ * below accept both forms.
25
+ */
26
+ type ReadonlyHoconValue = {
27
+ readonly kind: 'object';
28
+ readonly fields: ReadonlyMap<string, ReadonlyHoconValue>;
29
+ } | {
30
+ readonly kind: 'array';
31
+ readonly items: readonly ReadonlyHoconValue[];
32
+ } | {
33
+ readonly kind: 'scalar';
34
+ readonly raw: string;
35
+ readonly valueType: ScalarValueType;
36
+ };
37
+ /** Scalar string only (strict): non-string scalars and containers → undefined. */
38
+ declare function asString(v: ReadonlyHoconValue): string | undefined;
39
+ /**
40
+ * Scalar coerced to a number via {@link coerceNumber} (lenient, matching the
41
+ * `getNumber` getter: a numeric-looking string scalar coerces too). TS has a
42
+ * single `number` type, so this unifies rs's `as_i64` + `as_f64`.
43
+ */
44
+ declare function asNumber(v: ReadonlyHoconValue): number | undefined;
45
+ /** Scalar coerced to a boolean via {@link coerceBoolean} (true/yes/on, false/no/off). */
46
+ declare function asBoolean(v: ReadonlyHoconValue): boolean | undefined;
47
+ /** The object's fields, or undefined if not an object. */
48
+ declare function asObject(v: ReadonlyHoconValue): ReadonlyMap<string, ReadonlyHoconValue> | undefined;
49
+ /** The array's items, or undefined if not an array. */
50
+ declare function asArray(v: ReadonlyHoconValue): readonly ReadonlyHoconValue[] | undefined;
51
+ /** Type guard: narrows to the object variant. */
52
+ declare function isObject(v: ReadonlyHoconValue): v is {
53
+ readonly kind: 'object';
54
+ readonly fields: ReadonlyMap<string, ReadonlyHoconValue>;
55
+ };
56
+ /** Type guard: narrows to the array variant. */
57
+ declare function isArray(v: ReadonlyHoconValue): v is {
58
+ readonly kind: 'array';
59
+ readonly items: readonly ReadonlyHoconValue[];
60
+ };
61
+ /** Type guard: narrows to the scalar variant. */
62
+ declare function isScalar(v: ReadonlyHoconValue): v is {
63
+ readonly kind: 'scalar';
64
+ readonly raw: string;
65
+ readonly valueType: ScalarValueType;
66
+ };
67
+ /** True for a null scalar. */
68
+ declare function isNull(v: ReadonlyHoconValue): boolean;
16
69
 
17
70
  interface Segment {
18
71
  readonly text: string;
@@ -212,6 +265,20 @@ declare class Config {
212
265
  resolveOpts: ResolveOptions$1;
213
266
  }): Config;
214
267
  get(path: string): unknown;
268
+ /**
269
+ * Returns the raw {@link HoconValue} node at `path` as a deeply-immutable
270
+ * {@link ReadonlyHoconValue} view, or `undefined` if the path is absent. Unlike
271
+ * `get` (which decodes to a plain JS value), this exposes the value tree for
272
+ * structural introspection via the `value.ts` accessors (`asString`, `asObject`,
273
+ * `isScalar`, …).
274
+ *
275
+ * `getValue('')` returns the root object node.
276
+ *
277
+ * Throws {@link NotResolvedError} when the node (or its subtree) is unresolved:
278
+ * the `HoconValue` union has no placeholder variant, so an unresolved value
279
+ * cannot be represented. This matches `getConfig` / `getList`.
280
+ */
281
+ getValue(path: string): ReadonlyHoconValue | undefined;
215
282
  getString(path: string): string;
216
283
  getNumber(path: string): number;
217
284
  getBoolean(path: string): boolean;
@@ -273,4 +340,4 @@ declare class Config {
273
340
  private requireScalar;
274
341
  }
275
342
 
276
- export { type ByteUnit as B, Config as C, type DurationUnit as D, type HoconValue as H, type PackageResolver as P, type ResolveOptions as R, type ScalarValueType as S, type ParseOptions as a, defaultResolveOptions as b, parseAsync as c, defaultParseOptions as d, parseFile as e, parseFileAsync as f, parseFileWithOptions as g, parseString as h, parseStringWithOptions as i, parse as p };
343
+ export { type ByteUnit as B, Config as C, type DurationUnit as D, type HoconValue as H, type PackageResolver as P, type ReadonlyHoconValue as R, type ScalarValueType as S, type ParseOptions as a, type ResolveOptions as b, asArray as c, asBoolean as d, asNumber as e, asObject as f, asString as g, defaultParseOptions as h, defaultResolveOptions as i, isArray as j, isNull as k, isObject as l, isScalar as m, parseAsync as n, parseFile as o, parse as p, parseFileAsync as q, parseFileWithOptions as r, parseString as s, parseStringWithOptions as t };
@@ -13,6 +13,59 @@ type HoconValue = {
13
13
  raw: string;
14
14
  valueType: ScalarValueType;
15
15
  };
16
+ /**
17
+ * Deeply-immutable view of a {@link HoconValue}. `Config.getValue` returns this
18
+ * so callers cannot mutate the parsed configuration tree (the live nodes are
19
+ * shared, not cloned). `Map`/array containers become `ReadonlyMap`/`readonly[]`
20
+ * recursively, so `.set` / `.push` / element assignment are compile errors.
21
+ *
22
+ * `HoconValue` is structurally assignable to `ReadonlyHoconValue`, so internal
23
+ * code holding a mutable node can return it here, and the accessor functions
24
+ * below accept both forms.
25
+ */
26
+ type ReadonlyHoconValue = {
27
+ readonly kind: 'object';
28
+ readonly fields: ReadonlyMap<string, ReadonlyHoconValue>;
29
+ } | {
30
+ readonly kind: 'array';
31
+ readonly items: readonly ReadonlyHoconValue[];
32
+ } | {
33
+ readonly kind: 'scalar';
34
+ readonly raw: string;
35
+ readonly valueType: ScalarValueType;
36
+ };
37
+ /** Scalar string only (strict): non-string scalars and containers → undefined. */
38
+ declare function asString(v: ReadonlyHoconValue): string | undefined;
39
+ /**
40
+ * Scalar coerced to a number via {@link coerceNumber} (lenient, matching the
41
+ * `getNumber` getter: a numeric-looking string scalar coerces too). TS has a
42
+ * single `number` type, so this unifies rs's `as_i64` + `as_f64`.
43
+ */
44
+ declare function asNumber(v: ReadonlyHoconValue): number | undefined;
45
+ /** Scalar coerced to a boolean via {@link coerceBoolean} (true/yes/on, false/no/off). */
46
+ declare function asBoolean(v: ReadonlyHoconValue): boolean | undefined;
47
+ /** The object's fields, or undefined if not an object. */
48
+ declare function asObject(v: ReadonlyHoconValue): ReadonlyMap<string, ReadonlyHoconValue> | undefined;
49
+ /** The array's items, or undefined if not an array. */
50
+ declare function asArray(v: ReadonlyHoconValue): readonly ReadonlyHoconValue[] | undefined;
51
+ /** Type guard: narrows to the object variant. */
52
+ declare function isObject(v: ReadonlyHoconValue): v is {
53
+ readonly kind: 'object';
54
+ readonly fields: ReadonlyMap<string, ReadonlyHoconValue>;
55
+ };
56
+ /** Type guard: narrows to the array variant. */
57
+ declare function isArray(v: ReadonlyHoconValue): v is {
58
+ readonly kind: 'array';
59
+ readonly items: readonly ReadonlyHoconValue[];
60
+ };
61
+ /** Type guard: narrows to the scalar variant. */
62
+ declare function isScalar(v: ReadonlyHoconValue): v is {
63
+ readonly kind: 'scalar';
64
+ readonly raw: string;
65
+ readonly valueType: ScalarValueType;
66
+ };
67
+ /** True for a null scalar. */
68
+ declare function isNull(v: ReadonlyHoconValue): boolean;
16
69
 
17
70
  interface Segment {
18
71
  readonly text: string;
@@ -212,6 +265,20 @@ declare class Config {
212
265
  resolveOpts: ResolveOptions$1;
213
266
  }): Config;
214
267
  get(path: string): unknown;
268
+ /**
269
+ * Returns the raw {@link HoconValue} node at `path` as a deeply-immutable
270
+ * {@link ReadonlyHoconValue} view, or `undefined` if the path is absent. Unlike
271
+ * `get` (which decodes to a plain JS value), this exposes the value tree for
272
+ * structural introspection via the `value.ts` accessors (`asString`, `asObject`,
273
+ * `isScalar`, …).
274
+ *
275
+ * `getValue('')` returns the root object node.
276
+ *
277
+ * Throws {@link NotResolvedError} when the node (or its subtree) is unresolved:
278
+ * the `HoconValue` union has no placeholder variant, so an unresolved value
279
+ * cannot be represented. This matches `getConfig` / `getList`.
280
+ */
281
+ getValue(path: string): ReadonlyHoconValue | undefined;
215
282
  getString(path: string): string;
216
283
  getNumber(path: string): number;
217
284
  getBoolean(path: string): boolean;
@@ -273,4 +340,4 @@ declare class Config {
273
340
  private requireScalar;
274
341
  }
275
342
 
276
- export { type ByteUnit as B, Config as C, type DurationUnit as D, type HoconValue as H, type PackageResolver as P, type ResolveOptions as R, type ScalarValueType as S, type ParseOptions as a, defaultResolveOptions as b, parseAsync as c, defaultParseOptions as d, parseFile as e, parseFileAsync as f, parseFileWithOptions as g, parseString as h, parseStringWithOptions as i, parse as p };
343
+ export { type ByteUnit as B, Config as C, type DurationUnit as D, type HoconValue as H, type PackageResolver as P, type ReadonlyHoconValue as R, type ScalarValueType as S, type ParseOptions as a, type ResolveOptions as b, asArray as c, asBoolean as d, asNumber as e, asObject as f, asString as g, defaultParseOptions as h, defaultResolveOptions as i, isArray as j, isNull as k, isObject as l, isScalar as m, parseAsync as n, parseFile as o, parse as p, parseFileAsync as q, parseFileWithOptions as r, parseString as s, parseStringWithOptions as t };
package/dist/index.cjs CHANGED
@@ -36,10 +36,19 @@ __export(src_exports, {
36
36
  PackageLookupError: () => PackageLookupError,
37
37
  ParseError: () => ParseError,
38
38
  ResolveError: () => ResolveError,
39
+ asArray: () => asArray,
40
+ asBoolean: () => asBoolean,
41
+ asNumber: () => asNumber,
42
+ asObject: () => asObject,
43
+ asString: () => asString,
39
44
  defaultParseOptions: () => defaultParseOptions,
40
45
  defaultResolveOptions: () => defaultResolveOptions,
41
46
  empty: () => empty,
42
47
  fromMap: () => fromMap,
48
+ isArray: () => isArray,
49
+ isNull: () => isNull,
50
+ isObject: () => isObject,
51
+ isScalar: () => isScalar,
43
52
  parse: () => parse,
44
53
  parseAsync: () => parseAsync,
45
54
  parseFile: () => parseFile,
@@ -132,7 +141,7 @@ function parseDuration(value, outputUnit = "ms") {
132
141
  if (i === 0) return NaN;
133
142
  const num = Number(trimmed.slice(0, i));
134
143
  if (Number.isNaN(num)) return NaN;
135
- const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
144
+ const unit = trimHoconWs(trimmed.slice(i));
136
145
  const divisor = OUTPUT_DURATION_UNITS[outputUnit];
137
146
  if (divisor === void 0) return NaN;
138
147
  if (unit === "") {
@@ -692,20 +701,6 @@ function isFileNotFoundError(e) {
692
701
  var import_node_module = require("module");
693
702
  var nodePath = __toESM(require("path"), 1);
694
703
 
695
- // src/internal/parser/empty-check.ts
696
- function hasContentTokens(tokens) {
697
- return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
698
- }
699
- function assertNonEmptyDocument(tokens, sourceDescriptor) {
700
- if (!hasContentTokens(tokens)) {
701
- throw new ParseError(
702
- `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
703
- 1,
704
- 1
705
- );
706
- }
707
- }
708
-
709
704
  // src/internal/lexer/lexer.ts
710
705
  function isHoconWhitespace(ch) {
711
706
  const cp = ch.codePointAt(0) ?? -1;
@@ -1151,6 +1146,16 @@ var Parser = class {
1151
1146
  }
1152
1147
  return { kind: "object", fields: allFields, pos: first.pos };
1153
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
+ }
1154
1159
  return this.parseObject(false);
1155
1160
  }
1156
1161
  peek(offset = 0) {
@@ -1590,6 +1595,16 @@ var _require = (0, import_node_module.createRequire)(import_meta.url);
1590
1595
  function isPnpPath(p) {
1591
1596
  return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1592
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
+ }
1593
1608
  function validatePackageFile(file, identifier) {
1594
1609
  if (file.length === 0) {
1595
1610
  throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
@@ -1769,10 +1784,9 @@ var IncludeLoader = class {
1769
1784
  const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1770
1785
  const resolvedPath = resolver(identifier, file, void 0, baseDir);
1771
1786
  const content = readFileSync2(resolvedPath);
1772
- if (content.length === 0) return makeResObj();
1773
1787
  const tokens = tokenize(content);
1774
- assertNonEmptyDocument(tokens, resolvedPath);
1775
1788
  const ast = parseTokens(tokens);
1789
+ assertObjectRootedInclude(ast, resolvedPath);
1776
1790
  return this.onBuildResObj(ast, {
1777
1791
  ...this.opts,
1778
1792
  baseDir: nodePath.dirname(resolvedPath),
@@ -1802,10 +1816,9 @@ var IncludeLoader = class {
1802
1816
  const resolvedPath = resolver(identifier, file, void 0, baseDir);
1803
1817
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1804
1818
  const content = await read(resolvedPath);
1805
- if (content.length === 0) return makeResObj();
1806
1819
  const tokens = tokenize(content);
1807
- assertNonEmptyDocument(tokens, resolvedPath);
1808
1820
  const ast = parseTokens(tokens);
1821
+ assertObjectRootedInclude(ast, resolvedPath);
1809
1822
  return this.onBuildResObjAsync(ast, {
1810
1823
  ...this.opts,
1811
1824
  baseDir: nodePath.dirname(resolvedPath),
@@ -1828,10 +1841,8 @@ var IncludeLoader = class {
1828
1841
  return hoconValueToResObj(propertiesToHoconValue(content));
1829
1842
  }
1830
1843
  const tokens = tokenize(content);
1831
- if (!hasContentTokens(tokens)) {
1832
- return makeResObj();
1833
- }
1834
1844
  const ast = parseTokens(tokens);
1845
+ assertObjectRootedInclude(ast, candidate);
1835
1846
  return this.onBuildResObj(ast, {
1836
1847
  ...this.opts,
1837
1848
  baseDir: nodePath.dirname(candidate),
@@ -1855,10 +1866,8 @@ var IncludeLoader = class {
1855
1866
  return hoconValueToResObj(propertiesToHoconValue(content));
1856
1867
  }
1857
1868
  const tokens = tokenize(content);
1858
- if (!hasContentTokens(tokens)) {
1859
- return makeResObj();
1860
- }
1861
1869
  const ast = parseTokens(tokens);
1870
+ assertObjectRootedInclude(ast, candidate);
1862
1871
  return this.onBuildResObjAsync(ast, {
1863
1872
  ...this.opts,
1864
1873
  baseDir: nodePath.dirname(candidate),
@@ -2601,6 +2610,32 @@ var Config = class _Config {
2601
2610
  if (v === void 0) return void 0;
2602
2611
  return hoconToJs(v);
2603
2612
  }
2613
+ /**
2614
+ * Returns the raw {@link HoconValue} node at `path` as a deeply-immutable
2615
+ * {@link ReadonlyHoconValue} view, or `undefined` if the path is absent. Unlike
2616
+ * `get` (which decodes to a plain JS value), this exposes the value tree for
2617
+ * structural introspection via the `value.ts` accessors (`asString`, `asObject`,
2618
+ * `isScalar`, …).
2619
+ *
2620
+ * `getValue('')` returns the root object node.
2621
+ *
2622
+ * Throws {@link NotResolvedError} when the node (or its subtree) is unresolved:
2623
+ * the `HoconValue` union has no placeholder variant, so an unresolved value
2624
+ * cannot be represented. This matches `getConfig` / `getList`.
2625
+ */
2626
+ getValue(path2) {
2627
+ const v = this.lookupNode(path2);
2628
+ if (v === void 0) {
2629
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2630
+ throw new NotResolvedError(path2);
2631
+ }
2632
+ return void 0;
2633
+ }
2634
+ if (v.kind !== "scalar" && !this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2635
+ throw new NotResolvedError(path2);
2636
+ }
2637
+ return v;
2638
+ }
2604
2639
  getString(path2) {
2605
2640
  const v = this.requireScalar(path2);
2606
2641
  if (v.valueType === "null") {
@@ -3024,8 +3059,14 @@ async function defaultReadFile(filePath) {
3024
3059
  }
3025
3060
  function buildResolveContext(input, opts) {
3026
3061
  const tokens = tokenize(input);
3027
- assertNonEmptyDocument(tokens, "input");
3028
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
+ }
3029
3070
  const resolveOpts = {
3030
3071
  env: getEnv(opts),
3031
3072
  baseDir: opts.baseDir,
@@ -3108,6 +3149,35 @@ async function parseFileAsync(filePath, opts = {}) {
3108
3149
  });
3109
3150
  }
3110
3151
 
3152
+ // src/value.ts
3153
+ function asString(v) {
3154
+ return v.kind === "scalar" && v.valueType === "string" ? v.raw : void 0;
3155
+ }
3156
+ function asNumber(v) {
3157
+ return v.kind === "scalar" ? coerceNumber(v.raw) : void 0;
3158
+ }
3159
+ function asBoolean(v) {
3160
+ return v.kind === "scalar" ? coerceBoolean(v.raw) : void 0;
3161
+ }
3162
+ function asObject(v) {
3163
+ return v.kind === "object" ? v.fields : void 0;
3164
+ }
3165
+ function asArray(v) {
3166
+ return v.kind === "array" ? v.items : void 0;
3167
+ }
3168
+ function isObject(v) {
3169
+ return v.kind === "object";
3170
+ }
3171
+ function isArray(v) {
3172
+ return v.kind === "array";
3173
+ }
3174
+ function isScalar(v) {
3175
+ return v.kind === "scalar";
3176
+ }
3177
+ function isNull(v) {
3178
+ return v.kind === "scalar" && v.valueType === "null";
3179
+ }
3180
+
3111
3181
  // src/value-factory.ts
3112
3182
  function fromMap(values, originDescription) {
3113
3183
  const fields = coerceObject(values, "");
@@ -3182,10 +3252,19 @@ function coerceValue(v, atPath) {
3182
3252
  PackageLookupError,
3183
3253
  ParseError,
3184
3254
  ResolveError,
3255
+ asArray,
3256
+ asBoolean,
3257
+ asNumber,
3258
+ asObject,
3259
+ asString,
3185
3260
  defaultParseOptions,
3186
3261
  defaultResolveOptions,
3187
3262
  empty,
3188
3263
  fromMap,
3264
+ isArray,
3265
+ isNull,
3266
+ isObject,
3267
+ isScalar,
3189
3268
  parse,
3190
3269
  parseAsync,
3191
3270
  parseFile,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Config } from './config-DCSLXkNY.cjs';
2
- export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ResolveOptions, S as ScalarValueType, d as defaultParseOptions, b as defaultResolveOptions, p as parse, c as parseAsync, e as parseFile, f as parseFileAsync, g as parseFileWithOptions, h as parseString, i as parseStringWithOptions } from './config-DCSLXkNY.cjs';
1
+ import { C as Config } from './config-D27s3_aG.cjs';
2
+ export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ReadonlyHoconValue, b as ResolveOptions, S as ScalarValueType, c as asArray, d as asBoolean, e as asNumber, f as asObject, g as asString, h as defaultParseOptions, i as defaultResolveOptions, j as isArray, k as isNull, l as isObject, m as isScalar, p as parse, n as parseAsync, o as parseFile, q as parseFileAsync, r as parseFileWithOptions, s as parseString, t as parseStringWithOptions } from './config-D27s3_aG.cjs';
3
3
 
4
4
  declare class ParseError extends Error {
5
5
  readonly line: number;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Config } from './config-DCSLXkNY.js';
2
- export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ResolveOptions, S as ScalarValueType, d as defaultParseOptions, b as defaultResolveOptions, p as parse, c as parseAsync, e as parseFile, f as parseFileAsync, g as parseFileWithOptions, h as parseString, i as parseStringWithOptions } from './config-DCSLXkNY.js';
1
+ import { C as Config } from './config-D27s3_aG.js';
2
+ export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ReadonlyHoconValue, b as ResolveOptions, S as ScalarValueType, c as asArray, d as asBoolean, e as asNumber, f as asObject, g as asString, h as defaultParseOptions, i as defaultResolveOptions, j as isArray, k as isNull, l as isObject, m as isScalar, p as parse, n as parseAsync, o as parseFile, q as parseFileAsync, r as parseFileWithOptions, s as parseString, t as parseStringWithOptions } from './config-D27s3_aG.js';
3
3
 
4
4
  declare class ParseError extends Error {
5
5
  readonly line: number;
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)).toLowerCase();
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),
@@ -2548,6 +2548,32 @@ var Config = class _Config {
2548
2548
  if (v === void 0) return void 0;
2549
2549
  return hoconToJs(v);
2550
2550
  }
2551
+ /**
2552
+ * Returns the raw {@link HoconValue} node at `path` as a deeply-immutable
2553
+ * {@link ReadonlyHoconValue} view, or `undefined` if the path is absent. Unlike
2554
+ * `get` (which decodes to a plain JS value), this exposes the value tree for
2555
+ * structural introspection via the `value.ts` accessors (`asString`, `asObject`,
2556
+ * `isScalar`, …).
2557
+ *
2558
+ * `getValue('')` returns the root object node.
2559
+ *
2560
+ * Throws {@link NotResolvedError} when the node (or its subtree) is unresolved:
2561
+ * the `HoconValue` union has no placeholder variant, so an unresolved value
2562
+ * cannot be represented. This matches `getConfig` / `getList`.
2563
+ */
2564
+ getValue(path2) {
2565
+ const v = this.lookupNode(path2);
2566
+ if (v === void 0) {
2567
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2568
+ throw new NotResolvedError(path2);
2569
+ }
2570
+ return void 0;
2571
+ }
2572
+ if (v.kind !== "scalar" && !this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2573
+ throw new NotResolvedError(path2);
2574
+ }
2575
+ return v;
2576
+ }
2551
2577
  getString(path2) {
2552
2578
  const v = this.requireScalar(path2);
2553
2579
  if (v.valueType === "null") {
@@ -2971,8 +2997,14 @@ async function defaultReadFile(filePath) {
2971
2997
  }
2972
2998
  function buildResolveContext(input, opts) {
2973
2999
  const tokens = tokenize(input);
2974
- assertNonEmptyDocument(tokens, "input");
2975
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
+ }
2976
3008
  const resolveOpts = {
2977
3009
  env: getEnv(opts),
2978
3010
  baseDir: opts.baseDir,
@@ -3055,6 +3087,35 @@ async function parseFileAsync(filePath, opts = {}) {
3055
3087
  });
3056
3088
  }
3057
3089
 
3090
+ // src/value.ts
3091
+ function asString(v) {
3092
+ return v.kind === "scalar" && v.valueType === "string" ? v.raw : void 0;
3093
+ }
3094
+ function asNumber(v) {
3095
+ return v.kind === "scalar" ? coerceNumber(v.raw) : void 0;
3096
+ }
3097
+ function asBoolean(v) {
3098
+ return v.kind === "scalar" ? coerceBoolean(v.raw) : void 0;
3099
+ }
3100
+ function asObject(v) {
3101
+ return v.kind === "object" ? v.fields : void 0;
3102
+ }
3103
+ function asArray(v) {
3104
+ return v.kind === "array" ? v.items : void 0;
3105
+ }
3106
+ function isObject(v) {
3107
+ return v.kind === "object";
3108
+ }
3109
+ function isArray(v) {
3110
+ return v.kind === "array";
3111
+ }
3112
+ function isScalar(v) {
3113
+ return v.kind === "scalar";
3114
+ }
3115
+ function isNull(v) {
3116
+ return v.kind === "scalar" && v.valueType === "null";
3117
+ }
3118
+
3058
3119
  // src/value-factory.ts
3059
3120
  function fromMap(values, originDescription) {
3060
3121
  const fields = coerceObject(values, "");
@@ -3128,10 +3189,19 @@ export {
3128
3189
  PackageLookupError,
3129
3190
  ParseError,
3130
3191
  ResolveError,
3192
+ asArray,
3193
+ asBoolean,
3194
+ asNumber,
3195
+ asObject,
3196
+ asString,
3131
3197
  defaultParseOptions,
3132
3198
  defaultResolveOptions,
3133
3199
  empty,
3134
3200
  fromMap,
3201
+ isArray,
3202
+ isNull,
3203
+ isObject,
3204
+ isScalar,
3135
3205
  parse,
3136
3206
  parseAsync,
3137
3207
  parseFile,
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)).toLowerCase();
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),
@@ -2588,6 +2588,32 @@ var Config = class _Config {
2588
2588
  if (v === void 0) return void 0;
2589
2589
  return hoconToJs(v);
2590
2590
  }
2591
+ /**
2592
+ * Returns the raw {@link HoconValue} node at `path` as a deeply-immutable
2593
+ * {@link ReadonlyHoconValue} view, or `undefined` if the path is absent. Unlike
2594
+ * `get` (which decodes to a plain JS value), this exposes the value tree for
2595
+ * structural introspection via the `value.ts` accessors (`asString`, `asObject`,
2596
+ * `isScalar`, …).
2597
+ *
2598
+ * `getValue('')` returns the root object node.
2599
+ *
2600
+ * Throws {@link NotResolvedError} when the node (or its subtree) is unresolved:
2601
+ * the `HoconValue` union has no placeholder variant, so an unresolved value
2602
+ * cannot be represented. This matches `getConfig` / `getList`.
2603
+ */
2604
+ getValue(path2) {
2605
+ const v = this.lookupNode(path2);
2606
+ if (v === void 0) {
2607
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2608
+ throw new NotResolvedError(path2);
2609
+ }
2610
+ return void 0;
2611
+ }
2612
+ if (v.kind !== "scalar" && !this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2613
+ throw new NotResolvedError(path2);
2614
+ }
2615
+ return v;
2616
+ }
2591
2617
  getString(path2) {
2592
2618
  const v = this.requireScalar(path2);
2593
2619
  if (v.valueType === "null") {
@@ -3000,8 +3026,14 @@ function defaultReadFileSync(filePath) {
3000
3026
  }
3001
3027
  function buildResolveContext(input, opts) {
3002
3028
  const tokens = tokenize(input);
3003
- assertNonEmptyDocument(tokens, "input");
3004
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
+ }
3005
3037
  const resolveOpts = {
3006
3038
  env: getEnv(opts),
3007
3039
  baseDir: opts.baseDir,
package/dist/zod.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ZodType } from 'zod';
2
- import { C as Config, a as ParseOptions } from './config-DCSLXkNY.cjs';
2
+ import { C as Config, a as ParseOptions } from './config-D27s3_aG.cjs';
3
3
 
4
4
  declare function validate<T>(config: Config, schema: ZodType<T>): T;
5
5
  declare function getValidated<T>(config: Config, path: string, schema: ZodType<T>): T;
package/dist/zod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ZodType } from 'zod';
2
- import { C as Config, a as ParseOptions } from './config-DCSLXkNY.js';
2
+ import { C as Config, a as ParseOptions } from './config-D27s3_aG.js';
3
3
 
4
4
  declare function validate<T>(config: Config, schema: ZodType<T>): T;
5
5
  declare function getValidated<T>(config: Config, path: string, schema: ZodType<T>): T;
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)).toLowerCase();
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),
@@ -2548,6 +2548,32 @@ var Config = class _Config {
2548
2548
  if (v === void 0) return void 0;
2549
2549
  return hoconToJs(v);
2550
2550
  }
2551
+ /**
2552
+ * Returns the raw {@link HoconValue} node at `path` as a deeply-immutable
2553
+ * {@link ReadonlyHoconValue} view, or `undefined` if the path is absent. Unlike
2554
+ * `get` (which decodes to a plain JS value), this exposes the value tree for
2555
+ * structural introspection via the `value.ts` accessors (`asString`, `asObject`,
2556
+ * `isScalar`, …).
2557
+ *
2558
+ * `getValue('')` returns the root object node.
2559
+ *
2560
+ * Throws {@link NotResolvedError} when the node (or its subtree) is unresolved:
2561
+ * the `HoconValue` union has no placeholder variant, so an unresolved value
2562
+ * cannot be represented. This matches `getConfig` / `getList`.
2563
+ */
2564
+ getValue(path2) {
2565
+ const v = this.lookupNode(path2);
2566
+ if (v === void 0) {
2567
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2568
+ throw new NotResolvedError(path2);
2569
+ }
2570
+ return void 0;
2571
+ }
2572
+ if (v.kind !== "scalar" && !this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2573
+ throw new NotResolvedError(path2);
2574
+ }
2575
+ return v;
2576
+ }
2551
2577
  getString(path2) {
2552
2578
  const v = this.requireScalar(path2);
2553
2579
  if (v.valueType === "null") {
@@ -2960,8 +2986,14 @@ function defaultReadFileSync(filePath) {
2960
2986
  }
2961
2987
  function buildResolveContext(input, opts) {
2962
2988
  const tokens = tokenize(input);
2963
- assertNonEmptyDocument(tokens, "input");
2964
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
+ }
2965
2997
  const resolveOpts = {
2966
2998
  env: getEnv(opts),
2967
2999
  baseDir: opts.baseDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/ts.hocon",
3
- "version": "1.7.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.0",
43
- "@vitest/coverage-v8": "4.1.2",
44
- "js-yaml": "^4.1.1",
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.18",
55
+ "typedoc": "^0.28.20",
47
56
  "typescript": "^5.9.3",
48
- "vitest": "4.1.2",
49
- "zod": "^4.3.6"
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
- "scripts": {
62
- "build": "tsup",
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
+ }