@o3co/ts.hocon 1.12.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.
@@ -34,13 +34,22 @@ function coerceNumber(value) {
34
34
  return Number(value);
35
35
  }
36
36
  var DURATION_UNITS = {
37
+ // S19.1–S19.3: the bare nano/micro/milli (+plural) aliases are part of the
38
+ // spec's unit lists (HOCON.md L1307–L1309) and accepted by Lightbend
39
+ // (typesafe-config 1.4.6 probe, 2026-08-18); they were missing here.
37
40
  ns: 1e-6,
41
+ nano: 1e-6,
42
+ nanos: 1e-6,
38
43
  nanosecond: 1e-6,
39
44
  nanoseconds: 1e-6,
40
45
  us: 1e-3,
46
+ micro: 1e-3,
47
+ micros: 1e-3,
41
48
  microsecond: 1e-3,
42
49
  microseconds: 1e-3,
43
50
  ms: 1,
51
+ milli: 1,
52
+ millis: 1,
44
53
  millisecond: 1,
45
54
  milliseconds: 1,
46
55
  s: 1e3,
@@ -92,37 +101,76 @@ function parseDuration(value, outputUnit = "ms") {
92
101
  return ms / divisor;
93
102
  }
94
103
  var BYTE_UNITS = {
104
+ // S21.1–S21.4 — the EXACT Lightbend/typesafe-config unit set (1.4.6 probe,
105
+ // 2026-08-18). Multi-letter units are case-sensitive: the SI decimal short
106
+ // form is `kB` (NOT `KB`/`kb` — Lightbend rejects both), higher decimals
107
+ // are `MB`…`YB`, binary prefixes are `Ki`/`KiB`…`Yi`/`YiB` with capital
108
+ // first letter, and long forms are lowercase only (`kilobyte`, never
109
+ // `Kilobyte`). Only the bare byte unit (`B`/`b`) and the single-letter
110
+ // -Xmx forms accept both cases. The old case-insensitive fallback and the
111
+ // lowercase alias rows accepted spellings Lightbend rejects — removed.
95
112
  B: 1,
113
+ b: 1,
96
114
  byte: 1,
97
115
  bytes: 1,
98
- KB: 1e3,
116
+ kB: 1e3,
99
117
  kilobyte: 1e3,
100
118
  kilobytes: 1e3,
101
- KiB: 1024,
102
- kibibyte: 1024,
103
- kibibytes: 1024,
104
119
  MB: 1e6,
105
120
  megabyte: 1e6,
106
121
  megabytes: 1e6,
107
- MiB: 1048576,
108
- mebibyte: 1048576,
109
- mebibytes: 1048576,
110
122
  GB: 1e9,
111
123
  gigabyte: 1e9,
112
124
  gigabytes: 1e9,
113
- GiB: 1073741824,
114
- gibibyte: 1073741824,
115
- gibibytes: 1073741824,
116
125
  TB: 1e12,
117
126
  terabyte: 1e12,
118
127
  terabytes: 1e12,
128
+ PB: 1e15,
129
+ petabyte: 1e15,
130
+ petabytes: 1e15,
131
+ EB: 1e18,
132
+ exabyte: 1e18,
133
+ exabytes: 1e18,
134
+ ZB: 1e21,
135
+ zettabyte: 1e21,
136
+ zettabytes: 1e21,
137
+ YB: 1e24,
138
+ yottabyte: 1e24,
139
+ yottabytes: 1e24,
140
+ Ki: 1024,
141
+ KiB: 1024,
142
+ kibibyte: 1024,
143
+ kibibytes: 1024,
144
+ Mi: 1048576,
145
+ MiB: 1048576,
146
+ mebibyte: 1048576,
147
+ mebibytes: 1048576,
148
+ Gi: 1073741824,
149
+ GiB: 1073741824,
150
+ gibibyte: 1073741824,
151
+ gibibytes: 1073741824,
152
+ Ti: 1099511627776,
119
153
  TiB: 1099511627776,
120
154
  tebibyte: 1099511627776,
121
155
  tebibytes: 1099511627776,
122
- // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
123
- // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
124
- // Both upper- and lowercase accepted per Lightbend behaviour.
125
- // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
156
+ Pi: 1024 ** 5,
157
+ PiB: 1024 ** 5,
158
+ pebibyte: 1024 ** 5,
159
+ pebibytes: 1024 ** 5,
160
+ Ei: 1024 ** 6,
161
+ EiB: 1024 ** 6,
162
+ exbibyte: 1024 ** 6,
163
+ exbibytes: 1024 ** 6,
164
+ Zi: 1024 ** 7,
165
+ ZiB: 1024 ** 7,
166
+ zebibyte: 1024 ** 7,
167
+ zebibytes: 1024 ** 7,
168
+ Yi: 1024 ** 8,
169
+ YiB: 1024 ** 8,
170
+ yobibyte: 1024 ** 8,
171
+ yobibytes: 1024 ** 8,
172
+ // S21.4 — single-letter abbreviations → powers of two (java -Xmx
173
+ // convention); Lightbend accepts BOTH cases here, through the full ladder.
126
174
  K: 1024,
127
175
  k: 1024,
128
176
  M: 1024 ** 2,
@@ -135,16 +183,10 @@ var BYTE_UNITS = {
135
183
  p: 1024 ** 5,
136
184
  E: 1024 ** 6,
137
185
  e: 1024 ** 6,
138
- // lowercase short-form aliases
139
- b: 1,
140
- kb: 1e3,
141
- kib: 1024,
142
- mb: 1e6,
143
- mib: 1048576,
144
- gb: 1e9,
145
- gib: 1073741824,
146
- tb: 1e12,
147
- tib: 1099511627776
186
+ Z: 1024 ** 7,
187
+ z: 1024 ** 7,
188
+ Y: 1024 ** 8,
189
+ y: 1024 ** 8
148
190
  };
149
191
  var OUTPUT_BYTE_UNITS = {
150
192
  B: 1,
@@ -182,10 +224,7 @@ function parseBytes(value, outputUnit = "B") {
182
224
  }
183
225
  return bytes2 / divisor;
184
226
  }
185
- let mult = BYTE_UNITS[unit];
186
- if (mult === void 0) {
187
- mult = BYTE_UNITS[unit.toLowerCase()];
188
- }
227
+ const mult = BYTE_UNITS[unit];
189
228
  if (mult === void 0) return NaN;
190
229
  const bytes = num * mult;
191
230
  if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
@@ -232,35 +271,114 @@ function stringSegmentsToKey(segments) {
232
271
  return t;
233
272
  }).join(".");
234
273
  }
