@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/zod.js CHANGED
@@ -241,55 +241,6 @@ var NotResolvedError = class extends ConfigError {
241
241
  }
242
242
  };
243
243
 
244
- // src/internal/resolver/types.ts
245
- var separatorValues = /* @__PURE__ */ new WeakSet();
246
- function isSubst(v) {
247
- return v._kind === "subst-placeholder";
248
- }
249
- function isConcat(v) {
250
- return v._kind === "concat-placeholder";
251
- }
252
- function isAppend(v) {
253
- return v._kind === "append-placeholder";
254
- }
255
- function isResObj(v) {
256
- return v._kind === "res-obj";
257
- }
258
- function makeResObj() {
259
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
260
- }
261
- function mergeUnresolved(receiver, fallback) {
262
- const result = makeResObj();
263
- for (const [k, v] of fallback.fields) {
264
- result.fields.set(k, v);
265
- }
266
- for (const [k, v] of fallback.priorValues) {
267
- result.priorValues.set(k, v);
268
- }
269
- for (const [k, rv] of receiver.fields) {
270
- const existing = result.fields.get(k);
271
- if (existing !== void 0) {
272
- const recPrior = receiver.priorValues.get(k);
273
- if (recPrior !== void 0 && !isResObj(recPrior)) {
274
- result.fields.set(k, rv);
275
- continue;
276
- }
277
- if (isResObj(rv) && isResObj(existing)) {
278
- result.fields.set(k, mergeUnresolved(rv, existing));
279
- continue;
280
- }
281
- result.priorValues.set(k, existing);
282
- }
283
- result.fields.set(k, rv);
284
- }
285
- for (const [k, v] of receiver.priorValues) {
286
- if (receiver.fields.has(k)) {
287
- result.priorValues.set(k, v);
288
- }
289
- }
290
- return result;
291
- }
292
-
293
244
  // src/internal/resolver/fold-self-ref.ts
294
245
  function substFullKey(s) {
295
246
  return stringSegmentsToKey(s.segments.map((seg) => seg.text));
@@ -303,11 +254,8 @@ function stringSegmentsToKey(segments) {
303
254
  }).join(".");
304
255
  }
