@o3co/ts.hocon 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -289,55 +289,6 @@ var NotResolvedError = class extends ConfigError {
289
289
  }
290
290
  };
291
291
 
292
- // src/internal/resolver/types.ts
293
- var separatorValues = /* @__PURE__ */ new WeakSet();
294
- function isSubst(v) {
295
- return v._kind === "subst-placeholder";
296
- }
297
- function isConcat(v) {
298
- return v._kind === "concat-placeholder";
299
- }
300
- function isAppend(v) {
301
- return v._kind === "append-placeholder";
302
- }
303
- function isResObj(v) {
304
- return v._kind === "res-obj";
305
- }
306
- function makeResObj() {
307
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
308
- }
309
- function mergeUnresolved(receiver, fallback) {
310
- const result = makeResObj();
311
- for (const [k, v] of fallback.fields) {
312
- result.fields.set(k, v);
313
- }
314
- for (const [k, v] of fallback.priorValues) {
315
- result.priorValues.set(k, v);
316
- }
317
- for (const [k, rv] of receiver.fields) {
318
- const existing = result.fields.get(k);
319
- if (existing !== void 0) {
320
- const recPrior = receiver.priorValues.get(k);
321
- if (recPrior !== void 0 && !isResObj(recPrior)) {
322
- result.fields.set(k, rv);
323
- continue;
324
- }
325
- if (isResObj(rv) && isResObj(existing)) {
326
- result.fields.set(k, mergeUnresolved(rv, existing));
327
- continue;
328
- }
329
- result.priorValues.set(k, existing);
330
- }
331
- result.fields.set(k, rv);
332
- }
333
- for (const [k, v] of receiver.priorValues) {
334
- if (receiver.fields.has(k)) {
335
- result.priorValues.set(k, v);
336
- }
337
- }
338
- return result;
339
- }
340
-
341
292
  // src/internal/resolver/fold-self-ref.ts
342
293
  function substFullKey(s) {
343
294
  return stringSegmentsToKey(s.segments.map((seg) => seg.text));
@@ -351,11 +302,8 @@ function stringSegmentsToKey(segments) {
351
302
  }).join(".");
352
303
  }