274
+ function prefixSelfRefRemainder(s, fullKey) {
275
+ const texts = s.segments.map((seg) => seg.text);
276
+ if (!stringSegmentsToKey(texts).startsWith(fullKey + ".")) return null;
277
+ for (let n = 1; n < texts.length; n++) {
278
+ if (stringSegmentsToKey(texts.slice(0, n)) === fullKey) return texts.slice(n);
279
+ }
280
+ return null;
281
+ }
282
+ function navigateResolverValue(v, remainder) {
283
+ let cur = v;
284
+ for (const seg of remainder) {
285
+ if (isSubst(cur) || isConcat(cur)) return null;
286
+ if (isResObj(cur)) {
287
+ const next = cur.fields.get(seg);
288
+ if (next === void 0) return void 0;
289
+ cur = next;
290
+ continue;
291
+ }
292
+ const hv = cur;
293
+ if (hv.kind === "object") {
294
+ const next = hv.fields.get(seg);
295
+ if (next === void 0) return void 0;
296
+ cur = next;
297
+ continue;
298
+ }
299
+ return void 0;
300
+ }
301
+ return cur;
302
+ }
303
+ function foldPrefixSelfRef(s, replacement, remainder) {
304
+ const navigated = navigateResolverValue(replacement, remainder);
305
+ if (navigated === null) return s;
306
+ if (navigated === void 0) {
307
+ return {
308
+ _kind: "subst-placeholder",
309
+ segments: s.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
310
+ optional: s.optional,
311
+ knownAbsent: true,
312
+ listSuffix: s.listSuffix,
313
+ line: s.line,
314
+ col: s.col,
315
+ prefixLen: s.prefixLen
316
+ };
317
+ }
318
+ return cloneResolverValue(navigated);
319
+ }
320
+ function mergeResObjLayers(base, top) {
321
+ const fields = new Map(base.fields);
322
+ for (const [k, tv] of top.fields) {
323
+ const bv = fields.get(k);
324
+ if (bv !== void 0 && isResObj(bv) && isResObj(tv)) {
325
+ fields.set(k, mergeResObjLayers(bv, tv));
326
+ } else {
327
+ fields.set(k, tv);
328
+ }
329
+ }
330
+ const priorValues = new Map(base.priorValues);
331
+ for (const [k, pv] of top.priorValues) priorValues.set(k, pv);
332
+ const resetKeys = new Set(base.resetKeys);
333
+ for (const k of top.resetKeys) resetKeys.add(k);
334
+ return {
335
+ _kind: "res-obj",
336
+ fields,
337
+ priorValues,
338
+ resetKeys
339
+ };
340
+ }
235
341
  function containsSelfRef(v, fullKey) {
236
- if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
237
- if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
342
+ return containsSelfRefInner(v, fullKey, true);
343
+ }
344
+ function containsSelfRefInner(v, fullKey, allowPrefix) {
345
+ if (isSubst(v))
346
+ return !v.knownAbsent && (substFullKey(v) === fullKey || allowPrefix && prefixSelfRefRemainder(v, fullKey) !== null);
347
+ if (isConcat(v)) return v.nodes.some((n) => containsSelfRefInner(n, fullKey, allowPrefix));
238
348
  if (isResObj(v)) {
239
349
  for (const f of v.fields.values()) {
240
- if (containsSelfRef(f, fullKey)) return true;
350
+ if (containsSelfRefInner(f, fullKey, false)) return true;
241
351
  }
242
352
  return false;
243
353
  }
244
354
  const hv = v;
245
355
  if (hv.kind === "array") {
246
- return hv.items.some((item) => containsSelfRef(item, fullKey));
356
+ return hv.items.some((item) => containsSelfRefInner(item, fullKey, allowPrefix));
247
357
  }
248
358
  if (hv.kind === "object") {
249
359
  for (const f of hv.fields.values()) {
250
- if (containsSelfRef(f, fullKey)) return true;
360
+ if (containsSelfRefInner(f, fullKey, false)) return true;
251
361
  }
252
362
  return false;
253
363
  }
254
364
  return false;
255
365
  }