305
256
  function containsSelfRef(v, fullKey) {
306
- if (isSubst(v)) return substFullKey(v) === fullKey;
257
+ if (isSubst(v)) return !v.knownAbsent && substFullKey(v) === fullKey;
307
258
  if (isConcat(v)) return v.nodes.some((n) => containsSelfRef(n, fullKey));
308
- if (isAppend(v)) {
309
- return containsSelfRef(v.existing, fullKey) || containsSelfRef(v.elem, fullKey);
310
- }
311
259
  if (isResObj(v)) {
312
260
  for (const f of v.fields.values()) {
313
261
  if (containsSelfRef(f, fullKey)) return true;
@@ -338,19 +286,12 @@ function foldSelfRef(v, fullKey, replacement) {
338
286
  col: v.col
339
287
  };
340
288
  }
341
- if (isAppend(v)) {
342
- return {
343
- _kind: "append-placeholder",
344
- existing: foldSelfRef(v.existing, fullKey, replacement),
345
- elem: foldSelfRef(v.elem, fullKey, replacement)
346
- };
347
- }
348
289
  if (isResObj(v)) {
349
290
  const newFields = /* @__PURE__ */ new Map();
350
291
  for (const [k, val] of v.fields) {
351
292
  newFields.set(k, foldSelfRef(val, fullKey, replacement));
352
293
  }
353
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
294
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
354
295
  }
355
296
  const hv = v;
356
297
  if (hv.kind === "array") {
@@ -368,17 +309,106 @@ function foldSelfRef(v, fullKey, replacement) {
368
309
  }
369
310
  return v;
370
311
  }
312
+ function foldKnownAbsentSelfRef(v, fullKey, replacement) {
313
+ if (isSubst(v)) {
314
+ return v.knownAbsent && substFullKey(v) === fullKey ? replacement : v;
315
+ }
316
+ if (isConcat(v)) {
317
+ return {
318
+ _kind: "concat-placeholder",
319
+ nodes: v.nodes.map((n) => foldKnownAbsentSelfRef(n, fullKey, replacement)),
320
+ line: v.line,
321
+ col: v.col
322
+ };
323
+ }
324
+ if (isResObj(v)) {
325
+ const newFields = /* @__PURE__ */ new Map();
326
+ for (const [k, val] of v.fields) {
327
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
328
+ }
329
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
330
+ }
331
+ const hv = v;
332
+ if (hv.kind === "array") {
333
+ return {
334
+ kind: "array",
335
+ items: hv.items.map((item) => foldKnownAbsentSelfRef(item, fullKey, replacement))
336
+ };
337
+ }
338
+ if (hv.kind === "object") {
339
+ const newFields = /* @__PURE__ */ new Map();
340
+ for (const [k, val] of hv.fields) {
341
+ newFields.set(k, foldKnownAbsentSelfRef(val, fullKey, replacement));
342
+ }
343
+ return { kind: "object", fields: newFields };
344
+ }
345
+ return v;
346
+ }
371
347
  function foldOrSkipPrior(prior, fullKey, old) {
372
348
  if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
373
- if (old === void 0) return void 0;
349
+ if (old === void 0) return foldOptionalSelfRefAbsent(prior, fullKey);
374
350
  return foldSelfRef(prior, fullKey, old);
375
351
  }
352
+ function foldOptionalSelfRefAbsent(v, fullKey) {
353
+ if (isSubst(v) && substFullKey(v) === fullKey) {
354
+ if (!v.optional) return void 0;
355
+ return {
356
+ _kind: "subst-placeholder",
357
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
358
+ optional: v.optional,
359
+ knownAbsent: true,
360
+ listSuffix: v.listSuffix,
361
+ line: v.line,
362
+ col: v.col,
363
+ prefixLen: v.prefixLen
364
+ };
365
+ }
366
+ if (isConcat(v)) {
367
+ const nodes = [];
368
+ for (const node of v.nodes) {
369
+ const folded = foldOptionalSelfRefAbsent(node, fullKey);
370
+ if (folded === void 0) return void 0;
371
+ nodes.push(folded);
372
+ }
373
+ return { _kind: "concat-placeholder", nodes, line: v.line, col: v.col };
374
+ }
375
+ if (isResObj(v)) {
376
+ const fields = /* @__PURE__ */ new Map();
377
+ for (const [key, value] of v.fields) {
378
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
379
+ if (folded === void 0) return void 0;
380
+ fields.set(key, folded);
381
+ }
382
+ return { _kind: "res-obj", fields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
383
+ }
384
+ const hv = v;
385
+ if (hv.kind === "array") {
386
+ const items = [];
387
+ for (const item of hv.items) {
388
+ const folded = foldOptionalSelfRefAbsent(item, fullKey);
389
+ if (folded === void 0) return void 0;
390
+ items.push(folded);
391
+ }
392
+ return { kind: "array", items };
393
+ }
394
+ if (hv.kind === "object") {
395
+ const fields = /* @__PURE__ */ new Map();
396
+ for (const [key, value] of hv.fields) {
397
+ const folded = foldOptionalSelfRefAbsent(value, fullKey);
398
+ if (folded === void 0) return void 0;
399
+ fields.set(key, folded);
400
+ }
401
+ return { kind: "object", fields };
402
+ }
403
+ return cloneResolverValue(v);
404
+ }
376
405
  function cloneResolverValue(v) {
377
406
  if (isSubst(v)) {
378
407
  return {
379
408
  _kind: "subst-placeholder",
380
409
  segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
381
410
  optional: v.optional,
411
+ knownAbsent: v.knownAbsent,
382
412
  listSuffix: v.listSuffix,
383
413
  line: v.line,
384
414
  col: v.col,
@@ -398,14 +428,7 @@ function cloneResolverValue(v) {
398
428
  for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
399
429
  const newPriors = /* @__PURE__ */ new Map();
400
430
  for (const [k, val] of v.priorValues) newPriors.set(k, cloneResolverValue(val));
401
- return { _kind: "res-obj", fields: newFields, priorValues: newPriors };
402
- }
403
- if (isAppend(v)) {
404
- return {
405
- _kind: "append-placeholder",
406
- existing: cloneResolverValue(v.existing),
407
- elem: cloneResolverValue(v.elem)
408
- };
431
+ return { _kind: "res-obj", fields: newFields, priorValues: newPriors, resetKeys: new Set(v.resetKeys) };
409
432
  }
410
433
  const hv = v;
411
434
  if (hv.kind === "array") {
@@ -440,14 +463,11 @@ function foldNestedSelfRefs(v, pathPrefix) {
440
463
  }
441
464
  newFields.set(k, finalVal);
442
465
  }
443
- return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
466
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues), resetKeys: new Set(v.resetKeys) };
444
467
  }
445
468
  function containsSubstByPath(v, target) {
446
- if (isSubst(v)) return segmentsTextEqual(v.segments, target);
469
+ if (isSubst(v)) return !v.knownAbsent && segmentsTextEqual(v.segments, target);
447
470
  if (isConcat(v)) return v.nodes.some((n) => containsSubstByPath(n, target));
448
- if (isAppend(v)) {
449
- return containsSubstByPath(v.existing, target) || containsSubstByPath(v.elem, target);
450
- }
451
471
  if (isResObj(v)) {
452
472
  for (const f of v.fields.values()) {
453
473
  if (containsSubstByPath(f, target)) return true;
@@ -476,6 +496,53 @@ function segmentsTextEqual(a, b) {
476
496
  return true;
477
497
  }
478
498
 
499
+ // src/internal/resolver/types.ts
500
+ var separatorValues = /* @__PURE__ */ new WeakSet();
501
+ function isSubst(v) {
502
+ return v._kind === "subst-placeholder";
503
+ }
504
+ function isConcat(v) {
505
+ return v._kind === "concat-placeholder";
506
+ }
507
+ function isResObj(v) {
508
+ return v._kind === "res-obj";
509
+ }
510
+ function makeResObj() {
511
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map(), resetKeys: /* @__PURE__ */ new Set() };
512
+ }
513
+ function mergeUnresolved(receiver, fallback) {
514
+ const result = makeResObj();
515
+ for (const [k, v] of fallback.fields) {
516
+ result.fields.set(k, v);
517
+ }
518
+ for (const [k, v] of fallback.priorValues) {
519
+ result.priorValues.set(k, v);
520
+ }
521
+ for (const [k, rv] of receiver.fields) {
522
+ const existing = result.fields.get(k);
523
+ if (existing !== void 0) {
524
+ const recPrior = receiver.priorValues.get(k);
525
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
526
+ result.fields.set(k, rv);
527
+ continue;
528
+ }
529
+ if (isResObj(rv) && isResObj(existing)) {
530
+ result.fields.set(k, mergeUnresolved(rv, existing));
531
+ continue;
532
+ }
533
+ const foldedPrior = foldOrSkipPrior(existing, k, void 0);
534
+ if (foldedPrior !== void 0) result.priorValues.set(k, foldedPrior);
535
+ }
536
+ result.fields.set(k, rv);
537
+ }
538
+ for (const [k, v] of receiver.priorValues) {
539
+ if (receiver.fields.has(k)) {
540
+ result.priorValues.set(k, v);
541
+ }
542
+ }
543
+ return result;
544
+ }
545
+
479
546
  // src/internal/resolver/utils.ts
480
547
  function segmentsToKey(segments) {
481
548
  return segments.map((s) => {
@@ -529,9 +596,19 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
529
596
  deepMergeResObjInto(dstVal, srcVal, childPrefix);
530
597
  } else {
531
598
  if (dstVal !== void 0) {
532
- const priorExisting = dst.priorValues.get(k);
533
- const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
534
- if (prior !== void 0) dst.priorValues.set(k, prior);
599
+ if (src.resetKeys.has(k)) {
600
+ dst.priorValues.delete(k);
601
+ } else {
602
+ const dstFolded = foldOrSkipPrior(dstVal, fullKey, dst.priorValues.get(k));
603
+ if (dstFolded !== void 0) {
604
+ const srcPrior = src.priorValues.get(k);
605
+ if (srcPrior !== void 0) {
606
+ dst.priorValues.set(k, foldKnownAbsentSelfRef(srcPrior, fullKey, dstFolded));
607
+ } else {
608
+ dst.priorValues.set(k, dstFolded);
609
+ }
610
+ }
611
+ }
535
612
  }
536
613
  dst.fields.set(k, srcVal);
537
614
  }
@@ -541,6 +618,7 @@ function deepMergeResObjInto(dst, src, pathPrefix = []) {
541
618
  dst.priorValues.set(k, srcPrior);
542
619
  }
543
620
  }
621
+ for (const k of src.resetKeys) dst.resetKeys.add(k);
544
622
  }
545
623
  function hoconValueToResObj(hv) {
546
624
  const obj = makeResObj();
@@ -610,6 +688,11 @@ var Lexer = class {
610
688
  this.line = 1;
611
689
  this.col = 1;
612
690
  this.hadSpace = false;
691
+ // Accumulates literal whitespace chars consumed between tokens. Reset on every
692
+ // token push. Used so the parser can preserve path-expression whitespace
693
+ // (xx.hocon#42, E13) — e.g. tab vs space adjacent to dots in key paths.
694
+ // Comment text is NOT accumulated; only the actual WS chars.
695
+ this.whitespaceBuffer = "";
613
696
  this.tokens = [];
614
697
  if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
615
698
  }
@@ -625,6 +708,7 @@ var Lexer = class {
625
708
  continue;
626
709
  }
627
710
  if (isHoconWhitespace(ch)) {
711
+ this.whitespaceBuffer += ch;
628
712
  this.advance();
629
713
  this.hadSpace = true;
630
714
  continue;
@@ -716,7 +800,7 @@ var Lexer = class {
716
800
  }
717
801
  throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
718
802
  }
719
- this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
803
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false, precedingWhitespace: "" });
720
804
  return this.tokens;
721
805
  }
722
806
  peek(offset = 0) {
@@ -733,12 +817,14 @@ var Lexer = class {
733
817
  return ch;
734
818
  }
735
819
  push(kind, value, l, c, isQuoted = false) {
736
- this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
820
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer });
737
821
  this.hadSpace = false;
822
+ this.whitespaceBuffer = "";
738
823
  }
739
824
  pushSubst(payload, value, l, c) {
740
- this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
825
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, precedingWhitespace: this.whitespaceBuffer, subst: payload });
741
826
  this.hadSpace = false;