353
304
  function containsSelfRef(v, fullKey) {
354
- if (isSubst(v)) return substFullKey(v) === fullKey;
305
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
355
306
  if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
356
- if (isAppend(v)) {
357
- return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
358
- }
359
307
  if (isResObj(v)) {
360
308
  for (const f of v.fields.values()) {
361
309
  if (containsSelfRef(f, fullKey)) return true;
@@ -386,19 +334,12 @@ function foldSelfRef(v, fullKey, replacement) {
386
334
  col: v.col
387
335
  };
388
336
  }
389
- if (isAppend(v)) {
390
- return {
391
- _kind: "append-placeholder",
392
- existing: foldSelfRef(v.existing, fullKey, replacement),
393
- elem: foldSelfRef(v.elem, fullKey, replacement)
394
- };
395
- }
396
337
  if (isResObj(v)) {
397
338
  const newFields = /* @__PURE__ */ new Map();
398
339
  for (const [k, val] of v.fields) {
399
340
  newFields.set(k, foldSelfRef(val, fullKey, replacement));
400
341
  }
401
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
342
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
402
343
  }
403
344
  const hv = v;
404
345
  if (hv.kind === "array") {
@@ -416,17 +357,106 @@ function foldSelfRef(v, fullKey, replacement) {
416
357
  }
417
358
  return v;
418
359
  }
360
+ function foldKnownAbsentSelfRef(v, fullKey, replacement) {
361
+ if (isSubst(v)) {
362
+ return v.knownAbsent && substFullKey(v) === fullKey ? replacement : v;
363
+ }
364
+ if (isConcat(v)) {
365
+ return {
366
+ _kind: "concat-placeholder",
367
+ nodes: v.nodes.map((n) => foldKnownAbsentSelfRef(n, fullKey, replacement)),
368
+ line: v.line,
369
+ col: v.col
370
+ };
371
+ }
372
+ if (isResObj(v)) {
373
+ const newFields = /* @__PURE__ */ new Map();
374
+ for (const [k, val] of v.fields) {
375
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
376
+ }
377
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
378
+ }
379
+ const hv = v;
380
+ if (hv.kind === "array") {
381
+ return {
382
+ kind: "array",
383
+ items: hv.items.map((item) => foldKnownAbsentSelfRef(item, fullKey, replacement))
384
+ };
385
+ }
386
+ if (hv.kind === "object") {
387
+ const newFields = /* @__PURE__ */ new Map();
388
+ for (const [k, val] of hv.fields) {
389
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
390
+ }
391
+ return { kind: "object", fields: newFields };
392
+ }
393
+ return v;
394
+ }
419
395
  function foldOrSkipPrior(prior, fullKey, old) {
420
396
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
421
- if (old === void 0) return void 0;
397
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
422
398
  return foldSelfRef(prior, fullKey, old);
423
399
  }
400
+ function foldOptionalSelfRefAbsent(v, fullKey) {
401
+ if (isSubst(v) && substFullKey(v) === fullKey) {
402
+ if (!v.optional) return void 0;
403
+ return {
404
+ _kind: "subst-placeholder",
405
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
406
+ optional: v.optional,
407
+ knownAbsent: true,
408
+ listSuffix: v.listSuffix,
409
+ line: v.line,
410
+ col: v.col,
411
+ prefixLen: v.prefixLen
412
+ };
413
+ }
414
+ if (isConcat(v)) {
415
+ const nodes = [];
416
+ for (const node of v.nodes) {
417
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
418
+ if (folded === void 0) return void 0;
419
+ nodes.push(folded);
420
+ }
421
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
422
+ }
423
+ if (isResObj(v)) {
424
+ const fields = /* @__PURE__ */ new Map();
425
+ for (const [key, value] of v.fields) {
426
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
427
+ if (folded === void 0) return void 0;
428
+ fields.set(key, folded);
429
+ }
430
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
431
+ }
432
+ const hv = v;
433
+ if (hv.kind === "array") {
434
+ const items = [];
435
+ for (const item of hv.items) {
436
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
437
+ if (folded === void 0) return void 0;
438
+ items.push(folded);
439
+ }
440
+ return { kind: "array", items };
441
+ }
442
+ if (hv.kind === "object") {
443
+ const fields = /* @__PURE__ */ new Map();
444
+ for (const [key, value] of hv.fields) {
445
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
446
+ if (folded === void 0) return void 0;
447
+ fields.set(key, folded);
448
+ }
449
+ return { kind: "object", fields };
450
+ }
451
+ return cloneResolverValue(v);
452
+ }
424
453
  function cloneResolverValue(v) {
425
454
  if (isSubst(v)) {
426
455
  return {
427
456
  _kind: "subst-placeholder",
428
457
  segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
429
458
  optional: v.optional,
459
+ knownAbsent: v.knownAbsent,
430
460
  listSuffix: v.listSuffix,
431
461
  line: v.line,
432
462
  col: v.col,
@@ -446,14 +476,7 @@ function cloneResolverValue(v) {
446
476
  for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
447
477
  const newPriors = /* @__PURE__ */ new Map();
448
478
  for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
449
- return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
450
- }
451
- if (isAppend(v)) {
452
- return {
453
- _kind: "append-placeholder",
454
- existing: cloneResolverValue(v.existing),
455
- elem: cloneResolverValue(v.elem)
456
- };
479
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors, resetKeys: new Set(v.resetKeys) };
457
480
  }
458
481
  const hv = v;
459
482
  if (hv.kind === "array") {
@@ -488,14 +511,11 @@ function foldNestedSelfRefs(v, pathPrefix) {
488
511
  }
489
512
  newFields.set(k, finalVal);
490
513
  }
491
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
514
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
492
515
  }
493
516
  function containsSubstByPath(v, target) {
494
- if (isSubst(v)) return segmentsTextEqual(v.segments, target);
517
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
495
518
  if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
496
- if (isAppend(v)) {
497
- return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
498
- }
499
519
  if (isResObj(v)) {
500
520
  for (const f of v.fields.values()) {
501
521
  if (containsSubstByPath(f, target)) return true;
@@ -524,6 +544,53 @@ function segmentsTextEqual(a, b) {
524
544
  return true;
525
545
  }
526
546
 
547
+ // src/internal/resolver/types.ts
548
+ var separatorValues = /* @__PURE__ */ new WeakSet();
549
+ function isSubst(v) {
550
+ return v._kind === "subst-placeholder";
551
+ }
552
+ function isConcat(v) {
553
+ return v._kind === "concat-placeholder";
554
+ }
555
+ function isResObj(v) {
556
+ return v._kind === "res-obj";
557
+ }
558
+ function makeResObj() {
559
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map(), resetKeys: /* @__PURE__ */ new Set() };
560
+ }
561
+ function mergeUnresolved(receiver, fallback) {
562
+ const result = makeResObj();
563
+ for (const [k, v] of fallback.fields) {
564
+ result.fields.set(k, v);
565
+ }
566
+ for (const [k, v] of fallback.priorValues) {
567
+ result.priorValues.set(k, v);
568
+ }
569
+ for (const [k, rv] of receiver.fields) {
570
+ const existing = result.fields.get(k);
571
+ if (existing !== void 0) {
572
+ const recPrior = receiver.priorValues.get(k);
573
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
574
+ result.fields.set(k, rv);
575
+ continue;
576
+ }
577
+ if (isResObj(rv) && isResObj(existing)) {
578
+ result.fields.set(k, mergeUnresolved(rv, existing));
579
+ continue;
580
+ }
581
+ const foldedPrior = foldOrSkipPrior(existing, k, void 0);
582
+ if (foldedPrior !== void 0) result.priorValues.set(k, foldedPrior);
583
+ }
584
+ result.fields.set(k, rv);
585
+ }
586
+ for (const [k, v] of receiver.priorValues) {
587
+ if (receiver.fields.has(k)) {
588
+ result.priorValues.set(k, v);
589
+ }
590
+ }
591
+ return result;
592
+ }
593
+
527
594
  // src/internal/resolver/utils.ts
528
595
  function segmentsToKey(segments) {
529
596
  return segments.map((s) => {
@@ -577,9 +644,19 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
577
644
  deepMergeResObjInto(dstVal, srcVal, childPrefix);
578
645
  } else {
579
646
  if (dstVal !== void 0) {
580
- const priorExisting = dst.priorValues.get(k);
581
- const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
582
- if (prior !== void 0) dst.priorValues.set(k, prior);
647
+ if (src.resetKeys.has(k)) {
648
+ dst.priorValues.delete(k);
649
+ } else {
650
+ const dstFolded = foldOrSkipPrior(dstVal, fullKey, dst.priorValues.get(k));
651
+ if (dstFolded !== void 0) {
652
+ const srcPrior = src.priorValues.get(k);
653
+ if (srcPrior !== void 0) {
654
+ dst.priorValues.set(k, foldKnownAbsentSelfRef(srcPrior, fullKey, dstFolded));
655
+ } else {
656
+ dst.priorValues.set(k, dstFolded);
657
+ }
658
+ }
659
+ }
583
660
  }
584
661
  dst.fields.set(k, srcVal);
585
662
  }
@@ -589,6 +666,7 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
589
666
  dst.priorValues.set(k, srcPrior);
590
667
  }
591
668
  }
669
+ for (const k of src.resetKeys) dst.resetKeys.add(k);
592
670
  }
593
671
  function hoconValueToResObj(hv) {
594
672
  const obj = makeResObj();
@@ -658,6 +736,11 @@ var Lexer = class {
658
736
  this.line = 1;
659
737
  this.col = 1;
660
738
  this.hadSpace = false;
739
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
740
+ // token push. Used so the parser can preserve path-expression whitespace
741
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
742
+ // Comment text is NOT accumulated; only the actual WS chars.
743
+ this.whitespaceBuffer = "";
661
744
  this.tokens = [];
662
745
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
663
746
  }
@@ -673,6 +756,7 @@ var Lexer = class {
673
756
  continue;
674
757
  }
675
758
  if (isHoconWhitespace(ch)) {
759
+ this.whitespaceBuffer += ch;
676
760
  this.advance();
677
761
  this.hadSpace = true;
678
762
  continue;
@@ -764,7 +848,7 @@ var Lexer = class {
764
848
  }
765
849
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
766
850
  }
767
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
851
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
768
852
  return this.tokens;
769
853
  }
770
854
  peek(offset = 0) {
@@ -781,12 +865,14 @@ var Lexer = class {
781
865
  return ch;
782
866
  }
783
867
  push(kind, value, l, c, isQuoted = false) {
784
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
868
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
785
869
  this.hadSpace = false;
870
+ this.whitespaceBuffer = "";
786
871
  }
787
872
  pushSubst(payload, value, l, c) {
788
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
873
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
789
874
  this.hadSpace = false;
875
+ this.whitespaceBuffer = "";
790
876
  }
791
877
  /**
792
878
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -1032,7 +1118,7 @@ function isUnquotedContinue(ch, nextFn) {
1032
1118
  }
1033
1119
 
1034
1120
  // src/internal/parser/parser.ts
1035
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1121
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
1036
1122
  var Parser = class {
1037
1123
  constructor(tokens) {
1038
1124
  this.tokens = tokens;
@@ -1145,13 +1231,17 @@ var Parser = class {
1145
1231
  let trailingDot = false;
1146
1232
  let firstWasQuoted = false;
1147
1233
  let spaceConcat = false;
1234
+ let postDotPrefix = "";
1148
1235
  while (true) {
1149
1236
  const t = this.peek();
1150
1237
  if (t.kind === "string") {
1151
1238
  this.advance();
1152
1239
  if (segments.length === 0) firstWasQuoted = true;
1153
1240
  if (spaceConcat) {
1154
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1241
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1242
+ } else if (postDotPrefix !== "") {
1243
+ segments.push(`${postDotPrefix}${t.value}`);
1244
+ postDotPrefix = "";
1155
1245
  } else {
1156
1246
  segments.push(t.value);
1157
1247
  }
@@ -1161,24 +1251,23 @@ var Parser = class {
1161
1251
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1162
1252
  const parts = raw.split(".");
1163
1253
  const filtered = parts.filter((s) => s.length > 0);
1164
- for (const seg of filtered) {
1165
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
1166
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
1167
- throw new ParseError(
1168
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
1169
- t.line,
1170
- t.col
1171
- );
1172
- }
1173
- }
1174
- if (spaceConcat && filtered.length > 0) {
1254
+ if (spaceConcat) {
1255
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
1175
1256
  if (raw.startsWith(".")) {
1176
1257
  segments.push(...filtered);
1177
- } else {
1258
+ } else if (filtered.length > 0) {
1178
1259
  const [head, ...tail] = filtered;
1179
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1260
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
1180
1261
  segments.push(...tail);
1181
1262
  }
1263
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1264
+ segments.push(postDotPrefix);
1265
+ postDotPrefix = "";
1266
+ segments.push(...filtered);
1267
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1268
+ const [head, ...tail] = filtered;
1269
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1270
+ postDotPrefix = "";
1182
1271
  } else {
1183
1272
  segments.push(...filtered);
1184
1273
  }
@@ -1188,11 +1277,23 @@ var Parser = class {
1188
1277
  break;
1189
1278
  }
1190
1279
  spaceConcat = false;
1191
- if (trailingDot) continue;
1280
+ if (trailingDot) {
1281
+ const next2 = this.peek();
1282
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1283
+ postDotPrefix = next2.precedingWhitespace;
1284
+ } else {
1285
+ postDotPrefix = "";
1286
+ }
1287
+ continue;
1288
+ }
1192
1289
  const next = this.peek();
1193
1290
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
1194
1291
  this.advance();
1195
1292
  trailingDot = true;
1293
+ const afterDot = this.peek();
1294
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1295
+ postDotPrefix = afterDot.precedingWhitespace;
1296
+ }
1196
1297
  continue;
1197
1298
  }
1198
1299
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1205,6 +1306,14 @@ var Parser = class {
1205
1306
  }
1206
1307
  break;
1207
1308
  }
1309
+ if (trailingDot) {
1310
+ const here = this.peek();
1311
+ throw new ParseError(
1312
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1313
+ here.line,
1314
+ here.col
1315
+ );
1316
+ }
1208
1317
  return { segments, firstWasQuoted };
1209
1318
  }
1210
1319
  parseInclude() {
@@ -1377,7 +1486,8 @@ var Parser = class {
1377
1486
  break;
1378
1487
  }
1379
1488
  if (hadSpace) {
1380
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1489
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1490
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1381
1491
  }
1382
1492
  parts.push(node);
1383
1493
  }
@@ -1813,35 +1923,19 @@ var StructureBuilder = class _StructureBuilder {
1813
1923
  const childPrefix = [...pathPrefix, head];
1814
1924
  const fullKey = stringSegmentsToKey(childPrefix);
1815
1925
  if (field.append) {
1816
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1817
- const oldPrior = obj.priorValues.get(head);
1818
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1819
- if (folded !== void 0) obj.priorValues.set(head, folded);
1820
- obj.fields.set(head, {
1821
- _kind: "append-placeholder",
1822
- // Use the folded existing (self-ref-free) for the Append. When the
1823
- // fold returned undefined (self-ref present but no old prior),
1824
- // fall back to the original existing — resolveSubst will then
1825
- // raise the "self-ref with no prior value" error at resolve time,
1826
- // matching the pre-fix behaviour for that edge case.
1827
- //
1828
- // Clone `folded` again so the Append.existing and priorValues[head]
1829
- // do not share the same Subst/Concat references — relativizeResObj
1830
- // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1831
- // in place, so a shared reference would get its prefix applied
1832
- // twice (resulting in e.g. `${outer.outer.base}` instead of
1833
- // `${outer.base}`). Codex review on PR #131.
1834
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1835
- elem: this.astToResolverValue(field.value, childPrefix)
1836
- });
1926
+ const synthetic = this.desugarAppend(field, childPrefix);
1927
+ this.applyField(obj, synthetic, pathPrefix);
1837
1928
  return;
1838
1929
  }
1839
1930
  const existing = obj.fields.get(head);
1840
1931
  const newVal = this.astToResolverValue(field.value, childPrefix);
1932
+ if (!containsSelfRef(newVal, fullKey)) {
1933
+ obj.resetKeys.add(head);
1934
+ }
1841
1935
  if (existing !== void 0) {
1842
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1843
1936
  const oldPrior = obj.priorValues.get(head);
1844
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1937
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1938
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1845
1939
  if (prior !== void 0) obj.priorValues.set(head, prior);
1846
1940
  }
1847
1941
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1879,23 +1973,19 @@ var StructureBuilder = class _StructureBuilder {
1879
1973
  const childPrefix = [...pathPrefix, head];
1880
1974
  const fullKey = stringSegmentsToKey(childPrefix);
1881
1975
  if (field.append) {
1882
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1883
- const oldPrior = obj.priorValues.get(head);
1884
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1885
- if (folded !== void 0) obj.priorValues.set(head, folded);
1886
- obj.fields.set(head, {
1887
- _kind: "append-placeholder",
1888
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1889
- elem: await this.astToResolverValueAsync(field.value, childPrefix)
1890
- });
1976
+ const synthetic = this.desugarAppend(field, childPrefix);
1977
+ await this.applyFieldAsync(obj, synthetic, pathPrefix);
1891
1978
  return;
1892
1979
  }
1893
1980
  const existing = obj.fields.get(head);
1894
1981
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1982
+ if (!containsSelfRef(newVal, fullKey)) {
1983
+ obj.resetKeys.add(head);
1984
+ }
1895
1985
  if (existing !== void 0) {
1896
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1897
1986
  const oldPrior = obj.priorValues.get(head);
1898
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1987
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1988
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1899
1989
  if (prior !== void 0) obj.priorValues.set(head, prior);
1900
1990
  }
1901
1991
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1904,6 +1994,21 @@ var StructureBuilder = class _StructureBuilder {
1904
1994
  }
1905
1995
  obj.fields.set(head, newVal);
1906
1996
  }
1997
+ /** Builds the `key = ${?fullkey} [value]` field that `key += value` desugars
1998
+ * to (S13b.2, HOCON.md L732). `childPrefix` is the field's fully-qualified
1999
+ * path (pathPrefix + key), so a nested `srv { items += x }` references
2000
+ * `${?srv.items}`; include relativization rewrites it under a mount prefix.
2001
+ * `field.key` is already single-segment here (multi-segment keys are split
2002
+ * into nested objects upstream). Returns a NEW AstField (append=false);
2003
+ * positions inherit from the original field so resolve-time errors (the
2004
+ * S13b.2 non-array check, now via the scalar+array concat) keep a location. */
2005
+ desugarAppend(field, childPrefix) {
2006
+ const segments = childPrefix.map((text) => ({ text, line: field.pos.line, col: field.pos.col }));
2007
+ const subst = { kind: "subst", segments, optional: true, listSuffix: false, pos: field.pos };
2008
+ const elemArray = { kind: "array", items: [field.value], pos: field.pos };
2009
+ const synthetic = { kind: "concat", nodes: [subst, elemArray], pos: field.pos };
2010
+ return { key: field.key, value: synthetic, append: false, pos: field.pos };
2011
+ }
1907
2012
  astToResolverValue(ast, pathPrefix) {
1908
2013
  switch (ast.kind) {
1909
2014
  case "scalar": {
@@ -1918,7 +2023,7 @@ var StructureBuilder = class _StructureBuilder {
1918
2023
  return inner;
1919
2024
  }
1920
2025
  case "subst":
1921
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2026
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, knownAbsent: false, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1922
2027
  case "concat":
1923
2028
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1924
2029
  case "include":
@@ -1942,7 +2047,7 @@ var StructureBuilder = class _StructureBuilder {
1942
2047
  case "object":
1943
2048
  return await this.buildAsync(ast, pathPrefix);
1944
2049
  case "subst":
1945
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2050
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, knownAbsent: false, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1946
2051
  case "concat": {
1947
2052
  const nodes = [];
1948
2053
  for (const n of ast.nodes) {
@@ -1968,11 +2073,6 @@ var StructureBuilder = class _StructureBuilder {
1968
2073
  }
1969
2074
  return;
1970
2075
  }
1971
- if (isAppend(val)) {
1972
- this.relativizeSubstPaths(val.existing, prefixSegments);
1973
- this.relativizeSubstPaths(val.elem, prefixSegments);
1974
- return;
1975
- }
1976
2076
  if (isResObj(val)) {
1977
2077
  this.relativizeResObj(val, prefixSegments);
1978
2078
  return;
@@ -2060,6 +2160,8 @@ var SubstitutionResolver = class {
2060
2160
  const result = /* @__PURE__ */ new Map();
2061
2161
  for (const [key, val] of obj.fields) {
2062
2162
  this.resolvingFieldPath.push(key);
2163
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2164
+ this.cache.delete(fullCacheKey);
2063
2165
  let resolved;
2064
2166
  try {
2065
2167
  resolved = this.resolveVal(val, obj);
@@ -2072,28 +2174,42 @@ var SubstitutionResolver = class {
2072
2174
  if (prior !== void 0) {
2073
2175
  const priorResolved = this.resolveVal(prior, obj);
2074
2176
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
2075
- result.set(
2076
- key,
2077
- deepMergeHoconValues(
2078
- priorResolved,
2079
- resolved
2080
- )
2177
+ const finalValue = deepMergeHoconValues(
2178
+ priorResolved,
2179
+ resolved
2081
2180
  );
2181
+ result.set(key, finalValue);
2182
+ this.cache.set(fullCacheKey, finalValue);
2183
+ this.cacheDescendants(fullCacheKey, finalValue);
2082
2184
  continue;
2083
2185
  }
2084
2186
  }
2085
2187
  }
2086
2188
  result.set(key, resolved);
2189
+ this.cache.set(fullCacheKey, resolved);
2190
+ this.cacheDescendants(fullCacheKey, resolved);
2087
2191
  } else {
2088
2192
  const prior = obj.priorValues.get(key);
2089
2193
  if (prior !== void 0) {
2090
2194
  const priorResolved = this.resolveVal(prior, obj);
2091
- if (priorResolved !== void 0) result.set(key, priorResolved);
2195
+ if (priorResolved !== void 0) {
2196
+ result.set(key, priorResolved);
2197
+ this.cache.set(fullCacheKey, priorResolved);
2198
+ this.cacheDescendants(fullCacheKey, priorResolved);
2199
+ }
2092
2200
  }
2093
2201
  }
2094
2202
  }
2095
2203
  return { kind: "object", fields: result };
2096
2204
  }
2205
+ cacheDescendants(prefix, value) {
2206
+ if (value.kind !== "object") return;
2207
+ for (const [key, child] of value.fields) {
2208
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2209
+ this.cache.set(childKey, child);
2210
+ this.cacheDescendants(childKey, child);
2211
+ }
2212
+ }
2097
2213
  resolveVal(v, scope) {
2098
2214
  if (isSubst(v)) return this.resolveSubst(v, scope);
2099
2215
  if (isConcat(v)) {
@@ -2105,7 +2221,6 @@ var SubstitutionResolver = class {
2105
2221
  this.resolvingConcats.delete(v);
2106
2222
  }
2107
2223
  }
2108
- if (isAppend(v)) return this.resolveAppend(v, scope);
2109
2224
  if (isResObj(v)) return this.resolveResObj(v);
2110
2225
  const hv = v;
2111
2226
  if (hv.kind === "array") {
@@ -2119,6 +2234,7 @@ var SubstitutionResolver = class {
2119
2234
  return hv;
2120
2235
  }
2121
2236
  resolveSubst(s, scope) {
2237
+ if (s.knownAbsent) return void 0;
2122
2238
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2123
2239
  if (this.cache.has(key)) return this.cache.get(key);
2124
2240
  if (this.resolving.has(key)) {
@@ -2211,6 +2327,28 @@ var SubstitutionResolver = class {
2211
2327
  if (result !== void 0) this.cache.set(key, result);
2212
2328
  return result;
2213
2329
  }
2330
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2331
+ const originalSegments = s.segments.slice(s.prefixLen);
2332
+ const fallbackFound = lookupPath(this.root, originalSegments);
2333
+ if (fallbackFound !== void 0) {
2334
+ let result = this.resolveVal(fallbackFound, scope);
2335
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2336
+ const rootSeg = originalSegments[0]?.text ?? "";
2337
+ const prior = this.root.priorValues.get(rootSeg);
2338
+ if (prior !== void 0) {
2339
+ const priorResolved = this.resolveVal(prior, scope);
2340
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2341
+ result = deepMergeHoconValues(
2342
+ priorResolved,
2343
+ result
2344
+ );
2345
+ }
2346
+ }
2347
+ }
2348
+ if (result !== void 0) this.cache.set(key, result);
2349
+ return result;
2350
+ }
2351
+ }
2214
2352
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
2215
2353
  const result = this.resolveEnvList(s);
2216
2354
  if (result !== void 0) {
@@ -2367,38 +2505,6 @@ var SubstitutionResolver = class {
2367
2505
  }
2368
2506
  return folded;
2369
2507
  }
2370
- resolveAppend(a, scope) {
2371
- const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
2372
- if (this.opts.allowUnresolved && (isSubst(existing) || isConcat(existing) || isAppend(existing))) {
2373
- return a;
2374
- }
2375
- const elem = this.resolveVal(a.elem, scope);
2376
- let priorItems;
2377
- if (existing.kind === "array") {
2378
- priorItems = existing.items;
2379
- } else if (existing.kind === "object") {
2380
- const converted = numericObjectToArray(existing);
2381
- if (converted === null) {
2382
- throw new ResolveError(
2383
- `'+=' on non-array value: prior value is object with non-numeric keys (spec L732)`,
2384
- "",
2385
- 0,
2386
- 0
2387
- );
2388
- }
2389
- priorItems = converted;
2390
- } else {
2391
- throw new ResolveError(
2392
- `'+=' on non-array value: prior value is ${existing.kind} (spec L732)`,
2393
- "",
2394
- 0,
2395
- 0
2396
- );
2397
- }
2398
- const items = [...priorItems];
2399
- if (elem !== void 0) items.push(elem);
2400
- return { kind: "array", items };
2401
- }
2402
2508
  };
2403
2509
 
2404
2510
  // src/internal/resolver/resolver.ts
@@ -2416,7 +2522,7 @@ function containsPlaceholders(tree) {
2416
2522
  }
2417
2523
  function valContainsPlaceholders(v) {
2418
2524
  const rv = v;
2419
- if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2525
+ if (isSubst(rv) || isConcat(rv)) return true;
2420
2526
  if (isResObj(rv)) return containsPlaceholders(rv);
2421
2527
  const hv = v;
2422
2528
  if (hv?.kind === "array") {
@@ -2427,7 +2533,7 @@ function valContainsPlaceholders(v) {
2427
2533
  function buildPartialHoconFromResObj(tree) {
2428
2534
  const fields = /* @__PURE__ */ new Map();
2429
2535
  for (const [k, v] of tree.fields) {
2430
- if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2536
+ if (!isSubst(v) && !isConcat(v)) {
2431
2537
  if (isResObj(v)) {
2432
2538
  fields.set(k, buildPartialHoconFromResObj(v));
2433
2539
  } else {
@@ -2820,7 +2926,7 @@ function hoconToJs(v) {
2820
2926
  function resObjToKeyShape(tree) {
2821
2927
  const fields = /* @__PURE__ */ new Map();
2822
2928
  for (const [k, v] of tree.fields) {
2823
- if (isSubst(v) || isConcat(v) || isAppend(v)) {
2929
+ if (isSubst(v) || isConcat(v)) {
2824
2930
  fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2825
2931
  } else if (isResObj(v)) {
2826
2932
  fields.set(k, resObjToKeyShape(v));
@@ -2834,7 +2940,7 @@ function stripPlaceholderFields(v) {
2834
2940
  const fields = /* @__PURE__ */ new Map();
2835
2941
  let hadPlaceholders = false;
2836
2942
  for (const [k, val] of v.fields) {
2837
- if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2943
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder") {
2838
2944
  hadPlaceholders = true;
2839
2945
  } else if (val.kind === "object") {
2840
2946
  const inner = stripPlaceholderFields(val);