@o3co/ts.hocon 1.4.1 → 1.5.2

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
@@ -290,6 +290,192 @@ function mergeUnresolved(receiver, fallback) {
290
290
  return result;
291
291
  }
292
292
 
293
+ // src/internal/resolver/fold-self-ref.ts
294
+ function substFullKey(s) {
295
+ return stringSegmentsToKey(s.segments.map((seg) => seg.text));
296
+ }
297
+ function stringSegmentsToKey(segments) {
298
+ return segments.map((t) => {
299
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
300
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
301
+ }
302
+ return t;
303
+ }).join(".");
304
+ }
305
+ function containsSelfRef(v, fullKey) {
306
+ if (isSubst(v)) return substFullKey(v) === fullKey;
307
+ 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
+ if (isResObj(v)) {
312
+ for (const f of v.fields.values()) {
313
+ if (containsSelfRef(f, fullKey)) return true;
314
+ }
315
+ return false;
316
+ }
317
+ const hv = v;
318
+ if (hv.kind === "array") {
319
+ return hv.items.some((item) => containsSelfRef(item, fullKey));
320
+ }
321
+ if (hv.kind === "object") {
322
+ for (const f of hv.fields.values()) {
323
+ if (containsSelfRef(f, fullKey)) return true;
324
+ }
325
+ return false;
326
+ }
327
+ return false;
328
+ }
329
+ function foldSelfRef(v, fullKey, replacement) {
330
+ if (isSubst(v)) {
331
+ return substFullKey(v) === fullKey ? replacement : v;
332
+ }
333
+ if (isConcat(v)) {
334
+ return {
335
+ _kind: "concat-placeholder",
336
+ nodes: v.nodes.map((n) => foldSelfRef(n, fullKey, replacement)),
337
+ line: v.line,
338
+ col: v.col
339
+ };
340
+ }
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
+ if (isResObj(v)) {
349
+ const newFields = /* @__PURE__ */ new Map();
350
+ for (const [k, val] of v.fields) {
351
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
352
+ }
353
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
354
+ }
355
+ const hv = v;
356
+ if (hv.kind === "array") {
357
+ return {
358
+ kind: "array",
359
+ items: hv.items.map((item) => foldSelfRef(item, fullKey, replacement))
360
+ };
361
+ }
362
+ if (hv.kind === "object") {
363
+ const newFields = /* @__PURE__ */ new Map();
364
+ for (const [k, val] of hv.fields) {
365
+ newFields.set(k, foldSelfRef(val, fullKey, replacement));
366
+ }
367
+ return { kind: "object", fields: newFields };
368
+ }
369
+ return v;
370
+ }
371
+ function foldOrSkipPrior(prior, fullKey, old) {
372
+ if (!containsSelfRef(prior, fullKey)) return cloneResolverValue(prior);
373
+ if (old === void 0) return void 0;
374
+ return foldSelfRef(prior, fullKey, old);
375
+ }
376
+ function cloneResolverValue(v) {
377
+ if (isSubst(v)) {
378
+ return {
379
+ _kind: "subst-placeholder",
380
+ segments: v.segments.map((seg) => ({ text: seg.text, line: seg.line, col: seg.col })),
381
+ optional: v.optional,
382
+ listSuffix: v.listSuffix,
383
+ line: v.line,
384
+ col: v.col,
385
+ prefixLen: v.prefixLen
386
+ };
387
+ }
388
+ if (isConcat(v)) {
389
+ return {
390
+ _kind: "concat-placeholder",
391
+ nodes: v.nodes.map((n) => cloneResolverValue(n)),
392
+ line: v.line,
393
+ col: v.col
394
+ };
395
+ }
396
+ if (isResObj(v)) {
397
+ const newFields = /* @__PURE__ */ new Map();
398
+ for (const [k, val] of v.fields) newFields.set(k, cloneResolverValue(val));
399
+ const newPriors = /* @__PURE__ */ new Map();
400
+ 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
+ };
409
+ }
410
+ const hv = v;
411
+ if (hv.kind === "array") {
412
+ return {
413
+ kind: "array",
414
+ items: hv.items.map((item) => cloneResolverValue(item))
415
+ };
416
+ }
417
+ if (hv.kind === "object") {
418
+ const newFields = /* @__PURE__ */ new Map();
419
+ for (const [k, val] of hv.fields) {
420
+ newFields.set(k, cloneResolverValue(val));
421
+ }
422
+ return { kind: "object", fields: newFields };
423
+ }
424
+ return hv;
425
+ }
426
+ function foldNestedSelfRefs(v, pathPrefix) {
427
+ if (!isResObj(v)) return v;
428
+ const newFields = /* @__PURE__ */ new Map();
429
+ for (const [k, fieldVal] of v.fields) {
430
+ const childPath = [...pathPrefix, k];
431
+ const fullKey = stringSegmentsToKey(childPath);
432
+ const folded = foldNestedSelfRefs(fieldVal, childPath);
433
+ let finalVal = folded;
434
+ if (containsSelfRef(folded, fullKey)) {
435
+ const leafPrior = v.priorValues.get(k);
436
+ if (leafPrior !== void 0) {
437
+ const leafPriorFolded = foldNestedSelfRefs(leafPrior, childPath);
438
+ finalVal = foldSelfRef(folded, fullKey, leafPriorFolded);
439
+ }
440
+ }
441
+ newFields.set(k, finalVal);
442
+ }
443
+ return { _kind: "res-obj", fields: newFields, priorValues: new Map(v.priorValues) };
444
+ }
445
+ function containsSubstByPath(v, target) {
446
+ if (isSubst(v)) return segmentsTextEqual(v.segments, target);
447
+ 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
+ if (isResObj(v)) {
452
+ for (const f of v.fields.values()) {
453
+ if (containsSubstByPath(f, target)) return true;
454
+ }
455
+ return false;
456
+ }
457
+ const hv = v;
458
+ if (hv.kind === "array") {
459
+ return hv.items.some((item) => containsSubstByPath(item, target));
460
+ }
461
+ if (hv.kind === "object") {
462
+ for (const f of hv.fields.values()) {
463
+ if (containsSubstByPath(f, target)) return true;
464
+ }
465
+ return false;
466
+ }
467
+ return false;
468
+ }
469
+ function segmentsTextEqual(a, b) {
470
+ if (a.length !== b.length) return false;
471
+ for (let i = 0; i < a.length; i++) {
472
+ const ai = a[i];
473
+ const bi = b[i];
474
+ if (ai === void 0 || bi === void 0 || ai.text !== bi.text) return false;
475
+ }
476
+ return true;
477
+ }
478
+
293
479
  // src/internal/resolver/utils.ts