827
+ this.whitespaceBuffer = "";
742
828
  }
743
829
  /**
744
830
  * Read body of a quoted string. Opening `"` must already be consumed.
@@ -984,7 +1070,7 @@ function isUnquotedContinue(ch, nextFn) {
984
1070
  }
985
1071
 
986
1072
  // src/internal/parser/parser.ts
987
- var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
1073
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, precedingWhitespace: "", subst: void 0 };
988
1074
  var Parser = class {
989
1075
  constructor(tokens) {
990
1076
  this.tokens = tokens;
@@ -1097,13 +1183,17 @@ var Parser = class {
1097
1183
  let trailingDot = false;
1098
1184
  let firstWasQuoted = false;
1099
1185
  let spaceConcat = false;
1186
+ let postDotPrefix = "";
1100
1187
  while (true) {
1101
1188
  const t = this.peek();
1102
1189
  if (t.kind === "string") {
1103
1190
  this.advance();
1104
1191
  if (segments.length === 0) firstWasQuoted = true;
1105
1192
  if (spaceConcat) {
1106
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1193
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}${t.value}`;
1194
+ } else if (postDotPrefix !== "") {
1195
+ segments.push(`${postDotPrefix}${t.value}`);
1196
+ postDotPrefix = "";
1107
1197
  } else {
1108
1198
  segments.push(t.value);
1109
1199
  }
@@ -1113,24 +1203,23 @@ var Parser = class {
1113
1203
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1114
1204
  const parts = raw.split(".");
1115
1205
  const filtered = parts.filter((s) => s.length > 0);
1116
- for (const seg of filtered) {
1117
- if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
1118
- const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
1119
- throw new ParseError(
1120
- `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
1121
- t.line,
1122
- t.col
1123
- );
1124
- }
1125
- }
1126
- if (spaceConcat && filtered.length > 0) {
1206
+ if (spaceConcat) {
1207
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
1127
1208
  if (raw.startsWith(".")) {
1128
1209
  segments.push(...filtered);
1129
- } else {
1210
+ } else if (filtered.length > 0) {
1130
1211
  const [head, ...tail] = filtered;
1131
- segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1212
+ segments[segments.length - 1] = `${segments[segments.length - 1]}${head}`;
1132
1213
  segments.push(...tail);
1133
1214
  }
1215
+ } else if (postDotPrefix !== "" && raw.startsWith(".")) {
1216
+ segments.push(postDotPrefix);
1217
+ postDotPrefix = "";
1218
+ segments.push(...filtered);
1219
+ } else if (postDotPrefix !== "" && filtered.length > 0) {
1220
+ const [head, ...tail] = filtered;
1221
+ segments.push(`${postDotPrefix}${head}`, ...tail);
1222
+ postDotPrefix = "";
1134
1223
  } else {
1135
1224
  segments.push(...filtered);
1136
1225
  }
@@ -1140,11 +1229,23 @@ var Parser = class {
1140
1229
  break;
1141
1230
  }
1142
1231
  spaceConcat = false;
1143
- if (trailingDot) continue;
1232
+ if (trailingDot) {
1233
+ const next2 = this.peek();
1234
+ if ((next2.kind === "unquoted" || next2.kind === "string") && next2.precedingWhitespace.length > 0) {
1235
+ postDotPrefix = next2.precedingWhitespace;
1236
+ } else {
1237
+ postDotPrefix = "";
1238
+ }
1239
+ continue;
1240
+ }
1144
1241
  const next = this.peek();
1145
1242
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
1146
1243
  this.advance();
1147
1244
  trailingDot = true;
1245
+ const afterDot = this.peek();
1246
+ if ((afterDot.kind === "unquoted" || afterDot.kind === "string") && afterDot.precedingWhitespace.length > 0) {
1247
+ postDotPrefix = afterDot.precedingWhitespace;
1248
+ }
1148
1249
  continue;
1149
1250
  }
1150
1251
  if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
@@ -1157,6 +1258,14 @@ var Parser = class {
1157
1258
  }
1158
1259
  break;
1159
1260
  }
1261
+ if (trailingDot) {
1262
+ const here = this.peek();
1263
+ throw new ParseError(
1264
+ `path has a trailing period '.' \u2014 empty key segment not allowed (HOCON.md path rules)`,
1265
+ here.line,
1266
+ here.col
1267
+ );
1268
+ }
1160
1269
  return { segments, firstWasQuoted };
1161
1270
  }
1162
1271
  parseInclude() {
@@ -1329,7 +1438,8 @@ var Parser = class {
1329
1438
  break;
1330
1439
  }
1331
1440
  if (hadSpace) {
1332
- parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1441
+ const sep = t.precedingWhitespace.length > 0 ? t.precedingWhitespace : " ";
1442
+ parts.push({ kind: "scalar", raw: sep, valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
1333
1443
  }
1334
1444
  parts.push(node);
1335
1445
  }
@@ -1764,35 +1874,19 @@ var StructureBuilder = class _StructureBuilder {
1764
1874
  const childPrefix = [...pathPrefix, head];
1765
1875
  const fullKey = stringSegmentsToKey(childPrefix);
1766
1876
  if (field.append) {
1767
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1768
- const oldPrior = obj.priorValues.get(head);
1769
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1770
- if (folded !== void 0) obj.priorValues.set(head, folded);
1771
- obj.fields.set(head, {
1772
- _kind: "append-placeholder",
1773
- // Use the folded existing (self-ref-free) for the Append. When the
1774
- // fold returned undefined (self-ref present but no old prior),
1775
- // fall back to the original existing — resolveSubst will then
1776
- // raise the "self-ref with no prior value" error at resolve time,
1777
- // matching the pre-fix behaviour for that edge case.
1778
- //
1779
- // Clone `folded` again so the Append.existing and priorValues[head]
1780
- // do not share the same Subst/Concat references — relativizeResObj
1781
- // walks BOTH `fields` and `priorValues` and mutates Subst.segments
1782
- // in place, so a shared reference would get its prefix applied
1783
- // twice (resulting in e.g. `${outer.outer.base}` instead of
1784
- // `${outer.base}`). Codex review on PR #131.
1785
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1786
- elem: this.astToResolverValue(field.value, childPrefix)
1787
- });
1877
+ const synthetic = this.desugarAppend(field, childPrefix);
1878
+ this.applyField(obj, synthetic, pathPrefix);
1788
1879
  return;
1789
1880
  }
1790
1881
  const existing = obj.fields.get(head);
1791
1882
  const newVal = this.astToResolverValue(field.value, childPrefix);
1883
+ if (!containsSelfRef(newVal, fullKey)) {
1884
+ obj.resetKeys.add(head);
1885
+ }
1792
1886
  if (existing !== void 0) {
1793
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1794
1887
  const oldPrior = obj.priorValues.get(head);
1795
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1888
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1889
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1796
1890
  if (prior !== void 0) obj.priorValues.set(head, prior);
1797
1891
  }
1798
1892
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1830,23 +1924,19 @@ var StructureBuilder = class _StructureBuilder {
1830
1924
  const childPrefix = [...pathPrefix, head];
1831
1925
  const fullKey = stringSegmentsToKey(childPrefix);
1832
1926
  if (field.append) {
1833
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1834
- const oldPrior = obj.priorValues.get(head);
1835
- const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1836
- if (folded !== void 0) obj.priorValues.set(head, folded);
1837
- obj.fields.set(head, {
1838
- _kind: "append-placeholder",
1839
- existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1840
- elem: await this.astToResolverValueAsync(field.value, childPrefix)
1841
- });
1927
+ const synthetic = this.desugarAppend(field, childPrefix);
1928
+ await this.applyFieldAsync(obj, synthetic, pathPrefix);
1842
1929
  return;
1843
1930
  }
1844
1931
  const existing = obj.fields.get(head);
1845
1932
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1933
+ if (!containsSelfRef(newVal, fullKey)) {
1934
+ obj.resetKeys.add(head);
1935
+ }
1846
1936
  if (existing !== void 0) {
1847
- const exFolded = foldNestedSelfRefs(existing, childPrefix);
1848
1937
  const oldPrior = obj.priorValues.get(head);
1849
- const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1938
+ const priorInput = isResObj(existing) ? foldNestedSelfRefs(existing, childPrefix) : existing;
1939
+ const prior = foldOrSkipPrior(priorInput, fullKey, oldPrior);
1850
1940
  if (prior !== void 0) obj.priorValues.set(head, prior);
1851
1941
  }
1852
1942
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
@@ -1855,6 +1945,21 @@ var StructureBuilder = class _StructureBuilder {
1855
1945
  }
1856
1946
  obj.fields.set(head, newVal);
1857
1947
  }
1948
+ /** Builds the `key = ${?fullkey} [value]` field that `key += value` desugars
1949
+ * to (S13b.2, HOCON.md L732). `childPrefix` is the field's fully-qualified
1950
+ * path (pathPrefix + key), so a nested `srv { items += x }` references
1951
+ * `${?srv.items}`; include relativization rewrites it under a mount prefix.
1952
+ * `field.key` is already single-segment here (multi-segment keys are split
1953
+ * into nested objects upstream). Returns a NEW AstField (append=false);
1954
+ * positions inherit from the original field so resolve-time errors (the
1955
+ * S13b.2 non-array check, now via the scalar+array concat) keep a location. */
1956
+ desugarAppend(field, childPrefix) {
1957
+ const segments = childPrefix.map((text) => ({ text, line: field.pos.line, col: field.pos.col }));
1958
+ const subst = { kind: "subst", segments, optional: true, listSuffix: false, pos: field.pos };
1959
+ const elemArray = { kind: "array", items: [field.value], pos: field.pos };
1960
+ const synthetic = { kind: "concat", nodes: [subst, elemArray], pos: field.pos };
1961
+ return { key: field.key, value: synthetic, append: false, pos: field.pos };
1962
+ }
1858
1963
  astToResolverValue(ast, pathPrefix) {
1859
1964
  switch (ast.kind) {
1860
1965
  case "scalar": {
@@ -1869,7 +1974,7 @@ var StructureBuilder = class _StructureBuilder {
1869
1974
  return inner;
1870
1975
  }
1871
1976
  case "subst":
1872
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1977
+ 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 };
1873
1978
  case "concat":
1874
1979
  return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1875
1980
  case "include":
@@ -1893,7 +1998,7 @@ var StructureBuilder = class _StructureBuilder {
1893
1998
  case "object":
1894
1999
  return await this.buildAsync(ast, pathPrefix);
1895
2000
  case "subst":
1896
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
2001
+ 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 };
1897
2002
  case "concat": {
1898
2003
  const nodes = [];
1899
2004
  for (const n of ast.nodes) {
@@ -1919,11 +2024,6 @@ var StructureBuilder = class _StructureBuilder {
1919
2024
  }
1920
2025
  return;
1921
2026
  }
1922
- if (isAppend(val)) {
1923
- this.relativizeSubstPaths(val.existing, prefixSegments);
1924
- this.relativizeSubstPaths(val.elem, prefixSegments);
1925
- return;
1926
- }
1927
2027
  if (isResObj(val)) {
1928
2028
  this.relativizeResObj(val, prefixSegments);
1929
2029
  return;
@@ -2011,6 +2111,8 @@ var SubstitutionResolver = class {
2011
2111
  const result = /* @__PURE__ */ new Map();
2012
2112
  for (const [key, val] of obj.fields) {
2013
2113
  this.resolvingFieldPath.push(key);
2114
+ const fullCacheKey = stringSegmentsToKey(this.resolvingFieldPath);
2115
+ this.cache.delete(fullCacheKey);
2014
2116
  let resolved;
2015
2117
  try {
2016
2118
  resolved = this.resolveVal(val, obj);
@@ -2023,28 +2125,42 @@ var SubstitutionResolver = class {
2023
2125
  if (prior !== void 0) {
2024
2126
  const priorResolved = this.resolveVal(prior, obj);
2025
2127
  if (priorResolved !== void 0 && priorResolved.kind === "object") {
2026
- result.set(
2027
- key,
2028
- deepMergeHoconValues(
2029
- priorResolved,
2030
- resolved
2031
- )
2128
+ const finalValue = deepMergeHoconValues(
2129
+ priorResolved,
2130
+ resolved
2032
2131
  );
2132
+ result.set(key, finalValue);
2133
+ this.cache.set(fullCacheKey, finalValue);
2134
+ this.cacheDescendants(fullCacheKey, finalValue);
2033
2135
  continue;
2034
2136
  }
2035
2137
  }
2036
2138
  }
2037
2139
  result.set(key, resolved);
2140
+ this.cache.set(fullCacheKey, resolved);
2141
+ this.cacheDescendants(fullCacheKey, resolved);
2038
2142
  } else {
2039
2143
  const prior = obj.priorValues.get(key);
2040
2144
  if (prior !== void 0) {
2041
2145
  const priorResolved = this.resolveVal(prior, obj);
2042
- if (priorResolved !== void 0) result.set(key, priorResolved);
2146
+ if (priorResolved !== void 0) {
2147
+ result.set(key, priorResolved);
2148
+ this.cache.set(fullCacheKey, priorResolved);
2149
+ this.cacheDescendants(fullCacheKey, priorResolved);
2150
+ }
2043
2151
  }
2044
2152
  }
2045
2153
  }
2046
2154
  return { kind: "object", fields: result };
2047
2155
  }
2156
+ cacheDescendants(prefix, value) {
2157
+ if (value.kind !== "object") return;
2158
+ for (const [key, child] of value.fields) {
2159
+ const childKey = `${prefix}.${stringSegmentsToKey([key])}`;
2160
+ this.cache.set(childKey, child);
2161
+ this.cacheDescendants(childKey, child);
2162
+ }
2163
+ }
2048
2164
  resolveVal(v, scope) {
2049
2165
  if (isSubst(v)) return this.resolveSubst(v, scope);
2050
2166
  if (isConcat(v)) {
@@ -2056,7 +2172,6 @@ var SubstitutionResolver = class {
2056
2172
  this.resolvingConcats.delete(v);
2057
2173
  }
2058
2174
  }
2059
- if (isAppend(v)) return this.resolveAppend(v, scope);
2060
2175
  if (isResObj(v)) return this.resolveResObj(v);
2061
2176
  const hv = v;
2062
2177
  if (hv.kind === "array") {
@@ -2070,6 +2185,7 @@ var SubstitutionResolver = class {
2070
2185
  return hv;
2071
2186
  }
2072
2187
  resolveSubst(s, scope) {
2188
+ if (s.knownAbsent) return void 0;
2073
2189
  const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
2074
2190
  if (this.cache.has(key)) return this.cache.get(key);
2075
2191
  if (this.resolving.has(key)) {
@@ -2162,6 +2278,28 @@ var SubstitutionResolver = class {
2162
2278
  if (result !== void 0) this.cache.set(key, result);
2163
2279
  return result;
2164
2280
  }
2281
+ if (s.prefixLen > 0 && s.segments.length > s.prefixLen) {
2282
+ const originalSegments = s.segments.slice(s.prefixLen);
2283
+ const fallbackFound = lookupPath(this.root, originalSegments);
2284
+ if (fallbackFound !== void 0) {
2285
+ let result = this.resolveVal(fallbackFound, scope);
2286
+ if (originalSegments.length === 1 && result !== void 0 && result.kind === "object") {
2287
+ const rootSeg = originalSegments[0]?.text ?? "";
2288
+ const prior = this.root.priorValues.get(rootSeg);
2289
+ if (prior !== void 0) {
2290
+ const priorResolved = this.resolveVal(prior, scope);
2291
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
2292
+ result = deepMergeHoconValues(
2293
+ priorResolved,
2294
+ result
2295
+ );
2296
+ }
2297
+ }
2298
+ }
2299
+ if (result !== void 0) this.cache.set(key, result);
2300
+ return result;
2301
+ }
2302
+ }
2165
2303
  if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
2166
2304
  const result = this.resolveEnvList(s);
2167
2305
  if (result !== void 0) {
@@ -2318,38 +2456,6 @@ var SubstitutionResolver = class {
2318
2456
  }
2319
2457
  return folded;
2320
2458
  }
2321
- resolveAppend(a, scope) {
2322
- const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
2323
- if (this.opts.allowUnresolved && (isSubst(existing) || isConcat(existing) || isAppend(existing))) {
2324
- return a;
2325
- }
2326
- const elem = this.resolveVal(a.elem, scope);
2327
- let priorItems;
2328
- if (existing.kind === "array") {
2329
- priorItems = existing.items;
2330
- } else if (existing.kind === "object") {
2331
- const converted = numericObjectToArray(existing);
2332
- if (converted === null) {
2333
- throw new ResolveError(
2334
- `'+=' on non-array value: prior value is object with non-numeric keys (spec L732)`,
2335
- "",
2336
- 0,
2337
- 0
2338
- );
2339
- }
2340
- priorItems = converted;
2341
- } else {
2342
- throw new ResolveError(
2343
- `'+=' on non-array value: prior value is ${existing.kind} (spec L732)`,
2344
- "",
2345
- 0,
2346
- 0
2347
- );
2348
- }
2349
- const items = [...priorItems];
2350
- if (elem !== void 0) items.push(elem);
2351
- return { kind: "array", items };
2352
- }
2353
2459
  };
2354
2460
 
2355
2461
  // src/internal/resolver/resolver.ts
@@ -2367,7 +2473,7 @@ function containsPlaceholders(tree) {
2367
2473
  }
2368
2474
  function valContainsPlaceholders(v) {
2369
2475
  const rv = v;
2370
- if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2476
+ if (isSubst(rv) || isConcat(rv)) return true;
2371
2477
  if (isResObj(rv)) return containsPlaceholders(rv);
2372
2478
  const hv = v;
2373
2479
  if (hv?.kind === "array") {
@@ -2378,7 +2484,7 @@ function valContainsPlaceholders(v) {
2378
2484
  function buildPartialHoconFromResObj(tree) {
2379
2485
  const fields = /* @__PURE__ */ new Map();
2380
2486
  for (const [k, v] of tree.fields) {
2381
- if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2487
+ if (!isSubst(v) && !isConcat(v)) {
2382
2488
  if (isResObj(v)) {
2383
2489
  fields.set(k, buildPartialHoconFromResObj(v));
2384
2490
  } else {
@@ -2767,7 +2873,7 @@ function hoconToJs(v) {
2767
2873
  function resObjToKeyShape(tree) {
2768
2874
  const fields = /* @__PURE__ */ new Map();
2769
2875
  for (const [k, v] of tree.fields) {
2770
- if (isSubst(v) || isConcat(v) || isAppend(v)) {
2876
+ if (isSubst(v) || isConcat(v)) {
2771
2877
  fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2772
2878
  } else if (isResObj(v)) {
2773
2879
  fields.set(k, resObjToKeyShape(v));
@@ -2781,7 +2887,7 @@ function stripPlaceholderFields(v) {
2781
2887
  const fields = /* @__PURE__ */ new Map();
2782
2888
  let hadPlaceholders = false;
2783
2889
  for (const [k, val] of v.fields) {
2784
- if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2890
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder") {
2785
2891
  hadPlaceholders = true;
2786
2892
  } else if (val.kind === "object") {
2787
2893
  const inner = stripPlaceholderFields(val);