@o3co/ts.hocon 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/zod.cjs CHANGED
@@ -39,6 +39,27 @@ module.exports = __toCommonJS(zod_exports);
39
39
 
40
40
  // src/coerce.ts
41
41
  var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
42
+ function isHoconWs(cp) {
43
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
44
+ if (cp >= 28 && cp <= 31) return true;
45
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
46
+ if (cp === 5760) return true;
47
+ if (cp >= 8192 && cp <= 8202) return true;
48
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
49
+ if (cp === 12288) return true;
50
+ return false;
51
+ }
52
+ function trimHoconWs(s) {
53
+ let start = 0;
54
+ let end = s.length;
55
+ while (start < end && isHoconWs(s.charCodeAt(start))) {
56
+ start++;
57
+ }
58
+ while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
59
+ end--;
60
+ }
61
+ return s.slice(start, end);
62
+ }
42
63
  var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
43
64
  var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
44
65
  function coerceBoolean(value) {
@@ -84,22 +105,29 @@ var OUTPUT_DURATION_UNITS = {
84
105
  d: 864e5
85
106
  };
86
107
  function parseDuration(value, outputUnit = "ms") {
87
- const trimmed = value.trim();
108
+ const trimmed = trimHoconWs(value);
88
109
  let i = 0;
89
110
  while (i < trimmed.length) {
90
111
  const ch = trimmed[i];
91
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
112
+ if (i === 0 && (ch === "-" || ch === "+")) {
113
+ i++;
114
+ continue;
115
+ }
116
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
92
117
  i++;
93
118
  }
94
119
  if (i === 0) return NaN;
95
120
  const num = Number(trimmed.slice(0, i));
96
121
  if (Number.isNaN(num)) return NaN;
97
- const unit = trimmed.slice(i).trim().toLowerCase();
122
+ const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
123
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
124
+ if (divisor === void 0) return NaN;
125
+ if (unit === "") {
126
+ return num / divisor;
127
+ }
98
128
  const mult = DURATION_UNITS[unit];
99
129
  if (mult === void 0) return NaN;
100
130
  const ms = num * mult;
101
- const divisor = OUTPUT_DURATION_UNITS[outputUnit];
102
- if (divisor === void 0) return NaN;
103
131
  return ms / divisor;
104
132
  }
105
133
  var BYTE_UNITS = {
@@ -130,6 +158,22 @@ var BYTE_UNITS = {
130
158
  TiB: 1099511627776,
131
159
  tebibyte: 1099511627776,
132
160
  tebibytes: 1099511627776,
161
+ // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
162
+ // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
163
+ // Both upper- and lowercase accepted per Lightbend behaviour.
164
+ // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
165
+ K: 1024,
166
+ k: 1024,
167
+ M: 1024 ** 2,
168
+ m: 1024 ** 2,
169
+ G: 1024 ** 3,
170
+ g: 1024 ** 3,
171
+ T: 1024 ** 4,
172
+ t: 1024 ** 4,
173
+ P: 1024 ** 5,
174
+ p: 1024 ** 5,
175
+ E: 1024 ** 6,
176
+ e: 1024 ** 6,
133
177
  // lowercase short-form aliases
134
178
  b: 1,
135
179
  kb: 1e3,
@@ -153,29 +197,47 @@ var OUTPUT_BYTE_UNITS = {
153
197
  TiB: 1099511627776
154
198
  };
155
199
  function parseBytes(value, outputUnit = "B") {
156
- const trimmed = value.trim();
200
+ const trimmed = trimHoconWs(value);
157
201
  let i = 0;
158
202
  while (i < trimmed.length) {
159
203
  const ch = trimmed[i];
160
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
204
+ if (i === 0 && (ch === "-" || ch === "+")) {
205
+ i++;
206
+ continue;
207
+ }
208
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
161
209
  i++;
162
210
  }
163
211
  if (i === 0) return NaN;
164
212
  const num = Number(trimmed.slice(0, i));
165
213
  if (Number.isNaN(num)) return NaN;
166
- const unit = trimmed.slice(i).trim();
214
+ const unit = trimHoconWs(trimmed.slice(i));
215
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
216
+ if (divisor === void 0) return NaN;
217
+ if (unit === "") {
218
+ const bytes2 = Math.trunc(num);
219
+ if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
220
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
221
+ }
222
+ return bytes2 / divisor;
223
+ }
167
224
  let mult = BYTE_UNITS[unit];
168
225
  if (mult === void 0) {
169
226
  mult = BYTE_UNITS[unit.toLowerCase()];
170
227
  }
171
228
  if (mult === void 0) return NaN;
172
229
  const bytes = num * mult;
173
- const divisor = OUTPUT_BYTE_UNITS[outputUnit];
174
- if (divisor === void 0) return NaN;
230
+ if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
231
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
232
+ }
175
233
  const result = bytes / divisor;
176
234
  return outputUnit === "B" ? Math.round(result) : result;
177
235
  }
178
236
 
237
+ // src/parse.ts
238
+ var fs = __toESM(require("fs"), 1);
239
+ var path = __toESM(require("path"), 1);
240
+
179
241
  // src/errors.ts
180
242
  var ParseError = class extends Error {
181
243
  constructor(message, line, col, file) {
@@ -196,6 +258,14 @@ var ResolveError = class extends Error {
196
258
  this.name = "ResolveError";
197
259
  }
198
260
  };
261
+ var PackageLookupError = class extends ResolveError {
262
+ constructor(message, identifier, packageFile, line, col) {
263
+ super(message, `${identifier}/${packageFile}`, line, col);
264
+ this.identifier = identifier;
265
+ this.packageFile = packageFile;
266
+ this.name = "PackageLookupError";
267
+ }
268
+ };
199
269
  var ConfigError = class extends Error {
200
270
  constructor(message, path2) {
201
271
  super(message);
@@ -203,6 +273,156 @@ var ConfigError = class extends Error {
203
273
  this.name = "ConfigError";
204
274
  }
205
275
  };
276
+ var NotResolvedError = class extends ConfigError {
277
+ constructor(path2) {
278
+ super(`value at path "${path2}" is not resolved (call resolve() first)`, path2);
279
+ this.name = "NotResolvedError";
280
+ }
281
+ };
282
+
283
+ // src/internal/resolver/types.ts
284
+ var separatorValues = /* @__PURE__ */ new WeakSet();
285
+ function isSubst(v) {
286
+ return v._kind === "subst-placeholder";
287
+ }
288
+ function isConcat(v) {
289
+ return v._kind === "concat-placeholder";
290
+ }
291
+ function isAppend(v) {
292
+ return v._kind === "append-placeholder";
293
+ }
294
+ function isResObj(v) {
295
+ return v._kind === "res-obj";
296
+ }
297
+ function makeResObj() {
298
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
299
+ }
300
+ function mergeUnresolved(receiver, fallback) {
301
+ const result = makeResObj();
302
+ for (const [k, v] of fallback.fields) {
303
+ result.fields.set(k, v);
304
+ }
305
+ for (const [k, v] of fallback.priorValues) {
306
+ result.priorValues.set(k, v);
307
+ }
308
+ for (const [k, rv] of receiver.fields) {
309
+ const existing = result.fields.get(k);
310
+ if (existing !== void 0) {
311
+ const recPrior = receiver.priorValues.get(k);
312
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
313
+ result.fields.set(k, rv);
314
+ continue;
315
+ }
316
+ if (isResObj(rv) && isResObj(existing)) {
317
+ result.fields.set(k, mergeUnresolved(rv, existing));
318
+ continue;
319
+ }
320
+ result.priorValues.set(k, existing);
321
+ }
322
+ result.fields.set(k, rv);
323
+ }
324
+ for (const [k, v] of receiver.priorValues) {
325
+ if (receiver.fields.has(k)) {
326
+ result.priorValues.set(k, v);
327
+ }
328
+ }
329
+ return result;
330
+ }
331
+
332
+ // src/internal/resolver/utils.ts
333
+ function segmentsToKey(segments) {
334
+ return segments.map((s) => {
335
+ const t = s.text;
336
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
337
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
338
+ }
339
+ return t;
340
+ }).join(".");
341
+ }
342
+ function lookupPath(root, segments) {
343
+ const [head, ...tail] = segments;
344
+ if (head === void 0) return void 0;
345
+ const key = head.text;
346
+ const val = root.fields.get(key);
347
+ if (val === void 0) return void 0;
348
+ if (tail.length === 0) return val;
349
+ if (isResObj(val)) return lookupPath(val, tail);
350
+ return void 0;
351
+ }
352
+ function lookupResObj(root, segments) {
353
+ let cur = root;
354
+ for (const seg of segments) {
355
+ const val = cur.fields.get(seg.text);
356
+ if (val === void 0 || !isResObj(val)) return void 0;
357
+ cur = val;
358
+ }
359
+ return cur;
360
+ }
361
+ function deepMergeHoconValues(base, overlay) {
362
+ const merged = new Map(base.fields);
363
+ for (const [k, v] of overlay.fields) {
364
+ const existing = merged.get(k);
365
+ if (existing?.kind === "object" && v.kind === "object") {
366
+ merged.set(k, deepMergeHoconValues(existing, v));
367
+ } else {
368
+ merged.set(k, v);
369
+ }
370
+ }
371
+ return { kind: "object", fields: merged };
372
+ }
373
+ function deepMergeResObjInto(dst, src) {
374
+ for (const [k, srcVal] of src.fields) {
375
+ const dstVal = dst.fields.get(k);
376
+ if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
377
+ deepMergeResObjInto(dstVal, srcVal);
378
+ } else {
379
+ if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
380
+ dst.fields.set(k, srcVal);
381
+ }
382
+ }
383
+ for (const [k, srcPrior] of src.priorValues) {
384
+ if (!dst.priorValues.has(k)) {
385
+ dst.priorValues.set(k, srcPrior);
386
+ }
387
+ }
388
+ }
389
+ function hoconValueToResObj(hv) {
390
+ const obj = makeResObj();
391
+ if (hv.kind !== "object") return obj;
392
+ for (const [key, val] of hv.fields) {
393
+ if (val.kind === "object") {
394
+ obj.fields.set(key, hoconValueToResObj(val));
395
+ } else {
396
+ obj.fields.set(key, val);
397
+ }
398
+ }
399
+ return obj;
400
+ }
401
+ function isFileNotFoundError(e) {
402
+ if (!(e instanceof Error)) return false;
403
+ const code = e.code;
404
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
405
+ const msg = e.message.toLowerCase();
406
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
407
+ }
408
+
409
+ // src/internal/resolver/include-loader.ts
410
+ var import_node_module = require("module");
411
+ var nodePath = __toESM(require("path"), 1);
412
+
413
+ // src/internal/parser/empty-check.ts
414
+ function hasContentTokens(tokens) {
415
+ return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
416
+ }
417
+ function assertNonEmptyDocument(tokens, sourceDescriptor) {
418
+ if (!hasContentTokens(tokens)) {
419
+ throw new ParseError(
420
+ `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
421
+ 1,
422
+ 1
423
+ );
424
+ }
425
+ }
206
426
 
207
427
  // src/internal/lexer/lexer.ts
208
428
  function isHoconWhitespace(ch) {
@@ -331,14 +551,6 @@ var Lexer = class {
331
551
  continue;
332
552
  }
333
553
  if (isUnquotedStart(ch)) {
334
- if (ch === "-" && !isDecimalDigit(this.peek(1))) {
335
- const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
336
- throw new ParseError(
337
- `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
338
- sl,
339
- sc
340
- );
341
- }
342
554
  let value = "";
343
555
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
344
556
  value += this.advance();
@@ -442,6 +654,10 @@ var Lexer = class {
442
654
  /**
443
655
  * Parse the body of a `${...}` substitution (called after `${` has been consumed).
444
656
  * Implements Appendix A state machine from the cross-language spec.
657
+ *
658
+ * S13c extension: if the segment-collection loop encounters `[`, it delegates to
659
+ * `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
660
+ * ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
445
661
  */
446
662
  parseSubstBody(startLine, startCol) {
447
663
  let optional = false;
@@ -456,6 +672,7 @@ var Lexer = class {
456
672
  let pendingWs = "";
457
673
  const segments = [];
458
674
  let lastDot = null;
675
+ let listSuffix = false;
459
676
  while (true) {
460
677
  if (this.pos >= this.input.length) {
461
678
  throw new ParseError("unterminated substitution", startLine, startCol);
@@ -465,6 +682,23 @@ var Lexer = class {
465
682
  this.advance();
466
683
  pendingWs = "";
467
684
  break;
685
+ } else if (ch === "[") {
686
+ if (!curStarted) {
687
+ throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
688
+ }
689
+ for (const w of pendingWs) {
690
+ if (w !== " " && w !== " ") {
691
+ throw new ParseError(
692
+ `only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
693
+ startLine,
694
+ this.col
695
+ );
696
+ }
697
+ }
698
+ segments.push({ text: curText, line: curLine, col: curCol });
699
+ pendingWs = "";
700
+ listSuffix = this.parseListSuffix(startLine);
701
+ break;
468
702
  } else if (ch === '"') {
469
703
  const qLine = startLine;
470
704
  const qCol = this.col;
@@ -524,15 +758,47 @@ var Lexer = class {
524
758
  throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
525
759
  }
526
760
  }
527
- if (curStarted) {
528
- segments.push({ text: curText, line: curLine, col: curCol });
529
- } else if (segments.length === 0) {
530
- throw new ParseError("empty substitution path", startLine, startCol);
531
- } else {
532
- const [errLine, errCol] = lastDot ?? [startLine, startCol];
533
- throw new ParseError("empty segment in path", errLine, errCol);
761
+ if (!listSuffix) {
762
+ if (curStarted) {
763
+ segments.push({ text: curText, line: curLine, col: curCol });
764
+ } else if (segments.length === 0) {
765
+ throw new ParseError("empty substitution path", startLine, startCol);
766
+ } else {
767
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
768
+ throw new ParseError("empty segment in path", errLine, errCol);
769
+ }
534
770
  }
535
- return { segments, optional };
771
+ return { segments, optional, listSuffix };
772
+ }
773
+ /**
774
+ * Consume the literal `[]` suffix inside a substitution body.
775
+ * Called after the `[` has been detected (but not yet consumed).
776
+ * Requires the next character to be `[`, then exactly `]`, then `}`.
777
+ * Whitespace inside `[]` is not permitted (per spec Decision §1).
778
+ * Returns true (always — only called when listSuffix path is taken).
779
+ */
780
+ parseListSuffix(startLine) {
781
+ this.advance();
782
+ const afterBracket = this.peek();
783
+ if (afterBracket !== "]") {
784
+ const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
785
+ throw new ParseError(
786
+ `expected ']' after '[' in substitution list suffix (got ${desc})`,
787
+ startLine,
788
+ this.col
789
+ );
790
+ }
791
+ this.advance();
792
+ if (this.peek() !== "}") {
793
+ const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
794
+ throw new ParseError(
795
+ `expected '}' after '[]' in substitution list suffix (got ${desc})`,
796
+ startLine,
797
+ this.col
798
+ );
799
+ }
800
+ this.advance();
801
+ return true;
536
802
  }
537
803
  };
538
804
  function tokenize(input) {
@@ -620,6 +886,14 @@ var Parser = class {
620
886
  const t = this.peek();
621
887
  if (t.kind === "eof" || t.kind === "rbrace") break;
622
888
  if (t.kind === "unquoted" && t.value === "include") {
889
+ const next = this.peek(1);
890
+ if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
891
+ throw new ParseError(
892
+ `'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
893
+ t.line,
894
+ t.col
895
+ );
896
+ }
623
897
  this.advance();
624
898
  fields.push(this.parseInclude());
625
899
  this.skip("newline");
@@ -628,7 +902,14 @@ var Parser = class {
628
902
  continue;
629
903
  }
630
904
  const keyPos = this.currentPos();
631
- const key = this.parseKey();
905
+ const { segments: key, firstWasQuoted } = this.parseKey();
906
+ if (key[0] === "include" && !firstWasQuoted) {
907
+ throw new ParseError(
908
+ `'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
909
+ keyPos.line,
910
+ keyPos.col
911
+ );
912
+ }
632
913
  this.skip("newline");
633
914
  let append = false;
634
915
  const sep = this.peek();
@@ -658,10 +939,12 @@ var Parser = class {
658
939
  parseKey() {
659
940
  const segments = [];
660
941
  let trailingDot = false;
942
+ let firstWasQuoted = false;
661
943
  while (true) {
662
944
  const t = this.peek();
663
945
  if (t.kind === "string") {
664
946
  this.advance();
947
+ if (segments.length === 0) firstWasQuoted = true;
665
948
  segments.push(t.value);
666
949
  trailingDot = false;
667
950
  } else if (t.kind === "unquoted") {
@@ -698,64 +981,143 @@ var Parser = class {
698
981
  }
699
982
  break;
700
983
  }
701
- return segments;
984
+ return { segments, firstWasQuoted };
702
985
  }
703
986
  parseInclude() {
704
987
  const p = this.currentPos();
705
988
  this.skip("newline");
706
989
  const t = this.peek();
707
- let path2;
708
- let required = false;
709
- let isFile = false;
710
990
  if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
711
991
  if (t.value === "required") {
712
- const next2 = this.peek(1);
713
- if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
992
+ const next = this.peek(1);
993
+ if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
714
994
  throw new ParseError("include required must be followed by (", t.line, t.col);
715
995
  }
716
996
  }
717
- required = true;
718
997
  this.advance();
719
998
  const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
720
- if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
999
+ if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
721
1000
  throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
722
1001
  }
723
- if (innerPrefix.startsWith("file")) {
724
- isFile = true;
1002
+ const nextTok = this.peek();
1003
+ if (innerPrefix === "package" || innerPrefix.startsWith("package(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "package(" || nextTok.value.startsWith("package("))) {
1004
+ const consumePackageToken = innerPrefix === "";
1005
+ const { qualifier, path: path3 } = this.parsePackageArgs(consumePackageToken);
1006
+ return this.makeIncludeField(qualifier, path3, true, p);
725
1007
  }
726
- const next = this.peek();
727
- if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
728
- throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
729
- }
730
- if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
731
- isFile = true;
732
- }
733
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
734
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
735
- path2 = this.advance().value;
736
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
737
- } else if (t.kind === "string") {
738
- path2 = this.advance().value;
739
- } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
740
- isFile = true;
1008
+ if (innerPrefix === "file" || innerPrefix.startsWith("file(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "file(" || nextTok.value === "file")) {
1009
+ if (innerPrefix === "") {
1010
+ this.advance();
1011
+ }
1012
+ const path3 = this.parseQuotedPathSkipWrapper(t);
1013
+ return this.makeIncludeField({ kind: "file" }, path3, true, p);
1014
+ }
1015
+ if (innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "url" || nextTok.value.startsWith("url(") || nextTok.value === "classpath" || nextTok.value.startsWith("classpath("))) {
1016
+ throw new ParseError("include url(...) and classpath(...) are not supported", nextTok.line, nextTok.col);
1017
+ }
1018
+ if (innerPrefix !== "") {
1019
+ if (innerPrefix === ")") {
1020
+ throw new ParseError("include required(...) must contain a path", t.line, t.col);
1021
+ }
1022
+ const name = innerPrefix.replace(/\(.*$/, "").replace(/\).*$/, "");
1023
+ throw new ParseError(`unknown include qualifier inside required(): "${name}"`, t.line, t.col);
1024
+ }
1025
+ const path2 = this.parseQuotedPathSkipWrapper(t);
1026
+ return this.makeIncludeField({ kind: "bare" }, path2, true, p);
1027
+ }
1028
+ if (t.kind === "string") {
1029
+ const path2 = this.advance().value;
1030
+ return this.makeIncludeField({ kind: "bare" }, path2, false, p);
1031
+ }
1032
+ if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
741
1033
  this.advance();
742
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
743
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
744
- path2 = this.advance().value;
745
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
746
- } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
1034
+ const path2 = this.parseQuotedPathSkipWrapper(t);
1035
+ return this.makeIncludeField({ kind: "file" }, path2, false, p);
1036
+ }
1037
+ if (t.kind === "unquoted" && (t.value === "package(" || t.value.startsWith("package("))) {
1038
+ const { qualifier, path: path2 } = this.parsePackageArgs();
1039
+ return this.makeIncludeField(qualifier, path2, false, p);
1040
+ }
1041
+ if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
747
1042
  throw new ParseError("include url(...) is not supported", t.line, t.col);
748
- } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
1043
+ }
1044
+ if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
749
1045
  throw new ParseError("include classpath(...) is not supported", t.line, t.col);
750
- } else {
751
- throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
752
1046
  }
753
- return {
754
- key: [],
755
- value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
756
- append: false,
757
- pos: p
758
- };
1047
+ throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
1048
+ }
1049
+ makeIncludeField(qualifier, path2, required, pos) {
1050
+ return { key: [], value: { kind: "include", qualifier, path: path2, required, pos }, append: false, pos };
1051
+ }
1052
+ /**
1053
+ * Skip past include-syntax wrapper tokens to find the quoted path string.
1054
+ * Strict: only allows `(`, `)`, and bare qualifier keywords (file / file( /
1055
+ * package / package( / url / url( / classpath / classpath() between the
1056
+ * directive and the quoted path. Anything else is a parse error rather than
1057
+ * silently swallowed (see ts#113).
1058
+ */
1059
+ parseQuotedPathSkipWrapper(errTok) {
1060
+ while (this.isIncludeWrapperToken(this.peek())) this.advance();
1061
+ if (this.peek().kind !== "string") {
1062
+ const tok = this.peek();
1063
+ if (tok.kind === "eof") throw new ParseError("expected include path", errTok.line, errTok.col);
1064
+ const desc = tok.kind === "unquoted" ? `unquoted "${tok.value}"` : tok.kind;
1065
+ throw new ParseError(`expected quoted include path, got ${desc}`, tok.line, tok.col);
1066
+ }
1067
+ const path2 = this.advance().value;
1068
+ while (this.peek().kind === "unquoted" && (this.peek().value === ")" || this.peek().value === "))")) {
1069
+ this.advance();
1070
+ }
1071
+ const after = this.peek();
1072
+ if (after.kind !== "newline" && after.kind !== "rbrace" && after.kind !== "eof" && after.kind !== "comma") {
1073
+ const desc = after.kind === "unquoted" ? `unquoted "${after.value}"` : after.kind;
1074
+ throw new ParseError(`unexpected token after include path: ${desc}`, after.line, after.col);
1075
+ }
1076
+ return path2;
1077
+ }
1078
+ isIncludeWrapperToken(tok) {
1079
+ return tok.kind === "unquoted" && tok.value === "(";
1080
+ }
1081
+ /**
1082
+ * Parse the body of `package("identifier", "file")`.
1083
+ *
1084
+ * @param consumePackageToken - When true (default), the current token is `package(` and
1085
+ * must be consumed first. When false, the lexer already bundled `package(` into the
1086
+ * preceding token (e.g. `required(package("`), so we are already positioned at the
1087
+ * first string argument.
1088
+ */
1089
+ parsePackageArgs(consumePackageToken = true) {
1090
+ const pkgTok = consumePackageToken ? this.advance() : this.peek();
1091
+ this.skip("newline");
1092
+ const idTok = this.peek();
1093
+ if (idTok.kind !== "string") {
1094
+ throw new ParseError("include package: expected quoted identifier as first argument", pkgTok.line, pkgTok.col);
1095
+ }
1096
+ const identifier = this.advance().value;
1097
+ this.skip("newline");
1098
+ if (this.peek().kind !== "comma") {
1099
+ const after = this.peek();
1100
+ throw new ParseError(
1101
+ after.kind === "string" ? "include package: missing comma between identifier and file arguments" : "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
1102
+ pkgTok.line,
1103
+ pkgTok.col
1104
+ );
1105
+ }
1106
+ this.advance();
1107
+ this.skip("newline");
1108
+ const fileTok = this.peek();
1109
+ if (fileTok.kind !== "string") {
1110
+ throw new ParseError(
1111
+ "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
1112
+ pkgTok.line,
1113
+ pkgTok.col
1114
+ );
1115
+ }
1116
+ const filePath = this.advance().value;
1117
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") {
1118
+ this.advance();
1119
+ }
1120
+ return { qualifier: { kind: "package", identifier }, path: filePath };
759
1121
  }
760
1122
  parseValue() {
761
1123
  const p = this.currentPos();
@@ -777,7 +1139,7 @@ var Parser = class {
777
1139
  throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
778
1140
  }
779
1141
  const payload = t.subst;
780
- node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
1142
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
781
1143
  } else if (t.kind === "string" || t.kind === "triple_string") {
782
1144
  this.advance();
783
1145
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -796,7 +1158,9 @@ var Parser = class {
796
1158
  parts.push(node);
797
1159
  }
798
1160
  if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
799
- if (parts.length === 1) return parts[0];
1161
+ if (parts.length === 1) {
1162
+ return parts[0];
1163
+ }
800
1164
  return { kind: "concat", nodes: parts, pos: p };
801
1165
  }
802
1166
  parseArray() {
@@ -828,107 +1192,10 @@ function parseTokens(tokens) {
828
1192
  return new Parser(tokens).parse();
829
1193
  }
830
1194
 
831
- // src/internal/resolver/types.ts
832
- var separatorValues = /* @__PURE__ */ new WeakSet();
833
- function isSubst(v) {
834
- return v._kind === "subst-placeholder";
835
- }
836
- function isConcat(v) {
837
- return v._kind === "concat-placeholder";
838
- }
839
- function isAppend(v) {
840
- return v._kind === "append-placeholder";
841
- }
842
- function isResObj(v) {
843
- return v._kind === "res-obj";
844
- }
845
- function makeResObj() {
846
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
847
- }
848
-
849
- // src/internal/resolver/utils.ts
850
- function segmentsToKey(segments) {
851
- return segments.map((s) => {
852
- const t = s.text;
853
- if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
854
- return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
855
- }
856
- return t;
857
- }).join(".");
858
- }
859
- function lookupPath(root, segments) {
860
- const [head, ...tail] = segments;
861
- if (head === void 0) return void 0;
862
- const key = head.text;
863
- const val = root.fields.get(key);
864
- if (val === void 0) return void 0;
865
- if (tail.length === 0) return val;
866
- if (isResObj(val)) return lookupPath(val, tail);
867
- return void 0;
868
- }
869
- function lookupResObj(root, segments) {
870
- let cur = root;
871
- for (const seg of segments) {
872
- const val = cur.fields.get(seg.text);
873
- if (val === void 0 || !isResObj(val)) return void 0;
874
- cur = val;
875
- }
876
- return cur;
877
- }
878
- function deepMergeHoconValues(base, overlay) {
879
- const merged = new Map(base.fields);
880
- for (const [k, v] of overlay.fields) {
881
- const existing = merged.get(k);
882
- if (existing?.kind === "object" && v.kind === "object") {
883
- merged.set(k, deepMergeHoconValues(existing, v));
884
- } else {
885
- merged.set(k, v);
886
- }
887
- }
888
- return { kind: "object", fields: merged };
889
- }
890
- function deepMergeResObjInto(dst, src) {
891
- for (const [k, srcVal] of src.fields) {
892
- const dstVal = dst.fields.get(k);
893
- if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
894
- deepMergeResObjInto(dstVal, srcVal);
895
- } else {
896
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
897
- dst.fields.set(k, srcVal);
898
- }
899
- }
900
- for (const [k, srcPrior] of src.priorValues) {
901
- if (!dst.priorValues.has(k)) {
902
- dst.priorValues.set(k, srcPrior);
903
- }
904
- }
905
- }
906
- function hoconValueToResObj(hv) {
907
- const obj = makeResObj();
908
- if (hv.kind !== "object") return obj;
909
- for (const [key, val] of hv.fields) {
910
- if (val.kind === "object") {
911
- obj.fields.set(key, hoconValueToResObj(val));
912
- } else {
913
- obj.fields.set(key, val);
914
- }
915
- }
916
- return obj;
917
- }
918
- function isFileNotFoundError(e) {
919
- if (!(e instanceof Error)) return false;
920
- const code = e.code;
921
- if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
922
- const msg = e.message.toLowerCase();
923
- return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
924
- }
925
-
926
- // src/internal/resolver/include-loader.ts
927
- var nodePath = __toESM(require("path"), 1);
928
-
929
1195
  // src/internal/properties/properties.ts
930
1196
  function parseProperties(input) {
931
1197
  const root = /* @__PURE__ */ Object.create(null);
1198
+ const pairs = [];
932
1199
  for (const line of input.split("\n")) {
933
1200
  const trimmed = line.trim();
934
1201
  if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
@@ -937,6 +1204,10 @@ function parseProperties(input) {
937
1204
  const key = trimmed.slice(0, sepIdx).trim();
938
1205
  const value = trimmed.slice(sepIdx + 1).trim();
939
1206
  if (key === "") continue;
1207
+ pairs.push([key, value]);
1208
+ }
1209
+ pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1210
+ for (const [key, value] of pairs) {
940
1211
  setNested(root, key.split("."), value);
941
1212
  }
942
1213
  return root;
@@ -960,6 +1231,7 @@ function setNested(obj, segments, value) {
960
1231
  }
961
1232
  const last = segments[segments.length - 1];
962
1233
  if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1234
+ if (typeof current[last] === "object" && current[last] !== null) return;
963
1235
  current[last] = value;
964
1236
  }
965
1237
  function propertiesToHoconValue(input) {
@@ -979,6 +1251,85 @@ function recordToHoconValue(obj) {
979
1251
  }
980
1252
 
981
1253
  // src/internal/resolver/include-loader.ts
1254
+ var import_meta = {};
1255
+ var _require = (0, import_node_module.createRequire)(import_meta.url);
1256
+ function isPnpPath(p) {
1257
+ return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1258
+ }
1259
+ function validatePackageFile(file, identifier) {
1260
+ if (file.length === 0) {
1261
+ throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
1262
+ }
1263
+ if (file.includes("\\")) {
1264
+ throw new ParseError(
1265
+ `include package("${identifier}", "${file}"): backslash not allowed in file argument (use forward slash)`,
1266
+ 0,
1267
+ 0
1268
+ );
1269
+ }
1270
+ if (file.startsWith("/")) {
1271
+ throw new ParseError(
1272
+ `include package("${identifier}", "${file}"): absolute path not allowed in file argument`,
1273
+ 0,
1274
+ 0
1275
+ );
1276
+ }
1277
+ if (/(?:^|\/)\.\.?(?:\/|$)/u.test(file)) {
1278
+ throw new ParseError(
1279
+ `include package("${identifier}", "${file}"): path traversal (. or ..) not allowed in file argument`,
1280
+ 0,
1281
+ 0
1282
+ );
1283
+ }
1284
+ if (file.includes("//")) {
1285
+ throw new ParseError(
1286
+ `include package("${identifier}", "${file}"): consecutive slashes not allowed in file argument`,
1287
+ 0,
1288
+ 0
1289
+ );
1290
+ }
1291
+ }
1292
+ function validatePackageIdentifier(identifier) {
1293
+ if (identifier.length === 0) {
1294
+ throw new ParseError("include package: identifier argument must be non-empty", 0, 0);
1295
+ }
1296
+ }
1297
+ function defaultPackageResolver(identifier, file, includingFile, baseDir, resolveFrom) {
1298
+ const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
1299
+ let resolved;
1300
+ try {
1301
+ resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
1302
+ } catch {
1303
+ throw new PackageLookupError(
1304
+ `include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
1305
+ identifier,
1306
+ file,
1307
+ 0,
1308
+ 0
1309
+ );
1310
+ }
1311
+ if (isPnpPath(resolved)) {
1312
+ throw new ResolveError(
1313
+ `include package("${identifier}", "${file}"): Yarn Berry PnP store paths are not supported. Provide a custom packageResolver option to handle PnP resolution.`,
1314
+ `${identifier}/${file}`,
1315
+ 0,
1316
+ 0
1317
+ );
1318
+ }
1319
+ const fileParts = file.split("/");
1320
+ const expectedBasename = fileParts[fileParts.length - 1] ?? file;
1321
+ const resolvedBasename = nodePath.basename(resolved);
1322
+ if (resolvedBasename !== expectedBasename) {
1323
+ throw new PackageLookupError(
1324
+ `include package("${identifier}", "${file}"): case mismatch \u2014 resolved "${resolvedBasename}" but expected "${expectedBasename}" (E11 decision 5: case-sensitive)`,
1325
+ identifier,
1326
+ file,
1327
+ 0,
1328
+ 0
1329
+ );
1330
+ }
1331
+ return resolved;
1332
+ }
982
1333
  var IncludeLoader = class {
983
1334
  constructor(opts) {
984
1335
  this.opts = opts;
@@ -1058,8 +1409,77 @@ var IncludeLoader = class {
1058
1409
  }
1059
1410
  return merged;
1060
1411
  }
1412
+ /**
1413
+ * Resolve and load `include package("identifier", "file")`.
1414
+ * Validates the identifier and file arguments, resolves via the package resolver,
1415
+ * performs cycle detection using a JSON-serialised cycle key, and parses the content.
1416
+ */
1417
+ // `required` is reserved for future optional-package semantics (E11 decision 7);
1418
+ // currently all package lookup failures throw regardless of this flag.
1419
+ loadPackage(identifier, file, _required) {
1420
+ validatePackageIdentifier(identifier);
1421
+ validatePackageFile(file, identifier);
1422
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFileSync: readFileSync2 } = this.opts;
1423
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1424
+ if (includeStack.includes(cycleKey)) {
1425
+ throw new ResolveError(
1426
+ `circular include: package("${identifier}", "${file}")`,
1427
+ `${identifier}/${file}`,
1428
+ 0,
1429
+ 0
1430
+ );
1431
+ }
1432
+ if (includeStack.length >= 50) {
1433
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1434
+ }
1435
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1436
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1437
+ const content = readFileSync2(resolvedPath);
1438
+ if (content.length === 0) return makeResObj();
1439
+ const tokens = tokenize(content);
1440
+ assertNonEmptyDocument(tokens, resolvedPath);
1441
+ const ast = parseTokens(tokens);
1442
+ return this.onBuildResObj(ast, {
1443
+ ...this.opts,
1444
+ baseDir: nodePath.dirname(resolvedPath),
1445
+ includeStack: [...includeStack, cycleKey]
1446
+ });
1447
+ }
1448
+ /**
1449
+ * Async variant of `loadPackage`.
1450
+ */
1451
+ async loadPackageAsync(identifier, file, _required) {
1452
+ validatePackageIdentifier(identifier);
1453
+ validatePackageFile(file, identifier);
1454
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFile, readFileSync: readFileSync2 } = this.opts;
1455
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1456
+ if (includeStack.includes(cycleKey)) {
1457
+ throw new ResolveError(
1458
+ `circular include: package("${identifier}", "${file}")`,
1459
+ `${identifier}/${file}`,
1460
+ 0,
1461
+ 0
1462
+ );
1463
+ }
1464
+ if (includeStack.length >= 50) {
1465
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1466
+ }
1467
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1468
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1469
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1470
+ const content = await read(resolvedPath);
1471
+ if (content.length === 0) return makeResObj();
1472
+ const tokens = tokenize(content);
1473
+ assertNonEmptyDocument(tokens, resolvedPath);
1474
+ const ast = parseTokens(tokens);
1475
+ return this.onBuildResObjAsync(ast, {
1476
+ ...this.opts,
1477
+ baseDir: nodePath.dirname(resolvedPath),
1478
+ includeStack: [...includeStack, cycleKey]
1479
+ });
1480
+ }
1061
1481
  loadSingle(candidate) {
1062
- const { readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1482
+ const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
1063
1483
  if (includeStack.includes(candidate)) {
1064
1484
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1065
1485
  }
@@ -1073,16 +1493,17 @@ var IncludeLoader = class {
1073
1493
  if (candidate.endsWith(".properties")) {
1074
1494
  return hoconValueToResObj(propertiesToHoconValue(content));
1075
1495
  }
1076
- const ast = parseTokens(tokenize(content));
1496
+ const tokens = tokenize(content);
1497
+ assertNonEmptyDocument(tokens, candidate);
1498
+ const ast = parseTokens(tokens);
1077
1499
  return this.onBuildResObj(ast, {
1078
- env,
1500
+ ...this.opts,
1079
1501
  baseDir: nodePath.dirname(candidate),
1080
- readFileSync: readFileSync2,
1081
1502
  includeStack: [...includeStack, candidate]
1082
1503
  });
1083
1504
  }
1084
1505
  async loadSingleAsync(candidate) {
1085
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1506
+ const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
1086
1507
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1087
1508
  if (includeStack.includes(candidate)) {
1088
1509
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
@@ -1097,12 +1518,12 @@ var IncludeLoader = class {
1097
1518
  if (candidate.endsWith(".properties")) {
1098
1519
  return hoconValueToResObj(propertiesToHoconValue(content));
1099
1520
  }
1100
- const ast = parseTokens(tokenize(content));
1521
+ const tokens = tokenize(content);
1522
+ assertNonEmptyDocument(tokens, candidate);
1523
+ const ast = parseTokens(tokens);
1101
1524
  return this.onBuildResObjAsync(ast, {
1102
- env,
1525
+ ...this.opts,
1103
1526
  baseDir: nodePath.dirname(candidate),
1104
- readFileSync: readFileSync2,
1105
- readFile,
1106
1527
  includeStack: [...includeStack, candidate]
1107
1528
  });
1108
1529
  }
@@ -1111,7 +1532,6 @@ var IncludeLoader = class {
1111
1532
  // src/internal/resolver/structure-builder.ts
1112
1533
  var StructureBuilder = class _StructureBuilder {
1113
1534
  constructor(opts) {
1114
- this.opts = opts;
1115
1535
  this.loader = new IncludeLoader(opts);
1116
1536
  this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1117
1537
  this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
@@ -1138,7 +1558,13 @@ var StructureBuilder = class _StructureBuilder {
1138
1558
  }
1139
1559
  applyField(obj, field, pathPrefix) {
1140
1560
  if (field.key.length === 0 && field.value.kind === "include") {
1141
- const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
1561
+ const { qualifier, path: path2, required } = field.value;
1562
+ let included;
1563
+ if (qualifier.kind === "package") {
1564
+ included = this.loader.loadPackage(qualifier.identifier, path2, required);
1565
+ } else {
1566
+ included = this.loader.load(path2, required, qualifier.kind === "file");
1567
+ }
1142
1568
  if (pathPrefix.length > 0) {
1143
1569
  this.relativizeResObj(included, pathPrefix);
1144
1570
  }
@@ -1180,7 +1606,13 @@ var StructureBuilder = class _StructureBuilder {
1180
1606
  }
1181
1607
  async applyFieldAsync(obj, field, pathPrefix) {
1182
1608
  if (field.key.length === 0 && field.value.kind === "include") {
1183
- const included = await this.loader.loadAsync(field.value.path, field.value.required, field.value.isFile);
1609
+ const { qualifier, path: path2, required } = field.value;
1610
+ let included;
1611
+ if (qualifier.kind === "package") {
1612
+ included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
1613
+ } else {
1614
+ included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
1615
+ }
1184
1616
  if (pathPrefix.length > 0) {
1185
1617
  this.relativizeResObj(included, pathPrefix);
1186
1618
  }
@@ -1234,9 +1666,9 @@ var StructureBuilder = class _StructureBuilder {
1234
1666
  return inner;
1235
1667
  }
1236
1668
  case "subst":
1237
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1669
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1238
1670
  case "concat":
1239
- return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1671
+ return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1240
1672
  case "include":
1241
1673
  return { kind: "scalar", raw: "null", valueType: "null" };
1242
1674
  }
@@ -1258,13 +1690,13 @@ var StructureBuilder = class _StructureBuilder {
1258
1690
  case "object":
1259
1691
  return await this.buildAsync(ast, pathPrefix);
1260
1692
  case "subst":
1261
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1693
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1262
1694
  case "concat": {
1263
1695
  const nodes = [];
1264
1696
  for (const n of ast.nodes) {
1265
1697
  nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1266
1698
  }
1267
- return { _kind: "concat-placeholder", nodes };
1699
+ return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
1268
1700
  }
1269
1701
  case "include":
1270
1702
  return { kind: "scalar", raw: "null", valueType: "null" };
@@ -1334,6 +1766,23 @@ var SubstitutionResolver = class {
1334
1766
  this.opts = opts;
1335
1767
  this.resolving = /* @__PURE__ */ new Set();
1336
1768
  this.cache = /* @__PURE__ */ new Map();
1769
+ // Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
1770
+ // Used by isSelfRef detection: fires only when found === a concat currently
1771
+ // mid-iteration (i.e. we are inside found's own resolution), preventing the
1772
+ // "foofoo" double-count without misfiring when an external field looks up the
1773
+ // same concat via a substitution (multi-reviewer convergence fix).
1774
+ //
1775
+ // Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
1776
+ // preservation for self-ref detection. Round-2 multi-agent-review surfaced a
1777
+ // false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
1778
+ // criterion was tightened to node-membership-in-iterating-set — strictly
1779
+ // narrower than path-equality. Spec amendment deferred to a follow-up
1780
+ // xx.hocon PR (see Phase 6 #3f close-out notes).
1781
+ this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1782
+ }
1783
+ /** Returns a prefix for error messages, including origin info when set. */
1784
+ originPrefix() {
1785
+ return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
1337
1786
  }
1338
1787
  resolve() {
1339
1788
  return this.resolveResObj(this.root);
@@ -1372,7 +1821,15 @@ var SubstitutionResolver = class {
1372
1821
  }
1373
1822
  resolveVal(v, scope) {
1374
1823
  if (isSubst(v)) return this.resolveSubst(v, scope);
1375
- if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1824
+ if (isConcat(v)) {
1825
+ this.resolvingConcats.add(v);
1826
+ try {
1827
+ const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
1828
+ return concatResult;
1829
+ } finally {
1830
+ this.resolvingConcats.delete(v);
1831
+ }
1832
+ }
1376
1833
  if (isAppend(v)) return this.resolveAppend(v, scope);
1377
1834
  if (isResObj(v)) return this.resolveResObj(v);
1378
1835
  const hv = v;
@@ -1386,15 +1843,12 @@ var SubstitutionResolver = class {
1386
1843
  }
1387
1844
  return hv;
1388
1845
  }
1389
- segmentsEqual(a, b) {
1390
- return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1391
- }
1392
1846
  resolveSubst(s, scope) {
1393
- const key = segmentsToKey(s.segments);
1847
+ const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1394
1848
  if (this.cache.has(key)) return this.cache.get(key);
1395
1849
  if (this.resolving.has(key)) {
1396
1850
  let prior;
1397
- if (s.prefixLen > 0) {
1851
+ if (s.segments.length > 1) {
1398
1852
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1399
1853
  const parentScope = lookupResObj(
1400
1854
  this.root,
@@ -1416,7 +1870,7 @@ var SubstitutionResolver = class {
1416
1870
  }
1417
1871
  if (s.optional) return void 0;
1418
1872
  throw new ResolveError(
1419
- `circular substitution: ${key}`,
1873
+ this.originPrefix() + `circular substitution: ${key}`,
1420
1874
  key,
1421
1875
  s.line,
1422
1876
  s.col
@@ -1426,29 +1880,31 @@ var SubstitutionResolver = class {
1426
1880
  try {
1427
1881
  const found = lookupPath(this.root, s.segments);
1428
1882
  if (found !== void 0) {
1429
- if (isSubst(found) || isConcat(found)) {
1430
- const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1431
- (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1432
- );
1433
- if (isSelfRef) {
1434
- let prior;
1435
- if (s.prefixLen > 0) {
1436
- const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1437
- const parentScope = lookupResObj(
1438
- this.root,
1439
- s.segments.slice(0, s.segments.length - 1)
1440
- );
1441
- prior = parentScope?.priorValues.get(leafSeg);
1442
- } else {
1443
- const rootSeg = s.segments[0]?.text ?? "";
1444
- prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1445
- }
1446
- if (prior !== void 0) {
1447
- const result2 = this.resolveVal(prior, scope);
1448
- if (result2 !== void 0) this.cache.set(key, result2);
1449
- return result2;
1450
- }
1883
+ if (isConcat(found) && this.resolvingConcats.has(found)) {
1884
+ let prior;
1885
+ if (s.segments.length > 1) {
1886
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1887
+ const parentScope = lookupResObj(
1888
+ this.root,
1889
+ s.segments.slice(0, s.segments.length - 1)
1890
+ );
1891
+ prior = parentScope?.priorValues.get(leafSeg);
1892
+ } else {
1893
+ const rootSeg = s.segments[0]?.text ?? "";
1894
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1895
+ }
1896
+ if (prior !== void 0) {
1897
+ const result2 = this.resolveVal(prior, scope);
1898
+ if (result2 !== void 0) this.cache.set(key, result2);
1899
+ return result2;
1451
1900
  }
1901
+ if (s.optional) return void 0;
1902
+ throw new ResolveError(
1903
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1904
+ key,
1905
+ s.line,
1906
+ s.col
1907
+ );
1452
1908
  }
1453
1909
  let result = this.resolveVal(found, scope);
1454
1910
  const effectiveLen = s.segments.length - s.prefixLen;
@@ -1477,16 +1933,45 @@ var SubstitutionResolver = class {
1477
1933
  if (result !== void 0) this.cache.set(key, result);
1478
1934
  return result;
1479
1935
  }
1480
- const envKey = s.segments.map((seg) => seg.text).join(".");
1481
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1482
- if (envVal !== void 0) {
1483
- const result = { kind: "scalar", raw: envVal, valueType: "string" };
1484
- this.cache.set(key, result);
1485
- return result;
1936
+ if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1937
+ const result = this.resolveEnvList(s);
1938
+ if (result !== void 0) {
1939
+ this.cache.set(key, result);
1940
+ return result;
1941
+ }
1942
+ if (s.optional) return void 0;
1943
+ const envBase = s.segments.map((seg) => seg.text).join(".");
1944
+ throw new ResolveError(
1945
+ `${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1946
+ key,
1947
+ s.line,
1948
+ s.col
1949
+ );
1950
+ } else if (s.listSuffix) {
1951
+ if (s.optional) return void 0;
1952
+ if (this.opts.allowUnresolved) return s;
1953
+ throw new ResolveError(
1954
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1955
+ key,
1956
+ s.line,
1957
+ s.col
1958
+ );
1959
+ }
1960
+ if (this.opts.useSystemEnvironment !== false) {
1961
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1962
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1963
+ if (envVal !== void 0) {
1964
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1965
+ this.cache.set(key, result);
1966
+ return result;
1967
+ }
1486
1968
  }
1487
1969
  if (s.optional) return void 0;
1970
+ if (this.opts.allowUnresolved) {
1971
+ return s;
1972
+ }
1488
1973
  throw new ResolveError(
1489
- `could not resolve substitution: \${${key}}`,
1974
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1490
1975
  key,
1491
1976
  s.line,
1492
1977
  s.col
@@ -1495,9 +1980,52 @@ var SubstitutionResolver = class {
1495
1980
  this.resolving.delete(key);
1496
1981
  }
1497
1982
  }
1498
- resolveConcat(nodes, scope) {
1499
- const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1500
- if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1983
+ /**
1984
+ * S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
1985
+ *
1986
+ * Candidate bases follow the same fully-qualified→bare order used by the scalar
1987
+ * env fallback. The first base whose _0 key is present wins entirely — no
1988
+ * cross-base merging (spec §Relativized fallback precedence rules).
1989
+ *
1990
+ * Returns the HoconValue Array if any candidate base has _0, or undefined if
1991
+ * none do (caller applies optional/required handling).
1992
+ */
1993
+ resolveEnvList(s) {
1994
+ const bases = this.candidateBases(s);
1995
+ for (const base of bases) {
1996
+ const key0 = base + "_0";
1997
+ if (!(key0 in this.opts.env)) continue;
1998
+ const items = [];
1999
+ for (let i = 0; ; i++) {
2000
+ const k = base + "_" + i;
2001
+ if (!(k in this.opts.env)) break;
2002
+ items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
2003
+ }
2004
+ return { kind: "array", items };
2005
+ }
2006
+ return void 0;
2007
+ }
2008
+ /**
2009
+ * Return the candidate base names for env-var-list lookup, in precedence order.
2010
+ * Mirrors the fully-qualified→bare fallback used by the scalar env path.
2011
+ */
2012
+ candidateBases(s) {
2013
+ const full = s.segments.map((seg) => seg.text).join(".");
2014
+ if (s.prefixLen === 0) return [full];
2015
+ const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
2016
+ return [full, bare];
2017
+ }
2018
+ resolveConcat(nodes, scope, line = 0, col = 0) {
2019
+ const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
2020
+ if (this.opts.allowUnresolved) {
2021
+ for (const v of rawResolved) {
2022
+ if (v !== void 0 && isSubst(v)) {
2023
+ return v;
2024
+ }
2025
+ }
2026
+ }
2027
+ const resolved = rawResolved.filter((v) => v !== void 0);
2028
+ if (resolved.length === 0) return void 0;
1501
2029
  if (resolved.length === 1) return resolved[0];
1502
2030
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1503
2031
  if (nonSep.length === 0) {
@@ -1513,27 +2041,45 @@ var SubstitutionResolver = class {
1513
2041
  if (converted !== null) {
1514
2042
  return { kind: "array", items: [...left.items, ...converted] };
1515
2043
  }
1516
- return { kind: "array", items: [...left.items, right] };
2044
+ throw new ResolveError(
2045
+ "cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
2046
+ "",
2047
+ line,
2048
+ col
2049
+ );
1517
2050
  }
1518
2051
  if (left.kind === "object" && right.kind === "array") {
1519
2052
  const converted = numericObjectToArray(left);
1520
2053
  if (converted !== null) {
1521
2054
  return { kind: "array", items: [...converted, ...right.items] };
1522
2055
  }
1523
- return { kind: "array", items: [left, ...right.items] };
2056
+ throw new ResolveError(
2057
+ "cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
2058
+ "",
2059
+ line,
2060
+ col
2061
+ );
1524
2062
  }
1525
2063
  if (left.kind === "array" && right.kind === "array") {
1526
2064
  return { kind: "array", items: [...left.items, ...right.items] };
1527
2065
  }
1528
- if (left.kind === "array") {
1529
- return { kind: "array", items: [...left.items, right] };
2066
+ if (left.kind === "array" || right.kind === "array") {
2067
+ throw new ResolveError(
2068
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
2069
+ "",
2070
+ line,
2071
+ col
2072
+ );
1530
2073
  }
1531
- if (right.kind === "array") {
1532
- return { kind: "array", items: [left, ...right.items] };
2074
+ if (left.kind === "object" || right.kind === "object") {
2075
+ throw new ResolveError(
2076
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
2077
+ "",
2078
+ line,
2079
+ col
2080
+ );
1533
2081
  }
1534
- const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1535
- const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1536
- return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
2082
+ return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
1537
2083
  };
1538
2084
  const [head, ...tail] = nonSep;
1539
2085
  const folded = tail.reduce(joinPair, head);
@@ -1553,15 +2099,89 @@ var SubstitutionResolver = class {
1553
2099
  };
1554
2100
 
1555
2101
  // src/internal/resolver/resolver.ts
2102
+ function buildTree(ast, opts) {
2103
+ return new StructureBuilder(opts).build(ast);
2104
+ }
2105
+ function resolveTree(tree, opts) {
2106
+ return new SubstitutionResolver(tree, opts).resolve();
2107
+ }
2108
+ function containsPlaceholders(tree) {
2109
+ for (const val of tree.fields.values()) {
2110
+ if (valContainsPlaceholders(val)) return true;
2111
+ }
2112
+ return false;
2113
+ }
2114
+ function valContainsPlaceholders(v) {
2115
+ const rv = v;
2116
+ if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2117
+ if (isResObj(rv)) return containsPlaceholders(rv);
2118
+ const hv = v;
2119
+ if (hv?.kind === "array") {
2120
+ return hv.items.some((item) => valContainsPlaceholders(item));
2121
+ }
2122
+ return false;
2123
+ }
2124
+ function buildPartialHoconFromResObj(tree) {
2125
+ const fields = /* @__PURE__ */ new Map();
2126
+ for (const [k, v] of tree.fields) {
2127
+ if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2128
+ if (isResObj(v)) {
2129
+ fields.set(k, buildPartialHoconFromResObj(v));
2130
+ } else {
2131
+ if (!valContainsPlaceholders(v)) {
2132
+ fields.set(k, v);
2133
+ }
2134
+ }
2135
+ }
2136
+ }
2137
+ return { kind: "object", fields };
2138
+ }
2139
+ function hoconValueToResObj2(value) {
2140
+ const obj = makeResObj();
2141
+ for (const [k, v] of value.fields) {
2142
+ if (v.kind === "object") {
2143
+ obj.fields.set(k, hoconValueToResObj2(v));
2144
+ } else {
2145
+ obj.fields.set(k, v);
2146
+ }
2147
+ }
2148
+ return obj;
2149
+ }
1556
2150
  function resolve2(ast, opts) {
1557
- const root = new StructureBuilder(opts).build(ast);
1558
- return new SubstitutionResolver(root, opts).resolve();
2151
+ const tree = buildTree(ast, opts);
2152
+ return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
1559
2153
  }
1560
2154
 
1561
2155
  // src/config.ts
1562
2156
  var Config = class _Config {
1563
- constructor(root) {
2157
+ constructor(root, opts) {
1564
2158
  this.root = root;
2159
+ this._resolved = opts?.resolved ?? true;
2160
+ this._parseBaseDir = opts?.parseBaseDir;
2161
+ this._originDescription = opts?.originDescription;
2162
+ this._resObjRoot = opts?.resObjRoot;
2163
+ this._resolveOpts = opts?.resolveOpts;
2164
+ }
2165
+ /** Returns true when the Config's value tree contains no unresolved
2166
+ * substitution placeholders. Whole-config granularity (E12 decision 11). */
2167
+ isResolved() {
2168
+ return this._resolved;
2169
+ }
2170
+ /** @internal Test-only: construct a resolved Config from a HoconValue. */
2171
+ static _fromResolvedValue(root, opts) {
2172
+ return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
2173
+ }
2174
+ /** @internal Construct an unresolved Config carrying a ResObj tree.
2175
+ * Used by parseStringWithOptions (deferred path) in T6. */
2176
+ static _fromUnresolvedResObj(tree, opts) {
2177
+ const partialRoot = buildPartialHoconFromResObj(tree);
2178
+ return new _Config(partialRoot, {
2179
+ resolved: opts.resolved,
2180
+ parseBaseDir: opts.parseBaseDir,
2181
+ originDescription: opts.originDescription,
2182
+ resObjRoot: tree,
2183
+ resolveOpts: opts.resolveOpts
2184
+ });
1565
2185
  }
1566
2186
  get(path2) {
1567
2187
  const v = this.lookupNode(path2);
@@ -1600,17 +2220,34 @@ var Config = class _Config {
1600
2220
  }
1601
2221
  const result = parseBytes(v.raw, unit);
1602
2222
  if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
2223
+ if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
1603
2224
  return result;
1604
2225
  }
1605
2226
  getConfig(path2) {
1606
2227
  const v = this.lookupNode(path2);
1607
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2228
+ if (v === void 0) {
2229
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2230
+ throw new NotResolvedError(path2);
2231
+ }
2232
+ throw new ConfigError(`path not found: ${path2}`, path2);
2233
+ }
1608
2234
  if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
1609
- return new _Config(v);
2235
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2236
+ throw new NotResolvedError(path2);
2237
+ }
2238
+ return new _Config(v, { resolved: this._resolved });
1610
2239
  }
1611
2240
  getList(path2) {
1612
2241
  const v = this.lookupNode(path2);
1613
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2242
+ if (v === void 0) {
2243
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2244
+ throw new NotResolvedError(path2);
2245
+ }
2246
+ throw new ConfigError(`path not found: ${path2}`, path2);
2247
+ }
2248
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2249
+ throw new NotResolvedError(path2);
2250
+ }
1614
2251
  if (v.kind === "object") {
1615
2252
  const converted = numericObjectToArray(v);
1616
2253
  if (converted !== null) return converted.map(hoconToJs);
@@ -1625,12 +2262,138 @@ var Config = class _Config {
1625
2262
  return [...this.root.fields.keys()];
1626
2263
  }
1627
2264
  withFallback(fallback) {
1628
- const merged = deepMergeHocon(this.root, fallback.root);
1629
- return new _Config(merged);
2265
+ if (fallback == null) return this;
2266
+ const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2267
+ const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
2268
+ const merged = mergeUnresolved(selfTree, fbTree);
2269
+ const hasPlaceholders = containsPlaceholders(merged);
2270
+ const partialRoot = buildPartialHoconFromResObj(merged);
2271
+ return new _Config(partialRoot, {
2272
+ resolved: !hasPlaceholders,
2273
+ parseBaseDir: this._parseBaseDir,
2274
+ originDescription: this._originDescription,
2275
+ resObjRoot: merged,
2276
+ resolveOpts: this._resolveOpts ?? fallback._resolveOpts
2277
+ });
2278
+ }
2279
+ /**
2280
+ * Runs substitution resolution (phase 2) on the stored unresolved tree.
2281
+ * Idempotent: calling resolve() on an already-resolved Config returns an
2282
+ * equivalent resolved Config.
2283
+ *
2284
+ * opts.allowUnresolved (default false) — when true, leaves unresolved
2285
+ * non-optional placeholders in place rather than throwing.
2286
+ * opts.useSystemEnvironment (default true) — when false, env var lookups
2287
+ * are suppressed (hermetic resolution).
2288
+ *
2289
+ * E12 decision 3.
2290
+ */
2291
+ resolve(opts = {}) {
2292
+ if (this._resolved) {
2293
+ return new _Config(this.root, {
2294
+ resolved: true,
2295
+ parseBaseDir: this._parseBaseDir,
2296
+ originDescription: this._originDescription
2297
+ });
2298
+ }
2299
+ const tree = this._resObjRoot;
2300
+ if (!tree) {
2301
+ return new _Config(this.root, { resolved: true });
2302
+ }
2303
+ const resolveOpts = {
2304
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2305
+ throw new Error("no files");
2306
+ } },
2307
+ allowUnresolved: opts.allowUnresolved ?? false,
2308
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2309
+ originDescription: this._originDescription
2310
+ };
2311
+ const resolved = resolveTree(tree, resolveOpts);
2312
+ if (resolved.kind !== "object") throw new Error("resolve: expected object root");
2313
+ if (opts.allowUnresolved) {
2314
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
2315
+ return new _Config(stripped, {
2316
+ resolved: !hadPlaceholders,
2317
+ parseBaseDir: this._parseBaseDir,
2318
+ originDescription: this._originDescription,
2319
+ resObjRoot: hadPlaceholders ? tree : void 0,
2320
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2321
+ });
2322
+ }
2323
+ return new _Config(resolved, {
2324
+ resolved: true,
2325
+ parseBaseDir: this._parseBaseDir,
2326
+ originDescription: this._originDescription
2327
+ });
2328
+ }
2329
+ /**
2330
+ * Resolves receiver substitutions using source as lookup context.
2331
+ * Source's keys are NOT merged into the result — differs from
2332
+ * `this.withFallback(source).resolve()` which includes source keys.
2333
+ *
2334
+ * Precondition (E12 decision 10): source must be resolved. If
2335
+ * source.isResolved() is false, throws NotResolvedError.
2336
+ *
2337
+ * On an already-resolved receiver, resolveWith is a no-op (returns an
2338
+ * equivalent resolved Config, source keys not included).
2339
+ *
2340
+ * E12 decisions 9, 10.
2341
+ */
2342
+ resolveWith(source, opts = {}) {
2343
+ if (!source.isResolved()) {
2344
+ throw new NotResolvedError("source");
2345
+ }
2346
+ if (this._resolved) {
2347
+ return new _Config(this.root, {
2348
+ resolved: true,
2349
+ parseBaseDir: this._parseBaseDir,
2350
+ originDescription: this._originDescription
2351
+ });
2352
+ }
2353
+ const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2354
+ const srcTree = hoconValueToResObj2(source.root);
2355
+ const merged = mergeUnresolved(receiverTree, srcTree);
2356
+ const resolveOpts = {
2357
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2358
+ throw new Error("no files");
2359
+ } },
2360
+ allowUnresolved: opts.allowUnresolved ?? false,
2361
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2362
+ originDescription: this._originDescription
2363
+ };
2364
+ const resolved = resolveTree(merged, resolveOpts);
2365
+ if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
2366
+ const receiverShapeForFilter = resObjToKeyShape(receiverTree);
2367
+ const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
2368
+ if (opts.allowUnresolved) {
2369
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
2370
+ return new _Config(stripped, {
2371
+ resolved: !hadPlaceholders,
2372
+ parseBaseDir: this._parseBaseDir,
2373
+ originDescription: this._originDescription,
2374
+ resObjRoot: hadPlaceholders ? receiverTree : void 0,
2375
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2376
+ });
2377
+ }
2378
+ return new _Config(filtered, {
2379
+ resolved: true,
2380
+ parseBaseDir: this._parseBaseDir,
2381
+ originDescription: this._originDescription
2382
+ });
1630
2383
  }
1631
2384
  toObject() {
1632
2385
  return hoconToJs(this.root);
1633
2386
  }
2387
+ /**
2388
+ * @internal Test-only: renders this resolved Config's value tree as canonical
2389
+ * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
2390
+ * against Lightbend ground truth. NOT part of the public API.
2391
+ *
2392
+ * Throws if the Config is unresolved (contains placeholders).
2393
+ */
2394
+ _renderJSONForTest() {
2395
+ return renderHoconAsJSON(this.root);
2396
+ }
1634
2397
  lookupNode(path2) {
1635
2398
  const segments = splitConfigPath(path2);
1636
2399
  let current = this.root;
@@ -1642,9 +2405,40 @@ var Config = class _Config {
1642
2405
  }
1643
2406
  return current;
1644
2407
  }
2408
+ /**
2409
+ * _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
2410
+ * return true if the subtree there contains any unresolved placeholder.
2411
+ * Used by getConfig / getList (T2) to detect transitive unresolved placeholders
2412
+ * in object / array subtrees that would otherwise appear to be "resolved" in the
2413
+ * partial HoconValue root (because individual leaf fields are omitted rather than
2414
+ * the whole subtree when their value is a direct placeholder scalar, but object
2415
+ * sub-containers are retained in the partial root with the placeholder-keyed
2416
+ * fields omitted from them — so the object appears "present but empty-ish").
2417
+ */
2418
+ _resObjRootSubtreeHasPlaceholders(path2) {
2419
+ const tree = this._resObjRoot;
2420
+ if (!tree) return false;
2421
+ const segments = splitConfigPath(path2);
2422
+ let cur = tree;
2423
+ for (const seg of segments) {
2424
+ const val = cur.fields.get(seg);
2425
+ if (val === void 0) return false;
2426
+ if (isResObj(val)) {
2427
+ cur = val;
2428
+ } else {
2429
+ return valContainsPlaceholders(val);
2430
+ }
2431
+ }
2432
+ return containsPlaceholders(cur);
2433
+ }
1645
2434
  requireScalar(path2) {
1646
2435
  const v = this.lookupNode(path2);
1647
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2436
+ if (v === void 0) {
2437
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2438
+ throw new NotResolvedError(path2);
2439
+ }
2440
+ throw new ConfigError(`path not found: ${path2}`, path2);
2441
+ }
1648
2442
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
1649
2443
  return v;
1650
2444
  }
@@ -1707,28 +2501,89 @@ function hoconToJs(v) {
1707
2501
  case "array":
1708
2502
  return v.items.map(hoconToJs);
1709
2503
  case "object": {
1710
- const obj = {};
2504
+ const obj = /* @__PURE__ */ Object.create(null);
1711
2505
  for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
1712
- return obj;
2506
+ return Object.assign({}, obj);
1713
2507
  }
1714
2508
  }
1715
2509
  }
1716
- function deepMergeHocon(receiver, fallback) {
1717
- const merged = new Map(fallback.fields);
1718
- for (const [k, v] of receiver.fields) {
1719
- const fb = merged.get(k);
1720
- if (fb?.kind === "object" && v.kind === "object") {
1721
- merged.set(k, deepMergeHocon(v, fb));
2510
+ function resObjToKeyShape(tree) {
2511
+ const fields = /* @__PURE__ */ new Map();
2512
+ for (const [k, v] of tree.fields) {
2513
+ if (isSubst(v) || isConcat(v) || isAppend(v)) {
2514
+ fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2515
+ } else if (isResObj(v)) {
2516
+ fields.set(k, resObjToKeyShape(v));
1722
2517
  } else {
1723
- merged.set(k, v);
2518
+ fields.set(k, v);
1724
2519
  }
1725
2520
  }
1726
- return { kind: "object", fields: merged };
2521
+ return { kind: "object", fields };
2522
+ }
2523
+ function stripPlaceholderFields(v) {
2524
+ const fields = /* @__PURE__ */ new Map();
2525
+ let hadPlaceholders = false;
2526
+ for (const [k, val] of v.fields) {
2527
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2528
+ hadPlaceholders = true;
2529
+ } else if (val.kind === "object") {
2530
+ const inner = stripPlaceholderFields(val);
2531
+ if (inner.hadPlaceholders) hadPlaceholders = true;
2532
+ fields.set(k, inner.stripped);
2533
+ } else {
2534
+ fields.set(k, val);
2535
+ }
2536
+ }
2537
+ return { stripped: { kind: "object", fields }, hadPlaceholders };
2538
+ }
2539
+ function filterByReceiverShape(resolved, receiverShape) {
2540
+ const fields = /* @__PURE__ */ new Map();
2541
+ for (const [k, rv] of resolved.fields) {
2542
+ if (!receiverShape.fields.has(k)) continue;
2543
+ const receiverVal = receiverShape.fields.get(k);
2544
+ if (rv.kind === "object" && receiverVal.kind === "object") {
2545
+ fields.set(k, filterByReceiverShape(rv, receiverVal));
2546
+ } else {
2547
+ fields.set(k, rv);
2548
+ }
2549
+ }
2550
+ return { kind: "object", fields };
2551
+ }
2552
+ function renderHoconAsJSON(v) {
2553
+ switch (v.kind) {
2554
+ case "scalar": {
2555
+ switch (v.valueType) {
2556
+ case "null":
2557
+ return "null";
2558
+ case "boolean":
2559
+ return v.raw;
2560
+ case "number": {
2561
+ const n = Number(v.raw);
2562
+ if (Number.isFinite(n)) return v.raw;
2563
+ return JSON.stringify(v.raw);
2564
+ }
2565
+ case "string":
2566
+ return JSON.stringify(v.raw);
2567
+ }
2568
+ break;
2569
+ }
2570
+ case "array": {
2571
+ const items = v.items.map(renderHoconAsJSON);
2572
+ return `[${items.join(",")}]`;
2573
+ }
2574
+ case "object": {
2575
+ const keys = [...v.fields.keys()].sort();
2576
+ const pairs = keys.map((k) => {
2577
+ const val = v.fields.get(k);
2578
+ return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
2579
+ });
2580
+ return `{${pairs.join(",")}}`;
2581
+ }
2582
+ }
2583
+ throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
1727
2584
  }
1728
2585
 
1729
2586
  // src/parse.ts
1730
- var fs = __toESM(require("fs"), 1);
1731
- var path = __toESM(require("path"), 1);
1732
2587
  function getEnv(opts) {
1733
2588
  if (opts.env !== void 0) return opts.env;
1734
2589
  if (typeof process !== "undefined" && process.env) return process.env;
@@ -1739,12 +2594,16 @@ function defaultReadFileSync(filePath) {
1739
2594
  }
1740
2595
  function buildResolveContext(input, opts) {
1741
2596
  const tokens = tokenize(input);
2597
+ assertNonEmptyDocument(tokens, "input");
1742
2598
  const ast = parseTokens(tokens);
1743
2599
  const resolveOpts = {
1744
2600
  env: getEnv(opts),
1745
2601
  baseDir: opts.baseDir,
1746
2602
  readFileSync: opts.readFileSync ?? defaultReadFileSync,
1747
- readFile: opts.readFile
2603
+ readFile: opts.readFile,
2604
+ originDescription: opts.originDescription,
2605
+ resolveFrom: opts.resolveFrom,
2606
+ packageResolver: opts.packageResolver
1748
2607
  };
1749
2608
  return { ast, resolveOpts };
1750
2609
  }
@@ -1752,7 +2611,7 @@ function parse(input, opts = {}) {
1752
2611
  const { ast, resolveOpts } = buildResolveContext(input, opts);
1753
2612
  const value = resolve2(ast, resolveOpts);
1754
2613
  if (value.kind !== "object") throw new Error("resolved value is not an object");
1755
- return new Config(value);
2614
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
1756
2615
  }
1757
2616
  function parseFile(filePath, opts = {}) {
1758
2617
  if (typeof process === "undefined" && !opts.readFileSync) {