294
480
  function segmentsToKey(segments) {
295
481
  return segments.map((s) => {
@@ -331,13 +517,22 @@ function deepMergeHoconValues(base, overlay) {
331
517
  }
332
518
  return { kind: "object", fields: merged };
333
519
  }
334
- function deepMergeResObjInto(dst, src) {
520
+ function deepMergeResObjInto(dst, src, pathPrefix = []) {
335
521
  for (const [k, srcVal] of src.fields) {
522
+ const childPrefix = [...pathPrefix, k];
523
+ const fullKey = stringSegmentsToKey(childPrefix);
336
524
  const dstVal = dst.fields.get(k);
337
525
  if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
338
- deepMergeResObjInto(dstVal, srcVal);
526
+ const priorExisting = dst.priorValues.get(k);
527
+ const prior = foldOrSkipPrior(dstVal, fullKey, priorExisting);
528
+ if (prior !== void 0) dst.priorValues.set(k, prior);
529
+ deepMergeResObjInto(dstVal, srcVal, childPrefix);
339
530
  } else {
340
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
531
+ 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);
535
+ }
341
536
  dst.fields.set(k, srcVal);
342
537
  }
343
538
  }
@@ -901,12 +1096,17 @@ var Parser = class {
901
1096
  const segments = [];
902
1097
  let trailingDot = false;
903
1098
  let firstWasQuoted = false;
1099
+ let spaceConcat = false;
904
1100
  while (true) {
905
1101
  const t = this.peek();
906
1102
  if (t.kind === "string") {
907
1103
  this.advance();
908
1104
  if (segments.length === 0) firstWasQuoted = true;
909
- segments.push(t.value);
1105
+ if (spaceConcat) {
1106
+ segments[segments.length - 1] = `${segments[segments.length - 1]} ${t.value}`;
1107
+ } else {
1108
+ segments.push(t.value);
1109
+ }
910
1110
  trailingDot = false;
911
1111
  } else if (t.kind === "unquoted") {
912
1112
  this.advance();
@@ -923,12 +1123,23 @@ var Parser = class {
923
1123
  );
924
1124
  }
925
1125
  }
926
- segments.push(...filtered);
1126
+ if (spaceConcat && filtered.length > 0) {
1127
+ if (raw.startsWith(".")) {
1128
+ segments.push(...filtered);
1129
+ } else {
1130
+ const [head, ...tail] = filtered;
1131
+ segments[segments.length - 1] = `${segments[segments.length - 1]} ${head}`;
1132
+ segments.push(...tail);
1133
+ }
1134
+ } else {
1135
+ segments.push(...filtered);
1136
+ }
927
1137
  trailingDot = raw.endsWith(".");
928
1138
  } else {
929
1139
  if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
930
1140
  break;
931
1141
  }
1142
+ spaceConcat = false;
932
1143
  if (trailingDot) continue;
933
1144
  const next = this.peek();
934
1145
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
@@ -940,6 +1151,10 @@ var Parser = class {
940
1151
  trailingDot = true;
941
1152
  continue;
942
1153
  }
1154
+ if ((next.kind === "unquoted" || next.kind === "string") && next.precedingSpace) {
1155
+ spaceConcat = true;
1156
+ continue;
1157
+ }
943
1158
  break;
944
1159
  }
945
1160
  return { segments, firstWasQuoted };
@@ -1327,7 +1542,7 @@ var IncludeLoader = class {
1327
1542
  for (const ext of probeExts) {
1328
1543
  const obj = this.loadSingle(`${absPath}${ext}`);
1329
1544
  if (obj !== void 0) {
1330
- deepMergeResObjInto(merged, obj);
1545
+ deepMergeResObjInto(merged, obj, []);
1331
1546
  foundAny = true;
1332
1547
  }
1333
1548
  }
@@ -1360,7 +1575,7 @@ var IncludeLoader = class {
1360
1575
  for (const ext of probeExts) {
1361
1576
  const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1362
1577
  if (obj !== void 0) {
1363
- deepMergeResObjInto(merged, obj);
1578
+ deepMergeResObjInto(merged, obj, []);
1364
1579
  foundAny = true;
1365
1580
  }
1366
1581
  }
@@ -1532,7 +1747,7 @@ var StructureBuilder = class _StructureBuilder {
1532
1747
  if (pathPrefix.length > 0) {
1533
1748
  this.relativizeResObj(included, pathPrefix);
1534
1749
  }
1535
- deepMergeResObjInto(obj, included);
1750
+ deepMergeResObjInto(obj, included, pathPrefix);
1536
1751
  return;
1537
1752
  }
1538
1753
  const [head, ...tail] = field.key;
@@ -1547,12 +1762,27 @@ var StructureBuilder = class _StructureBuilder {
1547
1762
  return;
1548
1763
  }
1549
1764
  const childPrefix = [...pathPrefix, head];
1765
+ const fullKey = stringSegmentsToKey(childPrefix);
1550
1766
  if (field.append) {
1551
1767
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1552
- obj.priorValues.set(head, existing2);
1768
+ const oldPrior = obj.priorValues.get(head);
1769
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1770
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1553
1771
  obj.fields.set(head, {
1554
1772
  _kind: "append-placeholder",
1555
- existing: existing2,
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,
1556
1786
  elem: this.astToResolverValue(field.value, childPrefix)
1557
1787
  });
1558
1788
  return;
@@ -1560,10 +1790,13 @@ var StructureBuilder = class _StructureBuilder {
1560
1790
  const existing = obj.fields.get(head);
1561
1791
  const newVal = this.astToResolverValue(field.value, childPrefix);
1562
1792
  if (existing !== void 0) {
1563
- obj.priorValues.set(head, existing);
1793
+ const exFolded = foldNestedSelfRefs(existing, childPrefix);
1794
+ const oldPrior = obj.priorValues.get(head);
1795
+ const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1796
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1564
1797
  }
1565
1798
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1566
- deepMergeResObjInto(existing, newVal);
1799
+ deepMergeResObjInto(existing, newVal, childPrefix);
1567
1800
  return;
1568
1801
  }
1569
1802
  obj.fields.set(head, newVal);
@@ -1580,7 +1813,7 @@ var StructureBuilder = class _StructureBuilder {
1580
1813
  if (pathPrefix.length > 0) {
1581
1814
  this.relativizeResObj(included, pathPrefix);
1582
1815
  }
1583
- deepMergeResObjInto(obj, included);
1816
+ deepMergeResObjInto(obj, included, pathPrefix);
1584
1817
  return;
1585
1818
  }
1586
1819
  const [head, ...tail] = field.key;
@@ -1595,12 +1828,15 @@ var StructureBuilder = class _StructureBuilder {
1595
1828
  return;
1596
1829
  }
1597
1830
  const childPrefix = [...pathPrefix, head];
1831
+ const fullKey = stringSegmentsToKey(childPrefix);
1598
1832
  if (field.append) {
1599
1833
  const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1600
- obj.priorValues.set(head, existing2);
1834
+ const oldPrior = obj.priorValues.get(head);
1835
+ const folded = foldOrSkipPrior(existing2, fullKey, oldPrior);
1836
+ if (folded !== void 0) obj.priorValues.set(head, folded);
1601
1837
  obj.fields.set(head, {
1602
1838
  _kind: "append-placeholder",
1603
- existing: existing2,
1839
+ existing: folded !== void 0 ? cloneResolverValue(folded) : existing2,
1604
1840
  elem: await this.astToResolverValueAsync(field.value, childPrefix)
1605
1841
  });
1606
1842
  return;
@@ -1608,10 +1844,13 @@ var StructureBuilder = class _StructureBuilder {
1608
1844
  const existing = obj.fields.get(head);
1609
1845
  const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1610
1846
  if (existing !== void 0) {
1611
- obj.priorValues.set(head, existing);
1847
+ const exFolded = foldNestedSelfRefs(existing, childPrefix);
1848
+ const oldPrior = obj.priorValues.get(head);
1849
+ const prior = foldOrSkipPrior(exFolded, fullKey, oldPrior);
1850
+ if (prior !== void 0) obj.priorValues.set(head, prior);
1612
1851
  }
1613
1852
  if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1614
- deepMergeResObjInto(existing, newVal);
1853
+ deepMergeResObjInto(existing, newVal, childPrefix);
1615
1854
  return;
1616
1855
  }
1617
1856
  obj.fields.set(head, newVal);
@@ -1743,6 +1982,23 @@ var SubstitutionResolver = class {
1743
1982
  // narrower than path-equality. Spec amendment deferred to a follow-up
1744
1983
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1745
1984
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1985
+ // Path stack tracking the full dotted path of the field currently being
1986
+ // assigned in `resolveResObj`. Leaf keys are pushed on entry to each
1987
+ // `(key, val)` iteration and popped on exit, so nested objects build up
1988
+ // the full path (e.g. `["o", "history"]` while resolving `o.history`).
1989
+ //
1990
+ // Used to tighten self-ref detection at the L192 found-lookup site:
1991
+ // a substitution `${X}` whose looked-up value contains a Subst pointing
1992
+ // at `X` is only a true self-reference when the field currently being
1993
+ // assigned is `X` (or a descendant of `X`). Without this guard, an
1994
+ // external lookup like `b = ${o}` (where o has a self-referential
1995
+ // field) would mis-fire and short-circuit to o's prior, returning the
1996
+ // wrong value for b. The is_owner gate (rfp prefix-matches s.segments)
1997
+ // narrows the check to the owner case only.
1998
+ //
1999
+ // Cross-impl with rs.hocon v1.5.1's resolving_field_path (same purpose,
2000
+ // same prefix-match semantics).
2001
+ this.resolvingFieldPath = [];
1746
2002
  }
1747
2003
  /** Returns a prefix for error messages, including origin info when set. */
1748
2004
  originPrefix() {
@@ -1754,7 +2010,13 @@ var SubstitutionResolver = class {
1754
2010
  resolveResObj(obj) {
1755
2011
  const result = /* @__PURE__ */ new Map();
1756
2012
  for (const [key, val] of obj.fields) {
1757
- const resolved = this.resolveVal(val, obj);
2013
+ this.resolvingFieldPath.push(key);
2014
+ let resolved;
2015
+ try {
2016
+ resolved = this.resolveVal(val, obj);
2017
+ } finally {
2018
+ this.resolvingFieldPath.pop();
2019
+ }
1758
2020
  if (resolved !== void 0) {
1759
2021
  if (resolved.kind === "object") {
1760
2022
  const prior = obj.priorValues.get(key);
@@ -1844,7 +2106,10 @@ var SubstitutionResolver = class {
1844
2106
  try {
1845
2107
  const found = lookupPath(this.root, s.segments);
1846
2108
  if (found !== void 0) {
1847
- if (isConcat(found) && this.resolvingConcats.has(found)) {
2109
+ const rfp = this.resolvingFieldPath;
2110
+ const isOwner = rfp.length >= s.segments.length && s.segments.every((seg, i) => rfp[i] === seg.text);
2111
+ const isSelfRef = isConcat(found) && this.resolvingConcats.has(found) || isOwner && containsSubstByPath(found, s.segments);
2112
+ if (isSelfRef) {
1848
2113
  let prior;
1849
2114
  if (s.segments.length > 1) {
1850
2115
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
@@ -2055,8 +2320,33 @@ var SubstitutionResolver = class {
2055
2320
  }
2056
2321
  resolveAppend(a, scope) {
2057
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
+ }
2058
2326
  const elem = this.resolveVal(a.elem, scope);
2059
- const items = existing.kind === "array" ? [...existing.items] : [existing];
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];
2060
2350
  if (elem !== void 0) items.push(elem);
2061
2351
  return { kind: "array", items };
2062
2352
  }
@@ -2154,6 +2444,9 @@ var Config = class _Config {
2154
2444
  }
2155
2445
  getString(path2) {
2156
2446
  const v = this.requireScalar(path2);
2447
+ if (v.valueType === "null") {
2448
+ throw new ConfigError(`expected string at ${path2}, got ${v.valueType}`, path2);
2449
+ }
2157
2450
  return v.raw;
2158
2451
  }
2159
2452
  getNumber(path2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/ts.hocon",
3
- "version": "1.4.1",
3
+ "version": "1.5.2",
4
4
  "description": "Full Lightbend HOCON spec-compliant parser and config library for TypeScript",
5
5
  "keywords": [
6
6
  "hocon",