256
366
  function foldSelfRef(v, fullKey, replacement) {
367
+ return foldSelfRefInner(v, fullKey, replacement, true);
368
+ }
369
+ function foldSelfRefInner(v, fullKey, replacement, allowPrefix) {
257
370
  if (isSubst(v)) {
258
- return substFullKey(v) === fullKey ? replacement : v;
371
+ if (substFullKey(v) === fullKey) return replacement;
372
+ if (allowPrefix) {
373
+ const remainder = prefixSelfRefRemainder(v, fullKey);
374
+ if (remainder !== null) return foldPrefixSelfRef(v, replacement, remainder);
375
+ }
376
+ return v;
259
377
  }
260
378
  if (isConcat(v)) {
261
379
  return {
262
380
  _kind: "concat-placeholder",
263
- nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
381
+ nodes: v.nodes.map((n) => foldSelfRefInner(n, fullKey, replacement, allowPrefix)),
264
382
  line: v.line,
265
383
  col: v.col
266
384
  };
@@ -268,7 +386,7 @@ function foldSelfRef(v, fullKey, replacement) {
268
386
  if (isResObj(v)) {
269
387
  const newFields = /* @__PURE__ */ new Map();
270
388
  for (const [k, val] of v.fields) {
271
- newFields.set(k, foldSelfRef(val, fullKey, replacement));
389
+ newFields.set(k, foldSelfRefInner(val, fullKey, replacement, false));
272
390
  }
273
391
  return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
274
392
  }
@@ -276,13 +394,15 @@ function foldSelfRef(v, fullKey, replacement) {
276
394
  if (hv.kind === "array") {
277
395
  return {
278
396
  kind: "array",
279
- items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
397
+ items: hv.items.map(
398
+ (item) => foldSelfRefInner(item, fullKey, replacement, allowPrefix)
399
+ )
280
400
  };
281
401
  }
282
402
  if (hv.kind === "object") {
283
403
  const newFields = /* @__PURE__ */ new Map();
284
404
  for (const [k, val] of hv.fields) {
285
- newFields.set(k, foldSelfRef(val, fullKey, replacement));
405
+ newFields.set(k, foldSelfRefInner(val, fullKey, replacement, false));
286
406
  }
287
407
  return { kind: "object", fields: newFields };
288
408
  }
@@ -291,10 +411,26 @@ function foldSelfRef(v, fullKey, replacement) {
291
411
  function foldOrSkipPrior(prior, fullKey, old) {
292
412
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
293
413
  if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
414
+ if (isSubst(prior)) {
415
+ const remainder = prefixSelfRefRemainder(prior, fullKey);
416
+ if (remainder !== null) {
417
+ const navigated = navigateResolverValue(old, remainder);
418
+ if (navigated === void 0 && prior.optional) return cloneResolverValue(old);
419
+ if (navigated !== null && navigated !== void 0 && isResObj(navigated) && isResObj(old)) {
420
+ return mergeResObjLayers(
421
+ cloneResolverValue(old),
422
+ cloneResolverValue(navigated)
423
+ );
424
+ }
425
+ }
426
+ }
294
427
  return foldSelfRef(prior, fullKey, old);
295
428
  }
296
429
  function foldOptionalSelfRefAbsent(v, fullKey) {
297
- if (isSubst(v) && substFullKey(v) === fullKey) {
430
+ return foldOptionalSelfRefAbsentInner(v, fullKey, true);
431
+ }
432
+ function foldOptionalSelfRefAbsentInner(v, fullKey, allowPrefix) {
433
+ if (isSubst(v) && (substFullKey(v) === fullKey || allowPrefix && prefixSelfRefRemainder(v, fullKey) !== null)) {
298
434
  if (!v.optional) return void 0;
299
435
  return {
300
436
  _kind: "subst-placeholder",
@@ -310,7 +446,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
310
446
  if (isConcat(v)) {
311
447
  const nodes = [];
312
448
  for (const node of v.nodes) {
313
- const folded = foldOptionalSelfRefAbsent(node, fullKey);
449
+ const folded = foldOptionalSelfRefAbsentInner(node, fullKey, allowPrefix);
314
450
  if (folded === void 0) return void 0;
315
451
  nodes.push(folded);
316
452
  }
@@ -319,7 +455,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
319
455
  if (isResObj(v)) {
320
456
  const fields = /* @__PURE__ */ new Map();
321
457
  for (const [key, value] of v.fields) {
322
- const folded = foldOptionalSelfRefAbsent(value, fullKey);
458
+ const folded = foldOptionalSelfRefAbsentInner(value, fullKey, false);
323
459
  if (folded === void 0) return void 0;
324
460
  fields.set(key, folded);
325
461
  }
@@ -329,7 +465,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
329
465
  if (hv.kind === "array") {
330
466
  const items = [];
331
467
  for (const item of hv.items) {
332
- const folded = foldOptionalSelfRefAbsent(item, fullKey);
468
+ const folded = foldOptionalSelfRefAbsentInner(item, fullKey, allowPrefix);
333
469
  if (folded === void 0) return void 0;
334
470
  items.push(folded);
335
471
  }
@@ -338,7 +474,7 @@ function foldOptionalSelfRefAbsent(v, fullKey) {
338
474
  if (hv.kind === "object") {
339
475
  const fields = /* @__PURE__ */ new Map();
340
476
  for (const [key, value] of hv.fields) {
341
- const folded = foldOptionalSelfRefAbsent(value, fullKey);
477
+ const folded = foldOptionalSelfRefAbsentInner(value, fullKey, false);
342
478
  if (folded === void 0) return void 0;
343
479
  fields.set(key, folded);
344
480
  }
@@ -595,6 +731,12 @@ var SubstitutionResolver = class {
595
731
  // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
596
732
  // same prefix-match semantics).
597
733
  this.resolvingFieldPath = [];
734
+ // S13a.12 recursion guard: field keys whose prior is currently being
735
+ // resolved through the prefix-self-ref branch. Saved priors are
736
+ // prefix-self-ref-free by the fold invariant, so re-entry only happens on
737
+ // a shape the fold could not see through (a live subst mid-navigation) —
738
+ // report it as a cycle instead of recursing forever.
739
+ this.resolvingPrefixPriors = /* @__PURE__ */ new Set();
598
740
  }
599
741
  /** Returns a prefix for error messages, including origin info when set. */
600
742
  originPrefix() {
@@ -609,46 +751,57 @@ var SubstitutionResolver = class {
609
751
  this.resolvingFieldPath.push(key);
610
752
  const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
611
753
  this.cache.delete(fullCacheKey);
612
- let resolved;
613
754
  try {
614
- resolved = this.resolveVal(val, obj);
615
- } finally {
616
- this.resolvingFieldPath.pop();
617
- }
618
- if (resolved !== void 0) {
619
- if (resolved.kind === "object") {
755
+ const resolved = this.resolveVal(val, obj);
756
+ if (resolved !== void 0) {
757
+ if (resolved.kind === "object") {
758
+ const prior = obj.priorValues.get(key);
759
+ if (prior !== void 0) {
760
+ const priorResolved = this.resolveVal(prior, obj);
761
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
762
+ const finalValue = deepMergeHoconValues(
763
+ priorResolved,
764
+ resolved
765
+ );
766
+ result.set(key, finalValue);
767
+ this.cache.set(fullCacheKey, finalValue);
768
+ this.cacheDescendants(fullCacheKey, finalValue);
769
+ continue;
770
+ }
771
+ }
772
+ }
773
+ result.set(key, resolved);
774
+ this.cache.set(fullCacheKey, resolved);
775
+ this.cacheDescendants(fullCacheKey, resolved);
776
+ } else {
620
777
  const prior = obj.priorValues.get(key);
621
778
  if (prior !== void 0) {
622
779
  const priorResolved = this.resolveVal(prior, obj);
623
- if (priorResolved !== void 0 && priorResolved.kind === "object") {
624
- const finalValue = deepMergeHoconValues(
625
- priorResolved,
626
- resolved
627
- );
628
- result.set(key, finalValue);
629
- this.cache.set(fullCacheKey, finalValue);
630
- this.cacheDescendants(fullCacheKey, finalValue);
631
- continue;
780
+ if (priorResolved !== void 0) {
781
+ result.set(key, priorResolved);
782
+ this.cache.set(fullCacheKey, priorResolved);
783
+ this.cacheDescendants(fullCacheKey, priorResolved);
632
784
  }
633
785
  }
634
786
  }
635
- result.set(key, resolved);
636
- this.cache.set(fullCacheKey, resolved);
637
- this.cacheDescendants(fullCacheKey, resolved);
638
- } else {
639
- const prior = obj.priorValues.get(key);
640
- if (prior !== void 0) {
641
- const priorResolved = this.resolveVal(prior, obj);
642
- if (priorResolved !== void 0) {
643
- result.set(key, priorResolved);
644
- this.cache.set(fullCacheKey, priorResolved);
645
- this.cacheDescendants(fullCacheKey, priorResolved);
646
- }
647
- }
787
+ } finally {
788
+ this.resolvingFieldPath.pop();
648
789
  }
649
790
  }
650
791
  return { kind: "object", fields: result };
651
792
  }
793
+ /** S13a.12: walk resolved-object fields by segment text. A missing segment
794
+ * or a walk into a scalar/array is path-absent → undefined. */
795
+ navigateResolved(v, remainder) {
796
+ let cur = v;
797
+ for (const seg of remainder) {
798
+ if (cur.kind !== "object") return void 0;
799
+ const next = cur.fields.get(seg);
800
+ if (next === void 0) return void 0;
801
+ cur = next;
802
+ }
803
+ return cur;
804
+ }
652
805
  cacheDescendants(prefix, value) {
653
806
  if (value.kind !== "object") return;
654
807
  for (const [key, child] of value.fields) {
@@ -671,17 +824,59 @@ var SubstitutionResolver = class {
671
824
  if (isResObj(v)) return this.resolveResObj(v);
672
825
  const hv = v;
673
826
  if (hv.kind === "array") {
674
- return {
675
- kind: "array",
676
- items: hv.items.map(
677
- (item) => this.resolveVal(item, scope) ?? { kind: "scalar", raw: "null", valueType: "null" }
678
- )
679
- };
827
+ const items = [];
828
+ for (const item of hv.items) {
829
+ const resolved = this.resolveVal(item, scope);
830
+ if (resolved === void 0) continue;
831
+ items.push(resolved);
832
+ }
833
+ return { kind: "array", items };
680
834
  }
681
835
  return hv;
682
836
  }
683
837
  resolveSubst(s, scope) {
684
- if (s.knownAbsent) return void 0;
838
+ if (s.knownAbsent) {
839
+ if (!s.optional) {
840
+ if (this.opts.allowUnresolved) return s;
841
+ const k = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
842
+ throw new ResolveError(
843
+ `${this.originPrefix()}could not resolve substitution: \${${k}}`,
844
+ k,
845
+ s.line,
846
+ s.col
847
+ );
848
+ }
849
+ return void 0;
850
+ }
851
+ {
852
+ const rfp = this.resolvingFieldPath;
853
+ const sTexts = s.segments.map((seg) => seg.text);
854
+ if (rfp.length > 0 && rfp.length < sTexts.length && stringSegmentsToKey(sTexts.slice(0, rfp.length)) === stringSegmentsToKey(rfp)) {
855
+ const guardKey = stringSegmentsToKey(rfp);
856
+ const prior = scope.priorValues.get(rfp[rfp.length - 1]);
857
+ if (prior !== void 0 && !this.resolvingPrefixPriors.has(guardKey)) {
858
+ this.resolvingPrefixPriors.add(guardKey);
859
+ try {
860
+ const priorResolved = this.resolveVal(prior, scope);
861
+ if (priorResolved !== void 0) {
862
+ const navigated = this.navigateResolved(priorResolved, sTexts.slice(rfp.length));
863
+ if (navigated !== void 0) return navigated;
864
+ }
865
+ } finally {
866
+ this.resolvingPrefixPriors.delete(guardKey);
867
+ }
868
+ }
869
+ if (s.optional) return void 0;
870
+ if (this.opts.allowUnresolved) return s;
871
+ const k = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
872
+ throw new ResolveError(
873
+ `${this.originPrefix()}could not resolve substitution: \${${k}}`,
874
+ k,
875
+ s.line,
876
+ s.col
877
+ );
878
+ }
879
+ }
685
880
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
686
881
  if (this.cache.has(key)) return this.cache.get(key);
687
882
  if (this.resolving.has(key)) {
@@ -1001,6 +1196,121 @@ function hoconValueToResObj2(value) {
1001
1196
  return obj;
1002
1197
  }
1003
1198
 
1199
+ // src/render-hocon.ts
1200
+ function renderHoconFromRoot(root) {
1201
+ const out = [];
1202
+ renderObjectBody(out, root, 0);
1203
+ return out.join("");
1204
+ }
1205
+ function renderObjectBody(out, o, depth) {
1206
+ const indent = " ".repeat(depth);
1207
+ for (const [k, v] of o.fields) {
1208
+ out.push(indent, renderKey(k));
1209
+ if (v.kind === "object") {
1210
+ out.push(" {");
1211
+ if (v.fields.size === 0) {
1212
+ out.push("}\n");
1213
+ continue;
1214
+ }
1215
+ out.push("\n");
1216
+ renderObjectBody(out, v, depth + 1);
1217
+ out.push(indent, "}\n");
1218
+ } else {
1219
+ out.push(" = ");
1220
+ renderValue(out, v, depth);
1221
+ out.push("\n");
1222
+ }
1223
+ }
1224
+ }
1225
+ function renderValue(out, v, depth) {
1226
+ switch (v.kind) {
1227
+ case "object": {
1228
+ if (v.fields.size === 0) {
1229
+ out.push("{}");
1230
+ return;
1231
+ }
1232
+ out.push("{\n");
1233
+ renderObjectBody(out, v, depth + 1);
1234
+ out.push(" ".repeat(depth), "}");
1235
+ return;
1236
+ }
1237
+ case "array":
1238
+ renderArray(out, v, depth);
1239
+ return;
1240
+ case "scalar":
1241
+ out.push(renderScalar(v));
1242
+ return;
1243
+ default:
1244
+ throw new ConfigError(
1245
+ "renderHocon: unrenderable value (config must be resolved data)",
1246
+ ""
1247
+ );
1248
+ }
1249
+ }
1250
+ function renderArray(out, a, depth) {
1251
+ if (a.items.length === 0) {
1252
+ out.push("[]");
1253
+ return;
1254
+ }
1255
+ const inner = " ".repeat(depth + 1);
1256
+ out.push("[\n");
1257
+ for (const e of a.items) {
1258
+ out.push(inner);
1259
+ renderValue(out, e, depth + 1);
1260
+ out.push("\n");
1261
+ }
1262
+ out.push(" ".repeat(depth), "]");
1263
+ }
1264
+ function renderScalar(s) {
1265
+ switch (s.valueType) {
1266
+ case "null":
1267
+ return "null";
1268
+ case "boolean":
1269
+ case "number":
1270
+ return s.raw;
1271
+ case "string":
1272
+ return renderString(s.raw);
1273
+ }
1274
+ }
1275
+ var SAFE_UNQUOTED_KEY = /^[A-Za-z0-9_-]+$/;
1276
+ function renderKey(k) {
1277
+ if (k === "include") return quoteString(k);
1278
+ return SAFE_UNQUOTED_KEY.test(k) ? k : quoteString(k);
1279
+ }
1280
+ var SAFE_BARE_STRING = /^[A-Za-z][A-Za-z0-9_-]*$/;
1281
+ var STRING_KEYWORDS = /* @__PURE__ */ new Set(["true", "false", "null", "yes", "no", "on", "off"]);
1282
+ function renderString(s) {
1283
+ return SAFE_BARE_STRING.test(s) && !STRING_KEYWORDS.has(s.toLowerCase()) ? s : quoteString(s);
1284
+ }
1285
+ function quoteString(s) {
1286
+ if (s.includes("\n") && !s.includes("\r") && !s.includes('"""') && !s.endsWith('"')) {
1287
+ return `"""${s}"""`;
1288
+ }
1289
+ let b = '"';
1290
+ for (const ch of s) {
1291
+ switch (ch) {
1292
+ case '"':
1293
+ b += '\\"';
1294
+ break;
1295
+ case "\\":
1296
+ b += "\\\\";
1297
+ break;
1298
+ case "\n":
1299
+ b += "\\n";
1300
+ break;
1301
+ case "\r":
1302
+ b += "\\r";
1303
+ break;
1304
+ case " ":
1305
+ b += "\\t";
1306
+ break;
1307
+ default:
1308
+ b += ch;
1309
+ }
1310
+ }
1311
+ return b + '"';
1312
+ }
1313
+
1004
1314
  // src/config.ts
1005
1315
  var Config = class _Config {
1006
1316
  constructor(root, opts) {
@@ -1306,6 +1616,36 @@ var Config = class _Config {
1306
1616
  toObject(opts) {
1307
1617
  return hoconToJs(this.root, opts?.nullPrototype === true);
1308
1618
  }
1619
+ /**
1620
+ * Renders this resolved Config as HOCON text (E18; lockstep with go.hocon's
1621
+ * `Config.RenderHOCON`, v1.11.0).
1622
+ *
1623
+ * The output round-trips: parsing it back yields the same value tree. That
1624
+ * is the correctness contract, not byte-for-byte formatting — a scalar is
1625
+ * quoted whenever leaving it bare would re-parse as a different type (a
1626
+ * string `"8080"` becomes `"8080"`, not `8080`), and left bare only when it
1627
+ * provably cannot.
1628
+ *
1629
+ * The Config must be resolved and hold only data (objects, arrays, string /
1630
+ * number / boolean / null scalars) — exactly what {@link fromMap} and the
1631
+ * format adapters produce. An unresolved placeholder throws
1632
+ * {@link ConfigError}; substitutions have no textual round trip through a
1633
+ * value tree.
1634
+ *
1635
+ * The root object's fields are emitted without enclosing braces, nested
1636
+ * objects as `key { … }`, arrays as newline-separated `[ … ]`, indented two
1637
+ * spaces. Source comments are not represented — a value tree does not carry
1638
+ * them.
1639
+ */
1640
+ renderHocon() {
1641
+ if (!this._resolved) {
1642
+ throw new ConfigError(
1643
+ "renderHocon: unrenderable unresolved substitution (config must be resolved data)",
1644
+ ""
1645
+ );
1646
+ }
1647
+ return renderHoconFromRoot(this.root);
1648
+ }
1309
1649
  /**
1310
1650
  * @internal Test-only: renders this resolved Config's value tree as canonical
1311
1651
  * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare