@o3co/ts.hocon 1.11.0 → 1.13.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/dist/index.cjs CHANGED
@@ -95,13 +95,22 @@ function coerceNumber(value) {
95
95
  return Number(value);
96
96
  }
97
97
  var DURATION_UNITS = {
98
+ // S19.1–S19.3: the bare nano/micro/milli (+plural) aliases are part of the
99
+ // spec's unit lists (HOCON.md L1307–L1309) and accepted by Lightbend
100
+ // (typesafe-config 1.4.6 probe, 2026-08-18); they were missing here.
98
101
  ns: 1e-6,
102
+ nano: 1e-6,
103
+ nanos: 1e-6,
99
104
  nanosecond: 1e-6,
100
105
  nanoseconds: 1e-6,
101
106
  us: 1e-3,
107
+ micro: 1e-3,
108
+ micros: 1e-3,
102
109
  microsecond: 1e-3,
103
110
  microseconds: 1e-3,
104
111
  ms: 1,
112
+ milli: 1,
113
+ millis: 1,
105
114
  millisecond: 1,
106
115
  milliseconds: 1,
107
116
  s: 1e3,
@@ -153,37 +162,76 @@ function parseDuration(value, outputUnit = "ms") {
153
162
  return ms / divisor;
154
163
  }
155
164
  var BYTE_UNITS = {
165
+ // S21.1–S21.4 — the EXACT Lightbend/typesafe-config unit set (1.4.6 probe,
166
+ // 2026-08-18). Multi-letter units are case-sensitive: the SI decimal short
167
+ // form is `kB` (NOT `KB`/`kb` — Lightbend rejects both), higher decimals
168
+ // are `MB`…`YB`, binary prefixes are `Ki`/`KiB`…`Yi`/`YiB` with capital
169
+ // first letter, and long forms are lowercase only (`kilobyte`, never
170
+ // `Kilobyte`). Only the bare byte unit (`B`/`b`) and the single-letter
171
+ // -Xmx forms accept both cases. The old case-insensitive fallback and the
172
+ // lowercase alias rows accepted spellings Lightbend rejects — removed.
156
173
  B: 1,
174
+ b: 1,
157
175
  byte: 1,
158
176
  bytes: 1,
159
- KB: 1e3,
177
+ kB: 1e3,
160
178
  kilobyte: 1e3,
161
179
  kilobytes: 1e3,
162
- KiB: 1024,
163
- kibibyte: 1024,
164
- kibibytes: 1024,
165
180
  MB: 1e6,
166
181
  megabyte: 1e6,
167
182
  megabytes: 1e6,
168
- MiB: 1048576,
169
- mebibyte: 1048576,
170
- mebibytes: 1048576,
171
183
  GB: 1e9,
172
184
  gigabyte: 1e9,
173
185
  gigabytes: 1e9,
174
- GiB: 1073741824,
175
- gibibyte: 1073741824,
176
- gibibytes: 1073741824,
177
186
  TB: 1e12,
178
187
  terabyte: 1e12,
179
188
  terabytes: 1e12,
189
+ PB: 1e15,
190
+ petabyte: 1e15,
191
+ petabytes: 1e15,
192
+ EB: 1e18,
193
+ exabyte: 1e18,
194
+ exabytes: 1e18,
195
+ ZB: 1e21,
196
+ zettabyte: 1e21,
197
+ zettabytes: 1e21,
198
+ YB: 1e24,
199
+ yottabyte: 1e24,
200
+ yottabytes: 1e24,
201
+ Ki: 1024,
202
+ KiB: 1024,
203
+ kibibyte: 1024,
204
+ kibibytes: 1024,
205
+ Mi: 1048576,
206
+ MiB: 1048576,
207
+ mebibyte: 1048576,
208
+ mebibytes: 1048576,
209
+ Gi: 1073741824,
210
+ GiB: 1073741824,
211
+ gibibyte: 1073741824,
212
+ gibibytes: 1073741824,
213
+ Ti: 1099511627776,
180
214
  TiB: 1099511627776,
181
215
  tebibyte: 1099511627776,
182
216
  tebibytes: 1099511627776,
183
- // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
184
- // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
185
- // Both upper- and lowercase accepted per Lightbend behaviour.
186
- // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
217
+ Pi: 1024 ** 5,
218
+ PiB: 1024 ** 5,
219
+ pebibyte: 1024 ** 5,
220
+ pebibytes: 1024 ** 5,
221
+ Ei: 1024 ** 6,
222
+ EiB: 1024 ** 6,
223
+ exbibyte: 1024 ** 6,
224
+ exbibytes: 1024 ** 6,
225
+ Zi: 1024 ** 7,
226
+ ZiB: 1024 ** 7,
227
+ zebibyte: 1024 ** 7,
228
+ zebibytes: 1024 ** 7,
229
+ Yi: 1024 ** 8,
230
+ YiB: 1024 ** 8,
231
+ yobibyte: 1024 ** 8,
232
+ yobibytes: 1024 ** 8,
233
+ // S21.4 — single-letter abbreviations → powers of two (java -Xmx
234
+ // convention); Lightbend accepts BOTH cases here, through the full ladder.
187
235
  K: 1024,
188
236
  k: 1024,
189
237
  M: 1024 ** 2,
@@ -196,16 +244,10 @@ var BYTE_UNITS = {
196
244
  p: 1024 ** 5,
197
245
  E: 1024 ** 6,
198
246
  e: 1024 ** 6,
199
- // lowercase short-form aliases
200
- b: 1,
201
- kb: 1e3,
202
- kib: 1024,
203
- mb: 1e6,
204
- mib: 1048576,
205
- gb: 1e9,
206
- gib: 1073741824,
207
- tb: 1e12,
208
- tib: 1099511627776
247
+ Z: 1024 ** 7,
248
+ z: 1024 ** 7,
249
+ Y: 1024 ** 8,
250
+ y: 1024 ** 8
209
251
  };
210
252
  var OUTPUT_BYTE_UNITS = {
211
253
  B: 1,
@@ -243,10 +285,7 @@ function parseBytes(value, outputUnit = "B") {
243
285
  }
244
286
  return bytes2 / divisor;
245
287
  }
246
- let mult = BYTE_UNITS[unit];
247
- if (mult === void 0) {
248
- mult = BYTE_UNITS[unit.toLowerCase()];
249
- }
288
+ const mult = BYTE_UNITS[unit];
250
289
  if (mult === void 0) return NaN;
251
290
  const bytes = num * mult;
252
291
  if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
@@ -310,35 +349,114 @@ function stringSegmentsToKey(segments) {
310
349
  return t;
311
350
  }).join(".");
312
351
  }
352
+ function prefixSelfRefRemainder(s, fullKey) {
353
+ const texts = s.segments.map((seg) => seg.text);
354
+ if (!stringSegmentsToKey(texts).startsWith(fullKey + ".")) return null;
355
+ for (let n = 1; n < texts.length; n++) {
356
+ if (stringSegmentsToKey(texts.slice(0, n)) === fullKey) return texts.slice(n);
357
+ }
358
+ return null;
359
+ }
360
+ function navigateResolverValue(v, remainder) {
361
+ let cur = v;
362
+ for (const seg of remainder) {
363
+ if (isSubst(cur) || isConcat(cur)) return null;
364
+ if (isResObj(cur)) {
365
+ const next = cur.fields.get(seg);
366
+ if (next === void 0) return void 0;
367
+ cur = next;
368
+ continue;
369
+ }
370
+ const hv = cur;
371
+ if (hv.kind === "object") {
372
+ const next = hv.fields.get(seg);
373
+ if (next === void 0) return void 0;
374
+ cur = next;
375
+ continue;
376
+ }
377
+ return void 0;
378
+ }
379
+ return cur;
380
+ }
381
+ function foldPrefixSelfRef(s, replacement, remainder) {
382
+ const navigated = navigateResolverValue(replacement, remainder);
383
+ if (navigated === null) return s;
384
+ if (navigated === void 0) {
385
+ return {
386
+ _kind: "subst-placeholder",
387
+ segments: s.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
388
+ optional: s.optional,
389
+ knownAbsent: true,
390
+ listSuffix: s.listSuffix,
391
+ line: s.line,
392
+ col: s.col,
393
+ prefixLen: s.prefixLen
394
+ };
395
+ }
396
+ return cloneResolverValue(navigated);
397
+ }
398
+ function mergeResObjLayers(base, top) {
399
+ const fields = new Map(base.fields);
400
+ for (const [k, tv] of top.fields) {
401
+ const bv = fields.get(k);
402
+ if (bv !== void 0 && isResObj(bv) && isResObj(tv)) {
403
+ fields.set(k, mergeResObjLayers(bv, tv));
404
+ } else {
405
+ fields.set(k, tv);
406
+ }
407
+ }
408
+ const priorValues = new Map(base.priorValues);
409
+ for (const [k, pv] of top.priorValues) priorValues.set(k, pv);
410
+ const resetKeys = new Set(base.resetKeys);
411
+ for (const k of top.resetKeys) resetKeys.add(k);
412
+ return {
413
+ _kind: "res-obj",
414
+ fields,
415
+ priorValues,
416
+ resetKeys
417
+ };
418
+ }
313
419
  function containsSelfRef(v, fullKey) {
314
- if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
315
- if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
420
+ return containsSelfRefInner(v, fullKey, true);
421
+ }
422
+ function containsSelfRefInner(v, fullKey, allowPrefix) {
423
+ if (isSubst(v))
424
+ return !v.knownAbsent && (substFullKey(v) === fullKey || allowPrefix && prefixSelfRefRemainder(v, fullKey) !== null);
425
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRefInner(n, fullKey, allowPrefix));
316
426
  if (isResObj(v)) {
317
427
  for (const f of v.fields.values()) {
318
- if (containsSelfRef(f, fullKey)) return true;
428
+ if (containsSelfRefInner(f, fullKey, false)) return true;
319
429
  }
320
430
  return false;
321
431
  }
322
432
  const hv = v;
323
433
  if (hv.kind === "array") {
324
- return hv.items.some((item) => containsSelfRef(item, fullKey));
434
+ return hv.items.some((item) => containsSelfRefInner(item, fullKey, allowPrefix));
325
435
  }
326
436
  if (hv.kind === "object") {
327
437
  for (const f of hv.fields.values()) {
328
- if (containsSelfRef(f, fullKey)) return true;
438
+ if (containsSelfRefInner(f, fullKey, false)) return true;
329
439
  }
330
440
  return false;
331
441
  }
332
442
  return false;
333
443
  }
334
444
  function foldSelfRef(v, fullKey, replacement) {
445
+ return foldSelfRefInner(v, fullKey, replacement, true);
446
+ }
447
+ function foldSelfRefInner(v, fullKey, replacement, allowPrefix) {
335
448
  if (isSubst(v)) {
336
- return substFullKey(v) === fullKey ? replacement : v;
449
+ if (substFullKey(v) === fullKey) return replacement;
450
+ if (allowPrefix) {
451
+ const remainder = prefixSelfRefRemainder(v, fullKey);
452
+ if (remainder !== null) return foldPrefixSelfRef(v, replacement, remainder);
453
+ }
454
+ return v;
337
455
  }
338
456
  if (isConcat(v)) {
339
457
  return {
340
458
  _kind: "concat-placeholder",
341
- nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
459
+ nodes: v.nodes.map((n) => foldSelfRefInner(n, fullKey, replacement, allowPrefix)),
342
460
  line: v.line,
343
461
  col: v.col
344
462
  };
@@ -346,7 +464,7 @@ function foldSelfRef(v, fullKey, replacement) {
346
464
  if (isResObj(v)) {
347
465
  const newFields = /* @__PURE__ */ new Map();
348
466
  for (const [k, val] of v.fields) {
349
- newFields.set(k, foldSelfRef(val, fullKey, replacement));
467
+ newFields.set(k, foldSelfRefInner(val, fullKey, replacement, false));
350
468
  }
351
469
  return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
352
470
  }
@@ -354,13 +472,15 @@ function foldSelfRef(v, fullKey, replacement) {
354
472
  if (hv.kind === "array") {
355
473
  return {
356
474
  kind: "array",
357
- items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
475
+ items: hv.items.map(
476
+ (item) => foldSelfRefInner(item, fullKey, replacement, allowPrefix)
477
+ )
358
478
  };
359
479
  }
360
480
  if (hv.kind === "object") {
361
481
  const newFields = /* @__PURE__ */ new Map();
362
482
  for (const [k, val] of hv.fields) {
363
- newFields.set(k, foldSelfRef(val, fullKey, replacement));
483
+ newFields.set(k, foldSelfRefInner(val, fullKey, replacement, false));
364
484
  }
365
485
  return { kind: "object", fields: newFields };
366
486
  }
@@ -404,10 +524,26 @@ function foldKnownAbsentSelfRef(v, fullKey, replacement) {
404
524
  function foldOrSkipPrior(prior, fullKey, old) {
405
525
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
406
526
  if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
527
+ if (isSubst(prior)) {
528
+ const remainder = prefixSelfRefRemainder(prior, fullKey);
529
+ if (remainder !== null) {
530
+ const navigated = navigateResolverValue(old, remainder);
531
+ if (navigated === void 0 && prior.optional) return cloneResolverValue(old);
532
+ if (navigated !== null && navigated !== void 0 && isResObj(navigated) && isResObj(old)) {
533
+ return mergeResObjLayers(
534
+ cloneResolverValue(old),
535
+ cloneResolverValue(navigated)
536
+ );
537
+ }
538
+ }
539
+ }
407
540
  return foldSelfRef(prior, fullKey, old);
408
541
  }
409
542
  function foldOptionalSelfRefAbsent(v, fullKey) {
410
- if (isSubst(v) && substFullKey(v) === fullKey) {
543
+ return foldOptionalSelfRefAbsentInner(v, fullKey, true);
544
+ }
545
+ function foldOptionalSelfRefAbsentInner(v, fullKey, allowPrefix) {
546
+ if (isSubst(v) && (substFullKey(v) === fullKey || allowPrefix && prefixSelfRefRemainder(v, fullKey) !== null)) {
411
547
  if (!v.optional) return void 0;
412
548
  return {
413
549
  _kind: "subst-placeholder",
@@ -423,7 +559,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
423
559
  if (isConcat(v)) {
424
560
  const nodes = [];
425
561
  for (const node of v.nodes) {
426
- const folded = foldOptionalSelfRefAbsent(node, fullKey);
562
+ const folded = foldOptionalSelfRefAbsentInner(node, fullKey, allowPrefix);
427
563
  if (folded === void 0) return void 0;
428
564
  nodes.push(folded);
429
565
  }
@@ -432,7 +568,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
432
568
  if (isResObj(v)) {
433
569
  const fields = /* @__PURE__ */ new Map();
434
570
  for (const [key, value] of v.fields) {
435
- const folded = foldOptionalSelfRefAbsent(value, fullKey);
571
+ const folded = foldOptionalSelfRefAbsentInner(value, fullKey, false);
436
572
  if (folded === void 0) return void 0;
437
573
  fields.set(key, folded);
438
574
  }
@@ -442,7 +578,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
442
578
  if (hv.kind === "array") {
443
579
  const items = [];
444
580
  for (const item of hv.items) {
445
- const folded = foldOptionalSelfRefAbsent(item, fullKey);
581
+ const folded = foldOptionalSelfRefAbsentInner(item, fullKey, allowPrefix);
446
582
  if (folded === void 0) return void 0;
447
583
  items.push(folded);
448
584
  }
@@ -451,7 +587,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
451
587
  if (hv.kind === "object") {
452
588
  const fields = /* @__PURE__ */ new Map();
453
589
  for (const [key, value] of hv.fields) {
454
- const folded = foldOptionalSelfRefAbsent(value, fullKey);
590
+ const folded = foldOptionalSelfRefAbsentInner(value, fullKey, false);
455
591
  if (folded === void 0) return void 0;
456
592
  fields.set(key, folded);
457
593
  }
@@ -824,7 +960,6 @@ var Lexer = class {
824
960
  if (!closed) {
825
961
  throw new ParseError("unterminated triple-quoted string", sl, sc);
826
962
  }
827
- if (value.startsWith("\n")) value = value.slice(1);
828
963
  this.push("triple_string", value, sl, sc, true);
829
964
  continue;
830
965
  }
@@ -1141,9 +1276,9 @@ var Parser = class {
1141
1276
  }
1142
1277
  }
1143
1278
  this.skip("newline");
1144
- const remaining = this.peek();
1145
- if (remaining.kind !== "eof") {
1146
- throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
1279
+ const remaining2 = this.peek();
1280
+ if (remaining2.kind !== "eof") {
1281
+ throw new ParseError(`Unexpected token '${remaining2.value}' after closing brace`, remaining2.line, remaining2.col);
1147
1282
  }
1148
1283
  return { kind: "object", fields: allFields, pos: first.pos };
1149
1284
  }
@@ -1151,13 +1286,19 @@ var Parser = class {
1151
1286
  this.advance();
1152
1287
  const arr = this.parseArray();
1153
1288
  this.skip("newline");
1154
- const remaining = this.peek();
1155
- if (remaining.kind !== "eof") {
1156
- throw new ParseError(`Unexpected token '${remaining.value}' after root array`, remaining.line, remaining.col);
1289
+ const remaining2 = this.peek();
1290
+ if (remaining2.kind !== "eof") {
1291
+ throw new ParseError(`Unexpected token '${remaining2.value}' after root array`, remaining2.line, remaining2.col);
1157
1292
  }
1158
1293
  return { ...arr, pos: { line: t.line, col: t.col } };
1159
1294
  }
1160
- return this.parseObject(false);
1295
+ const root = this.parseObject(false);
1296
+ this.skip("newline");
1297
+ const remaining = this.peek();
1298
+ if (remaining.kind !== "eof") {
1299
+ throw new ParseError(`Unexpected token '${remaining.value}' after root content`, remaining.line, remaining.col);
1300
+ }
1301
+ return root;
1161
1302
  }
1162
1303
  peek(offset = 0) {
1163
1304
  return this.tokens[this.pos + offset] ?? EOF_TOKEN;
@@ -1548,6 +1689,39 @@ function stripBom(input) {
1548
1689
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1549
1690
  }
1550
1691
 
1692
+ // src/internal/depth.ts
1693
+ var MAX_PATH_SEGMENTS = 64;
1694
+ function tooDeep(segments) {
1695
+ return segments > MAX_PATH_SEGMENTS;
1696
+ }
1697
+ function guardStackDepth(fn, wrap) {
1698
+ try {
1699
+ return fn();
1700
+ } catch (e) {
1701
+ if (!isStackOverflow(e)) throw e;
1702
+ throw wrap(STACK_MESSAGE);
1703
+ }
1704
+ }
1705
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
1706
+ function isStackOverflow(e) {
1707
+ if (!(e instanceof RangeError)) return false;
1708
+ const m = e.message.toLowerCase();
1709
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
1710
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
1711
+ m.includes("stack overflow");
1712
+ }
1713
+ function depthError(message) {
1714
+ return new ConfigError(message, "");
1715
+ }
1716
+ async function guardStackDepthAsync(fn, wrap) {
1717
+ try {
1718
+ return await fn();
1719
+ } catch (e) {
1720
+ if (!isStackOverflow(e)) throw e;
1721
+ throw wrap(STACK_MESSAGE);
1722
+ }
1723
+ }
1724
+
1551
1725
  // src/internal/properties/properties.ts
1552
1726
  function parseProperties(input) {
1553
1727
  const pairs = [];
@@ -1555,7 +1729,15 @@ function parseProperties(input) {
1555
1729
  const [rawKey, rawValue] = splitKeyValue(text);
1556
1730
  const key = unescapeProps(rawKey, line);
1557
1731
  if (key === "") continue;
1558
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
1732
+ const segments = key.split(".");
1733
+ if (tooDeep(segments.length)) {
1734
+ throw new ParseError(
1735
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1736
+ line,
1737
+ 1
1738
+ );
1739
+ }
1740
+ pairs.push([segments, unescapeProps(rawValue, line)]);
1559
1741
  }
1560
1742
  return nestPairs(pairs);
1561
1743
  }
@@ -2274,6 +2456,12 @@ var SubstitutionResolver = class {
2274
2456
  // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2275
2457
  // same prefix-match semantics).
2276
2458
  this.resolvingFieldPath = [];
2459
+ // S13a.12 recursion guard: field keys whose prior is currently being
2460
+ // resolved through the prefix-self-ref branch. Saved priors are
2461
+ // prefix-self-ref-free by the fold invariant, so re-entry only happens on
2462
+ // a shape the fold could not see through (a live subst mid-navigation) —
2463
+ // report it as a cycle instead of recursing forever.
2464
+ this.resolvingPrefixPriors = /* @__PURE__ */ new Set();
2277
2465
  }
2278
2466
  /** Returns a prefix for error messages, including origin info when set. */
2279
2467
  originPrefix() {
@@ -2288,46 +2476,57 @@ var SubstitutionResolver = class {
2288
2476
  this.resolvingFieldPath.push(key);
2289
2477
  const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2290
2478
  this.cache.delete(fullCacheKey);
2291
- let resolved;
2292
2479
  try {
2293
- resolved = this.resolveVal(val, obj);
2294
- } finally {
2295
- this.resolvingFieldPath.pop();
2296
- }
2297
- if (resolved !== void 0) {
2298
- if (resolved.kind === "object") {
2480
+ const resolved = this.resolveVal(val, obj);
2481
+ if (resolved !== void 0) {
2482
+ if (resolved.kind === "object") {
2483
+ const prior = obj.priorValues.get(key);
2484
+ if (prior !== void 0) {
2485
+ const priorResolved = this.resolveVal(prior, obj);
2486
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2487
+ const finalValue = deepMergeHoconValues(
2488
+ priorResolved,
2489
+ resolved
2490
+ );
2491
+ result.set(key, finalValue);
2492
+ this.cache.set(fullCacheKey, finalValue);
2493
+ this.cacheDescendants(fullCacheKey, finalValue);
2494
+ continue;
2495
+ }
2496
+ }
2497
+ }
2498
+ result.set(key, resolved);
2499
+ this.cache.set(fullCacheKey, resolved);
2500
+ this.cacheDescendants(fullCacheKey, resolved);
2501
+ } else {
2299
2502
  const prior = obj.priorValues.get(key);
2300
2503
  if (prior !== void 0) {
2301
2504
  const priorResolved = this.resolveVal(prior, obj);
2302
- if (priorResolved !== void 0 && priorResolved.kind === "object") {
2303
- const finalValue = deepMergeHoconValues(
2304
- priorResolved,
2305
- resolved
2306
- );
2307
- result.set(key, finalValue);
2308
- this.cache.set(fullCacheKey, finalValue);
2309
- this.cacheDescendants(fullCacheKey, finalValue);
2310
- continue;
2505
+ if (priorResolved !== void 0) {
2506
+ result.set(key, priorResolved);
2507
+ this.cache.set(fullCacheKey, priorResolved);
2508
+ this.cacheDescendants(fullCacheKey, priorResolved);
2311
2509
  }
2312
2510
  }
2313
2511
  }
2314
- result.set(key, resolved);
2315
- this.cache.set(fullCacheKey, resolved);
2316
- this.cacheDescendants(fullCacheKey, resolved);
2317
- } else {
2318
- const prior = obj.priorValues.get(key);
2319
- if (prior !== void 0) {
2320
- const priorResolved = this.resolveVal(prior, obj);
2321
- if (priorResolved !== void 0) {
2322
- result.set(key, priorResolved);
2323
- this.cache.set(fullCacheKey, priorResolved);
2324
- this.cacheDescendants(fullCacheKey, priorResolved);
2325
- }
2326
- }
2512
+ } finally {
2513
+ this.resolvingFieldPath.pop();
2327
2514
  }
2328
2515
  }
2329
2516
  return { kind: "object", fields: result };
2330
2517
  }
2518
+ /** S13a.12: walk resolved-object fields by segment text. A missing segment
2519
+ * or a walk into a scalar/array is path-absent → undefined. */
2520
+ navigateResolved(v, remainder) {
2521
+ let cur = v;
2522
+ for (const seg of remainder) {
2523
+ if (cur.kind !== "object") return void 0;
2524
+ const next = cur.fields.get(seg);
2525
+ if (next === void 0) return void 0;
2526
+ cur = next;
2527
+ }
2528
+ return cur;
2529
+ }
2331
2530
  cacheDescendants(prefix, value) {
2332
2531
  if (value.kind !== "object") return;
2333
2532
  for (const [key, child] of value.fields) {
@@ -2350,17 +2549,59 @@ var SubstitutionResolver = class {
2350
2549
  if (isResObj(v)) return this.resolveResObj(v);
2351
2550
  const hv = v;
2352
2551
  if (hv.kind === "array") {
2353
- return {
2354
- kind: "array",
2355
- items: hv.items.map(
2356
- (item) => this.resolveVal(item, scope) ?? { kind: "scalar", raw: "null", valueType: "null" }
2357
- )
2358
- };
2552
+ const items = [];
2553
+ for (const item of hv.items) {
2554
+ const resolved = this.resolveVal(item, scope);
2555
+ if (resolved === void 0) continue;
2556
+ items.push(resolved);
2557
+ }
2558
+ return { kind: "array", items };
2359
2559
  }
2360
2560
  return hv;
2361
2561
  }
2362
2562
  resolveSubst(s, scope) {
2363
- if (s.knownAbsent) return void 0;
2563
+ if (s.knownAbsent) {
2564
+ if (!s.optional) {
2565
+ if (this.opts.allowUnresolved) return s;
2566
+ const k = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2567
+ throw new ResolveError(
2568
+ `${this.originPrefix()}could not resolve substitution: \${${k}}`,
2569
+ k,
2570
+ s.line,
2571
+ s.col
2572
+ );
2573
+ }
2574
+ return void 0;
2575
+ }
2576
+ {
2577
+ const rfp = this.resolvingFieldPath;
2578
+ const sTexts = s.segments.map((seg) => seg.text);
2579
+ if (rfp.length > 0 && rfp.length < sTexts.length && stringSegmentsToKey(sTexts.slice(0, rfp.length)) === stringSegmentsToKey(rfp)) {
2580
+ const guardKey = stringSegmentsToKey(rfp);
2581
+ const prior = scope.priorValues.get(rfp[rfp.length - 1]);
2582
+ if (prior !== void 0 && !this.resolvingPrefixPriors.has(guardKey)) {
2583
+ this.resolvingPrefixPriors.add(guardKey);
2584
+ try {
2585
+ const priorResolved = this.resolveVal(prior, scope);
2586
+ if (priorResolved !== void 0) {
2587
+ const navigated = this.navigateResolved(priorResolved, sTexts.slice(rfp.length));
2588
+ if (navigated !== void 0) return navigated;
2589
+ }
2590
+ } finally {
2591
+ this.resolvingPrefixPriors.delete(guardKey);
2592
+ }
2593
+ }
2594
+ if (s.optional) return void 0;
2595
+ if (this.opts.allowUnresolved) return s;
2596
+ const k = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2597
+ throw new ResolveError(
2598
+ `${this.originPrefix()}could not resolve substitution: \${${k}}`,
2599
+ k,
2600
+ s.line,
2601
+ s.col
2602
+ );
2603
+ }
2604
+ }
2364
2605
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2365
2606
  if (this.cache.has(key)) return this.cache.get(key);
2366
2607
  if (this.resolving.has(key)) {
@@ -2691,6 +2932,121 @@ async function resolveAsync(ast, opts) {
2691
2932
  return resolveTree(root, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
2692
2933
  }
2693
2934
 
2935
+ // src/render-hocon.ts
2936
+ function renderHoconFromRoot(root) {
2937
+ const out = [];
2938
+ renderObjectBody(out, root, 0);
2939
+ return out.join("");
2940
+ }
2941
+ function renderObjectBody(out, o, depth) {
2942
+ const indent = " ".repeat(depth);
2943
+ for (const [k, v] of o.fields) {
2944
+ out.push(indent, renderKey(k));
2945
+ if (v.kind === "object") {
2946
+ out.push(" {");
2947
+ if (v.fields.size === 0) {
2948
+ out.push("}\n");
2949
+ continue;
2950
+ }
2951
+ out.push("\n");
2952
+ renderObjectBody(out, v, depth + 1);
2953
+ out.push(indent, "}\n");
2954
+ } else {
2955
+ out.push(" = ");
2956
+ renderValue(out, v, depth);
2957
+ out.push("\n");
2958
+ }
2959
+ }
2960
+ }
2961
+ function renderValue(out, v, depth) {
2962
+ switch (v.kind) {
2963
+ case "object": {
2964
+ if (v.fields.size === 0) {
2965
+ out.push("{}");
2966
+ return;
2967
+ }
2968
+ out.push("{\n");
2969
+ renderObjectBody(out, v, depth + 1);
2970
+ out.push(" ".repeat(depth), "}");
2971
+ return;
2972
+ }
2973
+ case "array":
2974
+ renderArray(out, v, depth);
2975
+ return;
2976
+ case "scalar":
2977
+ out.push(renderScalar(v));
2978
+ return;
2979
+ default:
2980
+ throw new ConfigError(
2981
+ "renderHocon: unrenderable value (config must be resolved data)",
2982
+ ""
2983
+ );
2984
+ }
2985
+ }
2986
+ function renderArray(out, a, depth) {
2987
+ if (a.items.length === 0) {
2988
+ out.push("[]");
2989
+ return;
2990
+ }
2991
+ const inner = " ".repeat(depth + 1);
2992
+ out.push("[\n");
2993
+ for (const e of a.items) {
2994
+ out.push(inner);
2995
+ renderValue(out, e, depth + 1);
2996
+ out.push("\n");
2997
+ }
2998
+ out.push(" ".repeat(depth), "]");
2999
+ }
3000
+ function renderScalar(s) {
3001
+ switch (s.valueType) {
3002
+ case "null":
3003
+ return "null";
3004
+ case "boolean":
3005
+ case "number":
3006
+ return s.raw;
3007
+ case "string":
3008
+ return renderString(s.raw);
3009
+ }
3010
+ }
3011
+ var SAFE_UNQUOTED_KEY = /^[A-Za-z0-9_-]+$/;
3012
+ function renderKey(k) {
3013
+ if (k === "include") return quoteString(k);
3014
+ return SAFE_UNQUOTED_KEY.test(k) ? k : quoteString(k);
3015
+ }
3016
+ var SAFE_BARE_STRING = /^[A-Za-z][A-Za-z0-9_-]*$/;
3017
+ var STRING_KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "yes", "no", "on", "off"]);
3018
+ function renderString(s) {
3019
+ return SAFE_BARE_STRING.test(s) && !STRING_KEYWORDS.has(s.toLowerCase()) ? s : quoteString(s);
3020
+ }
3021
+ function quoteString(s) {
3022
+ if (s.includes("\n") && !s.includes("\r") && !s.includes('"""') && !s.endsWith('"')) {
3023
+ return `"""${s}"""`;
3024
+ }
3025
+ let b = '"';
3026
+ for (const ch of s) {
3027
+ switch (ch) {
3028
+ case '"':
3029
+ b += '\\"';
3030
+ break;
3031
+ case "\\":
3032
+ b += "\\\\";
3033
+ break;
3034
+ case "\n":
3035
+ b += "\\n";
3036
+ break;
3037
+ case "\r":
3038
+ b += "\\r";
3039
+ break;
3040
+ case " ":
3041
+ b += "\\t";
3042
+ break;
3043
+ default:
3044
+ b += ch;
3045
+ }
3046
+ }
3047
+ return b + '"';
3048
+ }
3049
+
2694
3050
  // src/config.ts
2695
3051
  var Config = class _Config {
2696
3052
  constructor(root, opts) {
@@ -2996,6 +3352,36 @@ var Config = class _Config {
2996
3352
  toObject(opts) {
2997
3353
  return hoconToJs(this.root, opts?.nullPrototype === true);
2998
3354
  }
3355
+ /**
3356
+ * Renders this resolved Config as HOCON text (E18; lockstep with go.hocon's
3357
+ * `Config.RenderHOCON`, v1.11.0).
3358
+ *
3359
+ * The output round-trips: parsing it back yields the same value tree. That
3360
+ * is the correctness contract, not byte-for-byte formatting — a scalar is
3361
+ * quoted whenever leaving it bare would re-parse as a different type (a
3362
+ * string `"8080"` becomes `"8080"`, not `8080`), and left bare only when it
3363
+ * provably cannot.
3364
+ *
3365
+ * The Config must be resolved and hold only data (objects, arrays, string /
3366
+ * number / boolean / null scalars) — exactly what {@link fromMap} and the
3367
+ * format adapters produce. An unresolved placeholder throws
3368
+ * {@link ConfigError}; substitutions have no textual round trip through a
3369
+ * value tree.
3370
+ *
3371
+ * The root object's fields are emitted without enclosing braces, nested
3372
+ * objects as `key { … }`, arrays as newline-separated `[ … ]`, indented two
3373
+ * spaces. Source comments are not represented — a value tree does not carry
3374
+ * them.
3375
+ */
3376
+ renderHocon() {
3377
+ if (!this._resolved) {
3378
+ throw new ConfigError(
3379
+ "renderHocon: unrenderable unresolved substitution (config must be resolved data)",
3380
+ ""
3381
+ );
3382
+ }
3383
+ return renderHoconFromRoot(this.root);
3384
+ }
2999
3385
  /**
3000
3386
  * @internal Test-only: renders this resolved Config's value tree as canonical
3001
3387
  * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
@@ -3249,13 +3635,16 @@ function buildResolveContext(input, opts) {
3249
3635
  return { ast, resolveOpts };
3250
3636
  }
3251
3637
  function parse(input, opts = {}) {
3252
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3253
- const value = resolve2(ast, resolveOpts);
3254
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3255
- return new Config(value, { resolved: true, originDescription: opts.originDescription });
3638
+ return parseStringWithOptions(input, opts);
3256
3639
  }
3257
3640
  var parseString = parse;
3258
3641
  function parseStringWithOptions(input, opts = {}) {
3642
+ return guardStackDepth(
3643
+ () => parseStringInner(input, opts),
3644
+ (msg) => new ParseError(msg, 1, 1, opts.originDescription)
3645
+ );
3646
+ }
3647
+ function parseStringInner(input, opts) {
3259
3648
  const resolveSubstitutions = opts.resolveSubstitutions ?? true;
3260
3649
  const { ast, resolveOpts } = buildResolveContext(input, opts);
3261
3650
  if (resolveSubstitutions) {
@@ -3286,10 +3675,12 @@ function parseFileWithOptions(filePath, opts = {}) {
3286
3675
  });
3287
3676
  }
3288
3677
  async function parseAsync(input, opts = {}) {
3289
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3290
- const value = await resolveAsync(ast, resolveOpts);
3291
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3292
- return new Config(value);
3678
+ return guardStackDepthAsync(async () => {
3679
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
3680
+ const value = await resolveAsync(ast, resolveOpts);
3681
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
3682
+ return new Config(value);
3683
+ }, (msg) => new ParseError(msg, 1, 1, opts.originDescription));
3293
3684
  }
3294
3685
  function parseFile(filePath, opts = {}) {
3295
3686
  if (typeof process === "undefined" && !opts.readFileSync) {
@@ -3350,7 +3741,10 @@ function isNull(v) {
3350
3741
 
3351
3742
  // src/value-factory.ts
3352
3743
  function fromMap(values, originDescription) {
3353
- const fields = coerceObject(values, "");
3744
+ const fields = guardStackDepth(
3745
+ () => coerceObject(values, ""),
3746
+ (msg) => depthError(`fromMap: ${msg}`)
3747
+ );
3354
3748
  const root = { kind: "object", fields };
3355
3749
  return Config._fromResolvedValue(root, { originDescription });
3356
3750
  }