@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.js CHANGED
@@ -1,5 +1,26 @@
1
1
  // src/coerce.ts
2
2
  var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
3
+ function isHoconWs(cp) {
4
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
5
+ if (cp >= 28 && cp <= 31) return true;
6
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
7
+ if (cp === 5760) return true;
8
+ if (cp >= 8192 && cp <= 8202) return true;
9
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
10
+ if (cp === 12288) return true;
11
+ return false;
12
+ }
13
+ function trimHoconWs(s) {
14
+ let start = 0;
15
+ let end = s.length;
16
+ while (start < end && isHoconWs(s.charCodeAt(start))) {
17
+ start++;
18
+ }
19
+ while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
20
+ end--;
21
+ }
22
+ return s.slice(start, end);
23
+ }
3
24
  var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
4
25
  var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
5
26
  function coerceBoolean(value) {
@@ -45,22 +66,29 @@ var OUTPUT_DURATION_UNITS = {
45
66
  d: 864e5
46
67
  };
47
68
  function parseDuration(value, outputUnit = "ms") {
48
- const trimmed = value.trim();
69
+ const trimmed = trimHoconWs(value);
49
70
  let i = 0;
50
71
  while (i < trimmed.length) {
51
72
  const ch = trimmed[i];
52
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
73
+ if (i === 0 && (ch === "-" || ch === "+")) {
74
+ i++;
75
+ continue;
76
+ }
77
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
53
78
  i++;
54
79
  }
55
80
  if (i === 0) return NaN;
56
81
  const num = Number(trimmed.slice(0, i));
57
82
  if (Number.isNaN(num)) return NaN;
58
- const unit = trimmed.slice(i).trim().toLowerCase();
83
+ const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
84
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
85
+ if (divisor === void 0) return NaN;
86
+ if (unit === "") {
87
+ return num / divisor;
88
+ }
59
89
  const mult = DURATION_UNITS[unit];
60
90
  if (mult === void 0) return NaN;
61
91
  const ms = num * mult;
62
- const divisor = OUTPUT_DURATION_UNITS[outputUnit];
63
- if (divisor === void 0) return NaN;
64
92
  return ms / divisor;
65
93
  }
66
94
  var BYTE_UNITS = {
@@ -91,6 +119,22 @@ var BYTE_UNITS = {
91
119
  TiB: 1099511627776,
92
120
  tebibyte: 1099511627776,
93
121
  tebibytes: 1099511627776,
122
+ // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
123
+ // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
124
+ // Both upper- and lowercase accepted per Lightbend behaviour.
125
+ // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
126
+ K: 1024,
127
+ k: 1024,
128
+ M: 1024 ** 2,
129
+ m: 1024 ** 2,
130
+ G: 1024 ** 3,
131
+ g: 1024 ** 3,
132
+ T: 1024 ** 4,
133
+ t: 1024 ** 4,
134
+ P: 1024 ** 5,
135
+ p: 1024 ** 5,
136
+ E: 1024 ** 6,
137
+ e: 1024 ** 6,
94
138
  // lowercase short-form aliases
95
139
  b: 1,
96
140
  kb: 1e3,
@@ -114,29 +158,47 @@ var OUTPUT_BYTE_UNITS = {
114
158
  TiB: 1099511627776
115
159
  };
116
160
  function parseBytes(value, outputUnit = "B") {
117
- const trimmed = value.trim();
161
+ const trimmed = trimHoconWs(value);
118
162
  let i = 0;
119
163
  while (i < trimmed.length) {
120
164
  const ch = trimmed[i];
121
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
165
+ if (i === 0 && (ch === "-" || ch === "+")) {
166
+ i++;
167
+ continue;
168
+ }
169
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
122
170
  i++;
123
171
  }
124
172
  if (i === 0) return NaN;
125
173
  const num = Number(trimmed.slice(0, i));
126
174
  if (Number.isNaN(num)) return NaN;
127
- const unit = trimmed.slice(i).trim();
175
+ const unit = trimHoconWs(trimmed.slice(i));
176
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
177
+ if (divisor === void 0) return NaN;
178
+ if (unit === "") {
179
+ const bytes2 = Math.trunc(num);
180
+ if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
181
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
182
+ }
183
+ return bytes2 / divisor;
184
+ }
128
185
  let mult = BYTE_UNITS[unit];
129
186
  if (mult === void 0) {
130
187
  mult = BYTE_UNITS[unit.toLowerCase()];
131
188
  }
132
189
  if (mult === void 0) return NaN;
133
190
  const bytes = num * mult;
134
- const divisor = OUTPUT_BYTE_UNITS[outputUnit];
135
- if (divisor === void 0) return NaN;
191
+ if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
192
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
193
+ }
136
194
  const result = bytes / divisor;
137
195
  return outputUnit === "B" ? Math.round(result) : result;
138
196
  }
139
197
 
198
+ // src/parse.ts
199
+ import * as fs from "fs";
200
+ import * as path from "path";
201
+
140
202
  // src/errors.ts
141
203
  var ParseError = class extends Error {
142
204
  constructor(message, line, col, file) {
@@ -157,6 +219,14 @@ var ResolveError = class extends Error {
157
219
  this.name = "ResolveError";
158
220
  }
159
221
  };
222
+ var PackageLookupError = class extends ResolveError {
223
+ constructor(message, identifier, packageFile, line, col) {
224
+ super(message, `${identifier}/${packageFile}`, line, col);
225
+ this.identifier = identifier;
226
+ this.packageFile = packageFile;
227
+ this.name = "PackageLookupError";
228
+ }
229
+ };
160
230
  var ConfigError = class extends Error {
161
231
  constructor(message, path2) {
162
232
  super(message);
@@ -164,6 +234,156 @@ var ConfigError = class extends Error {
164
234
  this.name = "ConfigError";
165
235
  }
166
236
  };
237
+ var NotResolvedError = class extends ConfigError {
238
+ constructor(path2) {
239
+ super(`value at path "${path2}" is not resolved (call resolve() first)`, path2);
240
+ this.name = "NotResolvedError";
241
+ }
242
+ };
243
+
244
+ // src/internal/resolver/types.ts
245
+ var separatorValues = /* @__PURE__ */ new WeakSet();
246
+ function isSubst(v) {
247
+ return v._kind === "subst-placeholder";
248
+ }
249
+ function isConcat(v) {
250
+ return v._kind === "concat-placeholder";
251
+ }
252
+ function isAppend(v) {
253
+ return v._kind === "append-placeholder";
254
+ }
255
+ function isResObj(v) {
256
+ return v._kind === "res-obj";
257
+ }
258
+ function makeResObj() {
259
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
260
+ }
261
+ function mergeUnresolved(receiver, fallback) {
262
+ const result = makeResObj();
263
+ for (const [k, v] of fallback.fields) {
264
+ result.fields.set(k, v);
265
+ }
266
+ for (const [k, v] of fallback.priorValues) {
267
+ result.priorValues.set(k, v);
268
+ }
269
+ for (const [k, rv] of receiver.fields) {
270
+ const existing = result.fields.get(k);
271
+ if (existing !== void 0) {
272
+ const recPrior = receiver.priorValues.get(k);
273
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
274
+ result.fields.set(k, rv);
275
+ continue;
276
+ }
277
+ if (isResObj(rv) && isResObj(existing)) {
278
+ result.fields.set(k, mergeUnresolved(rv, existing));
279
+ continue;
280
+ }
281
+ result.priorValues.set(k, existing);
282
+ }
283
+ result.fields.set(k, rv);
284
+ }
285
+ for (const [k, v] of receiver.priorValues) {
286
+ if (receiver.fields.has(k)) {
287
+ result.priorValues.set(k, v);
288
+ }
289
+ }
290
+ return result;
291
+ }
292
+
293
+ // src/internal/resolver/utils.ts
294
+ function segmentsToKey(segments) {
295
+ return segments.map((s) => {
296
+ const t = s.text;
297
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
298
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
299
+ }
300
+ return t;
301
+ }).join(".");
302
+ }
303
+ function lookupPath(root, segments) {
304
+ const [head, ...tail] = segments;
305
+ if (head === void 0) return void 0;
306
+ const key = head.text;
307
+ const val = root.fields.get(key);
308
+ if (val === void 0) return void 0;
309
+ if (tail.length === 0) return val;
310
+ if (isResObj(val)) return lookupPath(val, tail);
311
+ return void 0;
312
+ }
313
+ function lookupResObj(root, segments) {
314
+ let cur = root;
315
+ for (const seg of segments) {
316
+ const val = cur.fields.get(seg.text);
317
+ if (val === void 0 || !isResObj(val)) return void 0;
318
+ cur = val;
319
+ }
320
+ return cur;
321
+ }
322
+ function deepMergeHoconValues(base, overlay) {
323
+ const merged = new Map(base.fields);
324
+ for (const [k, v] of overlay.fields) {
325
+ const existing = merged.get(k);
326
+ if (existing?.kind === "object" && v.kind === "object") {
327
+ merged.set(k, deepMergeHoconValues(existing, v));
328
+ } else {
329
+ merged.set(k, v);
330
+ }
331
+ }
332
+ return { kind: "object", fields: merged };
333
+ }
334
+ function deepMergeResObjInto(dst, src) {
335
+ for (const [k, srcVal] of src.fields) {
336
+ const dstVal = dst.fields.get(k);
337
+ if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
338
+ deepMergeResObjInto(dstVal, srcVal);
339
+ } else {
340
+ if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
341
+ dst.fields.set(k, srcVal);
342
+ }
343
+ }
344
+ for (const [k, srcPrior] of src.priorValues) {
345
+ if (!dst.priorValues.has(k)) {
346
+ dst.priorValues.set(k, srcPrior);
347
+ }
348
+ }
349
+ }
350
+ function hoconValueToResObj(hv) {
351
+ const obj = makeResObj();
352
+ if (hv.kind !== "object") return obj;
353
+ for (const [key, val] of hv.fields) {
354
+ if (val.kind === "object") {
355
+ obj.fields.set(key, hoconValueToResObj(val));
356
+ } else {
357
+ obj.fields.set(key, val);
358
+ }
359
+ }
360
+ return obj;
361
+ }
362
+ function isFileNotFoundError(e) {
363
+ if (!(e instanceof Error)) return false;
364
+ const code = e.code;
365
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
366
+ const msg = e.message.toLowerCase();
367
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
368
+ }
369
+
370
+ // src/internal/resolver/include-loader.ts
371
+ import { createRequire } from "module";
372
+ import * as nodePath from "path";
373
+
374
+ // src/internal/parser/empty-check.ts
375
+ function hasContentTokens(tokens) {
376
+ return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
377
+ }
378
+ function assertNonEmptyDocument(tokens, sourceDescriptor) {
379
+ if (!hasContentTokens(tokens)) {
380
+ throw new ParseError(
381
+ `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
382
+ 1,
383
+ 1
384
+ );
385
+ }
386
+ }
167
387
 
168
388
  // src/internal/lexer/lexer.ts
169
389
  function isHoconWhitespace(ch) {
@@ -292,14 +512,6 @@ var Lexer = class {
292
512
  continue;
293
513
  }
294
514
  if (isUnquotedStart(ch)) {
295
- if (ch === "-" && !isDecimalDigit(this.peek(1))) {
296
- const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
297
- throw new ParseError(
298
- `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
299
- sl,
300
- sc
301
- );
302
- }
303
515
  let value = "";
304
516
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
305
517
  value += this.advance();
@@ -403,6 +615,10 @@ var Lexer = class {
403
615
  /**
404
616
  * Parse the body of a `${...}` substitution (called after `${` has been consumed).
405
617
  * Implements Appendix A state machine from the cross-language spec.
618
+ *
619
+ * S13c extension: if the segment-collection loop encounters `[`, it delegates to
620
+ * `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
621
+ * ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
406
622
  */
407
623
  parseSubstBody(startLine, startCol) {
408
624
  let optional = false;
@@ -417,6 +633,7 @@ var Lexer = class {
417
633
  let pendingWs = "";
418
634
  const segments = [];
419
635
  let lastDot = null;
636
+ let listSuffix = false;
420
637
  while (true) {
421
638
  if (this.pos >= this.input.length) {
422
639
  throw new ParseError("unterminated substitution", startLine, startCol);
@@ -426,6 +643,23 @@ var Lexer = class {
426
643
  this.advance();
427
644
  pendingWs = "";
428
645
  break;
646
+ } else if (ch === "[") {
647
+ if (!curStarted) {
648
+ throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
649
+ }
650
+ for (const w of pendingWs) {
651
+ if (w !== " " && w !== " ") {
652
+ throw new ParseError(
653
+ `only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
654
+ startLine,
655
+ this.col
656
+ );
657
+ }
658
+ }
659
+ segments.push({ text: curText, line: curLine, col: curCol });
660
+ pendingWs = "";
661
+ listSuffix = this.parseListSuffix(startLine);
662
+ break;
429
663
  } else if (ch === '"') {
430
664
  const qLine = startLine;
431
665
  const qCol = this.col;
@@ -485,15 +719,47 @@ var Lexer = class {
485
719
  throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
486
720
  }
487
721
  }
488
- if (curStarted) {
489
- segments.push({ text: curText, line: curLine, col: curCol });
490
- } else if (segments.length === 0) {
491
- throw new ParseError("empty substitution path", startLine, startCol);
492
- } else {
493
- const [errLine, errCol] = lastDot ?? [startLine, startCol];
494
- throw new ParseError("empty segment in path", errLine, errCol);
722
+ if (!listSuffix) {
723
+ if (curStarted) {
724
+ segments.push({ text: curText, line: curLine, col: curCol });
725
+ } else if (segments.length === 0) {
726
+ throw new ParseError("empty substitution path", startLine, startCol);
727
+ } else {
728
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
729
+ throw new ParseError("empty segment in path", errLine, errCol);
730
+ }
495
731
  }
496
- return { segments, optional };
732
+ return { segments, optional, listSuffix };
733
+ }
734
+ /**
735
+ * Consume the literal `[]` suffix inside a substitution body.
736
+ * Called after the `[` has been detected (but not yet consumed).
737
+ * Requires the next character to be `[`, then exactly `]`, then `}`.
738
+ * Whitespace inside `[]` is not permitted (per spec Decision §1).
739
+ * Returns true (always — only called when listSuffix path is taken).
740
+ */
741
+ parseListSuffix(startLine) {
742
+ this.advance();
743
+ const afterBracket = this.peek();
744
+ if (afterBracket !== "]") {
745
+ const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
746
+ throw new ParseError(
747
+ `expected ']' after '[' in substitution list suffix (got ${desc})`,
748
+ startLine,
749
+ this.col
750
+ );
751
+ }
752
+ this.advance();
753
+ if (this.peek() !== "}") {
754
+ const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
755
+ throw new ParseError(
756
+ `expected '}' after '[]' in substitution list suffix (got ${desc})`,
757
+ startLine,
758
+ this.col
759
+ );
760
+ }
761
+ this.advance();
762
+ return true;
497
763
  }
498
764
  };
499
765
  function tokenize(input) {
@@ -581,6 +847,14 @@ var Parser = class {
581
847
  const t = this.peek();
582
848
  if (t.kind === "eof" || t.kind === "rbrace") break;
583
849
  if (t.kind === "unquoted" && t.value === "include") {
850
+ const next = this.peek(1);
851
+ if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
852
+ throw new ParseError(
853
+ `'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
854
+ t.line,
855
+ t.col
856
+ );
857
+ }
584
858
  this.advance();
585
859
  fields.push(this.parseInclude());
586
860
  this.skip("newline");
@@ -589,7 +863,14 @@ var Parser = class {
589
863
  continue;
590
864
  }
591
865
  const keyPos = this.currentPos();
592
- const key = this.parseKey();
866
+ const { segments: key, firstWasQuoted } = this.parseKey();
867
+ if (key[0] === "include" && !firstWasQuoted) {
868
+ throw new ParseError(
869
+ `'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
870
+ keyPos.line,
871
+ keyPos.col
872
+ );
873
+ }
593
874
  this.skip("newline");
594
875
  let append = false;
595
876
  const sep = this.peek();
@@ -619,10 +900,12 @@ var Parser = class {
619
900
  parseKey() {
620
901
  const segments = [];
621
902
  let trailingDot = false;
903
+ let firstWasQuoted = false;
622
904
  while (true) {
623
905
  const t = this.peek();
624
906
  if (t.kind === "string") {
625
907
  this.advance();
908
+ if (segments.length === 0) firstWasQuoted = true;
626
909
  segments.push(t.value);
627
910
  trailingDot = false;
628
911
  } else if (t.kind === "unquoted") {
@@ -659,64 +942,143 @@ var Parser = class {
659
942
  }
660
943
  break;
661
944
  }
662
- return segments;
945
+ return { segments, firstWasQuoted };
663
946
  }
664
947
  parseInclude() {
665
948
  const p = this.currentPos();
666
949
  this.skip("newline");
667
950
  const t = this.peek();
668
- let path2;
669
- let required = false;
670
- let isFile = false;
671
951
  if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
672
952
  if (t.value === "required") {
673
- const next2 = this.peek(1);
674
- if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
953
+ const next = this.peek(1);
954
+ if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
675
955
  throw new ParseError("include required must be followed by (", t.line, t.col);
676
956
  }
677
957
  }
678
- required = true;
679
958
  this.advance();
680
959
  const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
681
- if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
960
+ if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
682
961
  throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
683
962
  }
684
- if (innerPrefix.startsWith("file")) {
685
- isFile = true;
963
+ const nextTok = this.peek();
964
+ if (innerPrefix === "package" || innerPrefix.startsWith("package(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "package(" || nextTok.value.startsWith("package("))) {
965
+ const consumePackageToken = innerPrefix === "";
966
+ const { qualifier, path: path3 } = this.parsePackageArgs(consumePackageToken);
967
+ return this.makeIncludeField(qualifier, path3, true, p);
686
968
  }
687
- const next = this.peek();
688
- if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
689
- throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
690
- }
691
- if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
692
- isFile = true;
693
- }
694
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
695
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
696
- path2 = this.advance().value;
697
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
698
- } else if (t.kind === "string") {
699
- path2 = this.advance().value;
700
- } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
701
- isFile = true;
969
+ if (innerPrefix === "file" || innerPrefix.startsWith("file(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "file(" || nextTok.value === "file")) {
970
+ if (innerPrefix === "") {
971
+ this.advance();
972
+ }
973
+ const path3 = this.parseQuotedPathSkipWrapper(t);
974
+ return this.makeIncludeField({ kind: "file" }, path3, true, p);
975
+ }
976
+ if (innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "url" || nextTok.value.startsWith("url(") || nextTok.value === "classpath" || nextTok.value.startsWith("classpath("))) {
977
+ throw new ParseError("include url(...) and classpath(...) are not supported", nextTok.line, nextTok.col);
978
+ }
979
+ if (innerPrefix !== "") {
980
+ if (innerPrefix === ")") {
981
+ throw new ParseError("include required(...) must contain a path", t.line, t.col);
982
+ }
983
+ const name = innerPrefix.replace(/\(.*$/, "").replace(/\).*$/, "");
984
+ throw new ParseError(`unknown include qualifier inside required(): "${name}"`, t.line, t.col);
985
+ }
986
+ const path2 = this.parseQuotedPathSkipWrapper(t);
987
+ return this.makeIncludeField({ kind: "bare" }, path2, true, p);
988
+ }
989
+ if (t.kind === "string") {
990
+ const path2 = this.advance().value;
991
+ return this.makeIncludeField({ kind: "bare" }, path2, false, p);
992
+ }
993
+ if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
702
994
  this.advance();
703
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
704
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
705
- path2 = this.advance().value;
706
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
707
- } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
995
+ const path2 = this.parseQuotedPathSkipWrapper(t);
996
+ return this.makeIncludeField({ kind: "file" }, path2, false, p);
997
+ }
998
+ if (t.kind === "unquoted" && (t.value === "package(" || t.value.startsWith("package("))) {
999
+ const { qualifier, path: path2 } = this.parsePackageArgs();
1000
+ return this.makeIncludeField(qualifier, path2, false, p);
1001
+ }
1002
+ if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
708
1003
  throw new ParseError("include url(...) is not supported", t.line, t.col);
709
- } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
1004
+ }
1005
+ if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
710
1006
  throw new ParseError("include classpath(...) is not supported", t.line, t.col);
711
- } else {
712
- throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
713
1007
  }
714
- return {
715
- key: [],
716
- value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
717
- append: false,
718
- pos: p
719
- };
1008
+ throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
1009
+ }
1010
+ makeIncludeField(qualifier, path2, required, pos) {
1011
+ return { key: [], value: { kind: "include", qualifier, path: path2, required, pos }, append: false, pos };
1012
+ }
1013
+ /**
1014
+ * Skip past include-syntax wrapper tokens to find the quoted path string.
1015
+ * Strict: only allows `(`, `)`, and bare qualifier keywords (file / file( /
1016
+ * package / package( / url / url( / classpath / classpath() between the
1017
+ * directive and the quoted path. Anything else is a parse error rather than
1018
+ * silently swallowed (see ts#113).
1019
+ */
1020
+ parseQuotedPathSkipWrapper(errTok) {
1021
+ while (this.isIncludeWrapperToken(this.peek())) this.advance();
1022
+ if (this.peek().kind !== "string") {
1023
+ const tok = this.peek();
1024
+ if (tok.kind === "eof") throw new ParseError("expected include path", errTok.line, errTok.col);
1025
+ const desc = tok.kind === "unquoted" ? `unquoted "${tok.value}"` : tok.kind;
1026
+ throw new ParseError(`expected quoted include path, got ${desc}`, tok.line, tok.col);
1027
+ }
1028
+ const path2 = this.advance().value;
1029
+ while (this.peek().kind === "unquoted" && (this.peek().value === ")" || this.peek().value === "))")) {
1030
+ this.advance();
1031
+ }
1032
+ const after = this.peek();
1033
+ if (after.kind !== "newline" && after.kind !== "rbrace" && after.kind !== "eof" && after.kind !== "comma") {
1034
+ const desc = after.kind === "unquoted" ? `unquoted "${after.value}"` : after.kind;
1035
+ throw new ParseError(`unexpected token after include path: ${desc}`, after.line, after.col);
1036
+ }
1037
+ return path2;
1038
+ }
1039
+ isIncludeWrapperToken(tok) {
1040
+ return tok.kind === "unquoted" && tok.value === "(";
1041
+ }
1042
+ /**
1043
+ * Parse the body of `package("identifier", "file")`.
1044
+ *
1045
+ * @param consumePackageToken - When true (default), the current token is `package(` and
1046
+ * must be consumed first. When false, the lexer already bundled `package(` into the
1047
+ * preceding token (e.g. `required(package("`), so we are already positioned at the
1048
+ * first string argument.
1049
+ */
1050
+ parsePackageArgs(consumePackageToken = true) {
1051
+ const pkgTok = consumePackageToken ? this.advance() : this.peek();
1052
+ this.skip("newline");
1053
+ const idTok = this.peek();
1054
+ if (idTok.kind !== "string") {
1055
+ throw new ParseError("include package: expected quoted identifier as first argument", pkgTok.line, pkgTok.col);
1056
+ }
1057
+ const identifier = this.advance().value;
1058
+ this.skip("newline");
1059
+ if (this.peek().kind !== "comma") {
1060
+ const after = this.peek();
1061
+ throw new ParseError(
1062
+ 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",
1063
+ pkgTok.line,
1064
+ pkgTok.col
1065
+ );
1066
+ }
1067
+ this.advance();
1068
+ this.skip("newline");
1069
+ const fileTok = this.peek();
1070
+ if (fileTok.kind !== "string") {
1071
+ throw new ParseError(
1072
+ "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
1073
+ pkgTok.line,
1074
+ pkgTok.col
1075
+ );
1076
+ }
1077
+ const filePath = this.advance().value;
1078
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") {
1079
+ this.advance();
1080
+ }
1081
+ return { qualifier: { kind: "package", identifier }, path: filePath };
720
1082
  }
721
1083
  parseValue() {
722
1084
  const p = this.currentPos();
@@ -738,7 +1100,7 @@ var Parser = class {
738
1100
  throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
739
1101
  }
740
1102
  const payload = t.subst;
741
- node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
1103
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
742
1104
  } else if (t.kind === "string" || t.kind === "triple_string") {
743
1105
  this.advance();
744
1106
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -757,7 +1119,9 @@ var Parser = class {
757
1119
  parts.push(node);
758
1120
  }
759
1121
  if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
760
- if (parts.length === 1) return parts[0];
1122
+ if (parts.length === 1) {
1123
+ return parts[0];
1124
+ }
761
1125
  return { kind: "concat", nodes: parts, pos: p };
762
1126
  }
763
1127
  parseArray() {
@@ -789,107 +1153,10 @@ function parseTokens(tokens) {
789
1153
  return new Parser(tokens).parse();
790
1154
  }
791
1155
 
792
- // src/internal/resolver/types.ts
793
- var separatorValues = /* @__PURE__ */ new WeakSet();
794
- function isSubst(v) {
795
- return v._kind === "subst-placeholder";
796
- }
797
- function isConcat(v) {
798
- return v._kind === "concat-placeholder";
799
- }
800
- function isAppend(v) {
801
- return v._kind === "append-placeholder";
802
- }
803
- function isResObj(v) {
804
- return v._kind === "res-obj";
805
- }
806
- function makeResObj() {
807
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
808
- }
809
-
810
- // src/internal/resolver/utils.ts
811
- function segmentsToKey(segments) {
812
- return segments.map((s) => {
813
- const t = s.text;
814
- if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
815
- return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
816
- }
817
- return t;
818
- }).join(".");
819
- }
820
- function lookupPath(root, segments) {
821
- const [head, ...tail] = segments;
822
- if (head === void 0) return void 0;
823
- const key = head.text;
824
- const val = root.fields.get(key);
825
- if (val === void 0) return void 0;
826
- if (tail.length === 0) return val;
827
- if (isResObj(val)) return lookupPath(val, tail);
828
- return void 0;
829
- }
830
- function lookupResObj(root, segments) {
831
- let cur = root;
832
- for (const seg of segments) {
833
- const val = cur.fields.get(seg.text);
834
- if (val === void 0 || !isResObj(val)) return void 0;
835
- cur = val;
836
- }
837
- return cur;
838
- }
839
- function deepMergeHoconValues(base, overlay) {
840
- const merged = new Map(base.fields);
841
- for (const [k, v] of overlay.fields) {
842
- const existing = merged.get(k);
843
- if (existing?.kind === "object" && v.kind === "object") {
844
- merged.set(k, deepMergeHoconValues(existing, v));
845
- } else {
846
- merged.set(k, v);
847
- }
848
- }
849
- return { kind: "object", fields: merged };
850
- }
851
- function deepMergeResObjInto(dst, src) {
852
- for (const [k, srcVal] of src.fields) {
853
- const dstVal = dst.fields.get(k);
854
- if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
855
- deepMergeResObjInto(dstVal, srcVal);
856
- } else {
857
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
858
- dst.fields.set(k, srcVal);
859
- }
860
- }
861
- for (const [k, srcPrior] of src.priorValues) {
862
- if (!dst.priorValues.has(k)) {
863
- dst.priorValues.set(k, srcPrior);
864
- }
865
- }
866
- }
867
- function hoconValueToResObj(hv) {
868
- const obj = makeResObj();
869
- if (hv.kind !== "object") return obj;
870
- for (const [key, val] of hv.fields) {
871
- if (val.kind === "object") {
872
- obj.fields.set(key, hoconValueToResObj(val));
873
- } else {
874
- obj.fields.set(key, val);
875
- }
876
- }
877
- return obj;
878
- }
879
- function isFileNotFoundError(e) {
880
- if (!(e instanceof Error)) return false;
881
- const code = e.code;
882
- if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
883
- const msg = e.message.toLowerCase();
884
- return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
885
- }
886
-
887
- // src/internal/resolver/include-loader.ts
888
- import * as nodePath from "path";
889
-
890
1156
  // src/internal/properties/properties.ts
891
1157
  function parseProperties(input) {
892
1158
  const root = /* @__PURE__ */ Object.create(null);
1159
+ const pairs = [];
893
1160
  for (const line of input.split("\n")) {
894
1161
  const trimmed = line.trim();
895
1162
  if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
@@ -898,6 +1165,10 @@ function parseProperties(input) {
898
1165
  const key = trimmed.slice(0, sepIdx).trim();
899
1166
  const value = trimmed.slice(sepIdx + 1).trim();
900
1167
  if (key === "") continue;
1168
+ pairs.push([key, value]);
1169
+ }
1170
+ pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1171
+ for (const [key, value] of pairs) {
901
1172
  setNested(root, key.split("."), value);
902
1173
  }
903
1174
  return root;
@@ -921,6 +1192,7 @@ function setNested(obj, segments, value) {
921
1192
  }
922
1193
  const last = segments[segments.length - 1];
923
1194
  if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1195
+ if (typeof current[last] === "object" && current[last] !== null) return;
924
1196
  current[last] = value;
925
1197
  }
926
1198
  function propertiesToHoconValue(input) {
@@ -940,6 +1212,84 @@ function recordToHoconValue(obj) {
940
1212
  }
941
1213
 
942
1214
  // src/internal/resolver/include-loader.ts
1215
+ var _require = createRequire(import.meta.url);
1216
+ function isPnpPath(p) {
1217
+ return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1218
+ }
1219
+ function validatePackageFile(file, identifier) {
1220
+ if (file.length === 0) {
1221
+ throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
1222
+ }
1223
+ if (file.includes("\\")) {
1224
+ throw new ParseError(
1225
+ `include package("${identifier}", "${file}"): backslash not allowed in file argument (use forward slash)`,
1226
+ 0,
1227
+ 0
1228
+ );
1229
+ }
1230
+ if (file.startsWith("/")) {
1231
+ throw new ParseError(
1232
+ `include package("${identifier}", "${file}"): absolute path not allowed in file argument`,
1233
+ 0,
1234
+ 0
1235
+ );
1236
+ }
1237
+ if (/(?:^|\/)\.\.?(?:\/|$)/u.test(file)) {
1238
+ throw new ParseError(
1239
+ `include package("${identifier}", "${file}"): path traversal (. or ..) not allowed in file argument`,
1240
+ 0,
1241
+ 0
1242
+ );
1243
+ }
1244
+ if (file.includes("//")) {
1245
+ throw new ParseError(
1246
+ `include package("${identifier}", "${file}"): consecutive slashes not allowed in file argument`,
1247
+ 0,
1248
+ 0
1249
+ );
1250
+ }
1251
+ }
1252
+ function validatePackageIdentifier(identifier) {
1253
+ if (identifier.length === 0) {
1254
+ throw new ParseError("include package: identifier argument must be non-empty", 0, 0);
1255
+ }
1256
+ }
1257
+ function defaultPackageResolver(identifier, file, includingFile, baseDir, resolveFrom) {
1258
+ const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
1259
+ let resolved;
1260
+ try {
1261
+ resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
1262
+ } catch {
1263
+ throw new PackageLookupError(
1264
+ `include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
1265
+ identifier,
1266
+ file,
1267
+ 0,
1268
+ 0
1269
+ );
1270
+ }
1271
+ if (isPnpPath(resolved)) {
1272
+ throw new ResolveError(
1273
+ `include package("${identifier}", "${file}"): Yarn Berry PnP store paths are not supported. Provide a custom packageResolver option to handle PnP resolution.`,
1274
+ `${identifier}/${file}`,
1275
+ 0,
1276
+ 0
1277
+ );
1278
+ }
1279
+ const fileParts = file.split("/");
1280
+ const expectedBasename = fileParts[fileParts.length - 1] ?? file;
1281
+ const resolvedBasename = nodePath.basename(resolved);
1282
+ if (resolvedBasename !== expectedBasename) {
1283
+ throw new PackageLookupError(
1284
+ `include package("${identifier}", "${file}"): case mismatch \u2014 resolved "${resolvedBasename}" but expected "${expectedBasename}" (E11 decision 5: case-sensitive)`,
1285
+ identifier,
1286
+ file,
1287
+ 0,
1288
+ 0
1289
+ );
1290
+ }
1291
+ return resolved;
1292
+ }
943
1293
  var IncludeLoader = class {
944
1294
  constructor(opts) {
945
1295
  this.opts = opts;
@@ -1019,8 +1369,77 @@ var IncludeLoader = class {
1019
1369
  }
1020
1370
  return merged;
1021
1371
  }
1372
+ /**
1373
+ * Resolve and load `include package("identifier", "file")`.
1374
+ * Validates the identifier and file arguments, resolves via the package resolver,
1375
+ * performs cycle detection using a JSON-serialised cycle key, and parses the content.
1376
+ */
1377
+ // `required` is reserved for future optional-package semantics (E11 decision 7);
1378
+ // currently all package lookup failures throw regardless of this flag.
1379
+ loadPackage(identifier, file, _required) {
1380
+ validatePackageIdentifier(identifier);
1381
+ validatePackageFile(file, identifier);
1382
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFileSync: readFileSync2 } = this.opts;
1383
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1384
+ if (includeStack.includes(cycleKey)) {
1385
+ throw new ResolveError(
1386
+ `circular include: package("${identifier}", "${file}")`,
1387
+ `${identifier}/${file}`,
1388
+ 0,
1389
+ 0
1390
+ );
1391
+ }
1392
+ if (includeStack.length >= 50) {
1393
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1394
+ }
1395
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1396
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1397
+ const content = readFileSync2(resolvedPath);
1398
+ if (content.length === 0) return makeResObj();
1399
+ const tokens = tokenize(content);
1400
+ assertNonEmptyDocument(tokens, resolvedPath);
1401
+ const ast = parseTokens(tokens);
1402
+ return this.onBuildResObj(ast, {
1403
+ ...this.opts,
1404
+ baseDir: nodePath.dirname(resolvedPath),
1405
+ includeStack: [...includeStack, cycleKey]
1406
+ });
1407
+ }
1408
+ /**
1409
+ * Async variant of `loadPackage`.
1410
+ */
1411
+ async loadPackageAsync(identifier, file, _required) {
1412
+ validatePackageIdentifier(identifier);
1413
+ validatePackageFile(file, identifier);
1414
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFile, readFileSync: readFileSync2 } = this.opts;
1415
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1416
+ if (includeStack.includes(cycleKey)) {
1417
+ throw new ResolveError(
1418
+ `circular include: package("${identifier}", "${file}")`,
1419
+ `${identifier}/${file}`,
1420
+ 0,
1421
+ 0
1422
+ );
1423
+ }
1424
+ if (includeStack.length >= 50) {
1425
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1426
+ }
1427
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1428
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1429
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1430
+ const content = await read(resolvedPath);
1431
+ if (content.length === 0) return makeResObj();
1432
+ const tokens = tokenize(content);
1433
+ assertNonEmptyDocument(tokens, resolvedPath);
1434
+ const ast = parseTokens(tokens);
1435
+ return this.onBuildResObjAsync(ast, {
1436
+ ...this.opts,
1437
+ baseDir: nodePath.dirname(resolvedPath),
1438
+ includeStack: [...includeStack, cycleKey]
1439
+ });
1440
+ }
1022
1441
  loadSingle(candidate) {
1023
- const { readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1442
+ const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
1024
1443
  if (includeStack.includes(candidate)) {
1025
1444
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1026
1445
  }
@@ -1034,16 +1453,17 @@ var IncludeLoader = class {
1034
1453
  if (candidate.endsWith(".properties")) {
1035
1454
  return hoconValueToResObj(propertiesToHoconValue(content));
1036
1455
  }
1037
- const ast = parseTokens(tokenize(content));
1456
+ const tokens = tokenize(content);
1457
+ assertNonEmptyDocument(tokens, candidate);
1458
+ const ast = parseTokens(tokens);
1038
1459
  return this.onBuildResObj(ast, {
1039
- env,
1460
+ ...this.opts,
1040
1461
  baseDir: nodePath.dirname(candidate),
1041
- readFileSync: readFileSync2,
1042
1462
  includeStack: [...includeStack, candidate]
1043
1463
  });
1044
1464
  }
1045
1465
  async loadSingleAsync(candidate) {
1046
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1466
+ const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
1047
1467
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1048
1468
  if (includeStack.includes(candidate)) {
1049
1469
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
@@ -1058,12 +1478,12 @@ var IncludeLoader = class {
1058
1478
  if (candidate.endsWith(".properties")) {
1059
1479
  return hoconValueToResObj(propertiesToHoconValue(content));
1060
1480
  }
1061
- const ast = parseTokens(tokenize(content));
1481
+ const tokens = tokenize(content);
1482
+ assertNonEmptyDocument(tokens, candidate);
1483
+ const ast = parseTokens(tokens);
1062
1484
  return this.onBuildResObjAsync(ast, {
1063
- env,
1485
+ ...this.opts,
1064
1486
  baseDir: nodePath.dirname(candidate),
1065
- readFileSync: readFileSync2,
1066
- readFile,
1067
1487
  includeStack: [...includeStack, candidate]
1068
1488
  });
1069
1489
  }
@@ -1072,7 +1492,6 @@ var IncludeLoader = class {
1072
1492
  // src/internal/resolver/structure-builder.ts
1073
1493
  var StructureBuilder = class _StructureBuilder {
1074
1494
  constructor(opts) {
1075
- this.opts = opts;
1076
1495
  this.loader = new IncludeLoader(opts);
1077
1496
  this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1078
1497
  this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
@@ -1099,7 +1518,13 @@ var StructureBuilder = class _StructureBuilder {
1099
1518
  }
1100
1519
  applyField(obj, field, pathPrefix) {
1101
1520
  if (field.key.length === 0 && field.value.kind === "include") {
1102
- const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
1521
+ const { qualifier, path: path2, required } = field.value;
1522
+ let included;
1523
+ if (qualifier.kind === "package") {
1524
+ included = this.loader.loadPackage(qualifier.identifier, path2, required);
1525
+ } else {
1526
+ included = this.loader.load(path2, required, qualifier.kind === "file");
1527
+ }
1103
1528
  if (pathPrefix.length > 0) {
1104
1529
  this.relativizeResObj(included, pathPrefix);
1105
1530
  }
@@ -1141,7 +1566,13 @@ var StructureBuilder = class _StructureBuilder {
1141
1566
  }
1142
1567
  async applyFieldAsync(obj, field, pathPrefix) {
1143
1568
  if (field.key.length === 0 && field.value.kind === "include") {
1144
- const included = await this.loader.loadAsync(field.value.path, field.value.required, field.value.isFile);
1569
+ const { qualifier, path: path2, required } = field.value;
1570
+ let included;
1571
+ if (qualifier.kind === "package") {
1572
+ included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
1573
+ } else {
1574
+ included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
1575
+ }
1145
1576
  if (pathPrefix.length > 0) {
1146
1577
  this.relativizeResObj(included, pathPrefix);
1147
1578
  }
@@ -1195,9 +1626,9 @@ var StructureBuilder = class _StructureBuilder {
1195
1626
  return inner;
1196
1627
  }
1197
1628
  case "subst":
1198
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1629
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1199
1630
  case "concat":
1200
- return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1631
+ return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1201
1632
  case "include":
1202
1633
  return { kind: "scalar", raw: "null", valueType: "null" };
1203
1634
  }
@@ -1219,13 +1650,13 @@ var StructureBuilder = class _StructureBuilder {
1219
1650
  case "object":
1220
1651
  return await this.buildAsync(ast, pathPrefix);
1221
1652
  case "subst":
1222
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1653
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1223
1654
  case "concat": {
1224
1655
  const nodes = [];
1225
1656
  for (const n of ast.nodes) {
1226
1657
  nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1227
1658
  }
1228
- return { _kind: "concat-placeholder", nodes };
1659
+ return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
1229
1660
  }
1230
1661
  case "include":
1231
1662
  return { kind: "scalar", raw: "null", valueType: "null" };
@@ -1295,6 +1726,23 @@ var SubstitutionResolver = class {
1295
1726
  this.opts = opts;
1296
1727
  this.resolving = /* @__PURE__ */ new Set();
1297
1728
  this.cache = /* @__PURE__ */ new Map();
1729
+ // Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
1730
+ // Used by isSelfRef detection: fires only when found === a concat currently
1731
+ // mid-iteration (i.e. we are inside found's own resolution), preventing the
1732
+ // "foofoo" double-count without misfiring when an external field looks up the
1733
+ // same concat via a substitution (multi-reviewer convergence fix).
1734
+ //
1735
+ // Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
1736
+ // preservation for self-ref detection. Round-2 multi-agent-review surfaced a
1737
+ // false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
1738
+ // criterion was tightened to node-membership-in-iterating-set — strictly
1739
+ // narrower than path-equality. Spec amendment deferred to a follow-up
1740
+ // xx.hocon PR (see Phase 6 #3f close-out notes).
1741
+ this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1742
+ }
1743
+ /** Returns a prefix for error messages, including origin info when set. */
1744
+ originPrefix() {
1745
+ return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
1298
1746
  }
1299
1747
  resolve() {
1300
1748
  return this.resolveResObj(this.root);
@@ -1333,7 +1781,15 @@ var SubstitutionResolver = class {
1333
1781
  }
1334
1782
  resolveVal(v, scope) {
1335
1783
  if (isSubst(v)) return this.resolveSubst(v, scope);
1336
- if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1784
+ if (isConcat(v)) {
1785
+ this.resolvingConcats.add(v);
1786
+ try {
1787
+ const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
1788
+ return concatResult;
1789
+ } finally {
1790
+ this.resolvingConcats.delete(v);
1791
+ }
1792
+ }
1337
1793
  if (isAppend(v)) return this.resolveAppend(v, scope);
1338
1794
  if (isResObj(v)) return this.resolveResObj(v);
1339
1795
  const hv = v;
@@ -1347,15 +1803,12 @@ var SubstitutionResolver = class {
1347
1803
  }
1348
1804
  return hv;
1349
1805
  }
1350
- segmentsEqual(a, b) {
1351
- return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1352
- }
1353
1806
  resolveSubst(s, scope) {
1354
- const key = segmentsToKey(s.segments);
1807
+ const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1355
1808
  if (this.cache.has(key)) return this.cache.get(key);
1356
1809
  if (this.resolving.has(key)) {
1357
1810
  let prior;
1358
- if (s.prefixLen > 0) {
1811
+ if (s.segments.length > 1) {
1359
1812
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1360
1813
  const parentScope = lookupResObj(
1361
1814
  this.root,
@@ -1377,7 +1830,7 @@ var SubstitutionResolver = class {
1377
1830
  }
1378
1831
  if (s.optional) return void 0;
1379
1832
  throw new ResolveError(
1380
- `circular substitution: ${key}`,
1833
+ this.originPrefix() + `circular substitution: ${key}`,
1381
1834
  key,
1382
1835
  s.line,
1383
1836
  s.col
@@ -1387,29 +1840,31 @@ var SubstitutionResolver = class {
1387
1840
  try {
1388
1841
  const found = lookupPath(this.root, s.segments);
1389
1842
  if (found !== void 0) {
1390
- if (isSubst(found) || isConcat(found)) {
1391
- const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1392
- (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1393
- );
1394
- if (isSelfRef) {
1395
- let prior;
1396
- if (s.prefixLen > 0) {
1397
- const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1398
- const parentScope = lookupResObj(
1399
- this.root,
1400
- s.segments.slice(0, s.segments.length - 1)
1401
- );
1402
- prior = parentScope?.priorValues.get(leafSeg);
1403
- } else {
1404
- const rootSeg = s.segments[0]?.text ?? "";
1405
- prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1406
- }
1407
- if (prior !== void 0) {
1408
- const result2 = this.resolveVal(prior, scope);
1409
- if (result2 !== void 0) this.cache.set(key, result2);
1410
- return result2;
1411
- }
1843
+ if (isConcat(found) && this.resolvingConcats.has(found)) {
1844
+ let prior;
1845
+ if (s.segments.length > 1) {
1846
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1847
+ const parentScope = lookupResObj(
1848
+ this.root,
1849
+ s.segments.slice(0, s.segments.length - 1)
1850
+ );
1851
+ prior = parentScope?.priorValues.get(leafSeg);
1852
+ } else {
1853
+ const rootSeg = s.segments[0]?.text ?? "";
1854
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1855
+ }
1856
+ if (prior !== void 0) {
1857
+ const result2 = this.resolveVal(prior, scope);
1858
+ if (result2 !== void 0) this.cache.set(key, result2);
1859
+ return result2;
1412
1860
  }
1861
+ if (s.optional) return void 0;
1862
+ throw new ResolveError(
1863
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1864
+ key,
1865
+ s.line,
1866
+ s.col
1867
+ );
1413
1868
  }
1414
1869
  let result = this.resolveVal(found, scope);
1415
1870
  const effectiveLen = s.segments.length - s.prefixLen;
@@ -1438,16 +1893,45 @@ var SubstitutionResolver = class {
1438
1893
  if (result !== void 0) this.cache.set(key, result);
1439
1894
  return result;
1440
1895
  }
1441
- const envKey = s.segments.map((seg) => seg.text).join(".");
1442
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1443
- if (envVal !== void 0) {
1444
- const result = { kind: "scalar", raw: envVal, valueType: "string" };
1445
- this.cache.set(key, result);
1446
- return result;
1896
+ if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1897
+ const result = this.resolveEnvList(s);
1898
+ if (result !== void 0) {
1899
+ this.cache.set(key, result);
1900
+ return result;
1901
+ }
1902
+ if (s.optional) return void 0;
1903
+ const envBase = s.segments.map((seg) => seg.text).join(".");
1904
+ throw new ResolveError(
1905
+ `${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1906
+ key,
1907
+ s.line,
1908
+ s.col
1909
+ );
1910
+ } else if (s.listSuffix) {
1911
+ if (s.optional) return void 0;
1912
+ if (this.opts.allowUnresolved) return s;
1913
+ throw new ResolveError(
1914
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1915
+ key,
1916
+ s.line,
1917
+ s.col
1918
+ );
1919
+ }
1920
+ if (this.opts.useSystemEnvironment !== false) {
1921
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1922
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1923
+ if (envVal !== void 0) {
1924
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1925
+ this.cache.set(key, result);
1926
+ return result;
1927
+ }
1447
1928
  }
1448
1929
  if (s.optional) return void 0;
1930
+ if (this.opts.allowUnresolved) {
1931
+ return s;
1932
+ }
1449
1933
  throw new ResolveError(
1450
- `could not resolve substitution: \${${key}}`,
1934
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1451
1935
  key,
1452
1936
  s.line,
1453
1937
  s.col
@@ -1456,9 +1940,52 @@ var SubstitutionResolver = class {
1456
1940
  this.resolving.delete(key);
1457
1941
  }
1458
1942
  }
1459
- resolveConcat(nodes, scope) {
1460
- const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1461
- if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1943
+ /**
1944
+ * S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
1945
+ *
1946
+ * Candidate bases follow the same fully-qualified→bare order used by the scalar
1947
+ * env fallback. The first base whose _0 key is present wins entirely — no
1948
+ * cross-base merging (spec §Relativized fallback precedence rules).
1949
+ *
1950
+ * Returns the HoconValue Array if any candidate base has _0, or undefined if
1951
+ * none do (caller applies optional/required handling).
1952
+ */
1953
+ resolveEnvList(s) {
1954
+ const bases = this.candidateBases(s);
1955
+ for (const base of bases) {
1956
+ const key0 = base + "_0";
1957
+ if (!(key0 in this.opts.env)) continue;
1958
+ const items = [];
1959
+ for (let i = 0; ; i++) {
1960
+ const k = base + "_" + i;
1961
+ if (!(k in this.opts.env)) break;
1962
+ items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
1963
+ }
1964
+ return { kind: "array", items };
1965
+ }
1966
+ return void 0;
1967
+ }
1968
+ /**
1969
+ * Return the candidate base names for env-var-list lookup, in precedence order.
1970
+ * Mirrors the fully-qualified→bare fallback used by the scalar env path.
1971
+ */
1972
+ candidateBases(s) {
1973
+ const full = s.segments.map((seg) => seg.text).join(".");
1974
+ if (s.prefixLen === 0) return [full];
1975
+ const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
1976
+ return [full, bare];
1977
+ }
1978
+ resolveConcat(nodes, scope, line = 0, col = 0) {
1979
+ const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
1980
+ if (this.opts.allowUnresolved) {
1981
+ for (const v of rawResolved) {
1982
+ if (v !== void 0 && isSubst(v)) {
1983
+ return v;
1984
+ }
1985
+ }
1986
+ }
1987
+ const resolved = rawResolved.filter((v) => v !== void 0);
1988
+ if (resolved.length === 0) return void 0;
1462
1989
  if (resolved.length === 1) return resolved[0];
1463
1990
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1464
1991
  if (nonSep.length === 0) {
@@ -1474,27 +2001,45 @@ var SubstitutionResolver = class {
1474
2001
  if (converted !== null) {
1475
2002
  return { kind: "array", items: [...left.items, ...converted] };
1476
2003
  }
1477
- return { kind: "array", items: [...left.items, right] };
2004
+ throw new ResolveError(
2005
+ "cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
2006
+ "",
2007
+ line,
2008
+ col
2009
+ );
1478
2010
  }
1479
2011
  if (left.kind === "object" && right.kind === "array") {
1480
2012
  const converted = numericObjectToArray(left);
1481
2013
  if (converted !== null) {
1482
2014
  return { kind: "array", items: [...converted, ...right.items] };
1483
2015
  }
1484
- return { kind: "array", items: [left, ...right.items] };
2016
+ throw new ResolveError(
2017
+ "cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
2018
+ "",
2019
+ line,
2020
+ col
2021
+ );
1485
2022
  }
1486
2023
  if (left.kind === "array" && right.kind === "array") {
1487
2024
  return { kind: "array", items: [...left.items, ...right.items] };
1488
2025
  }
1489
- if (left.kind === "array") {
1490
- return { kind: "array", items: [...left.items, right] };
2026
+ if (left.kind === "array" || right.kind === "array") {
2027
+ throw new ResolveError(
2028
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
2029
+ "",
2030
+ line,
2031
+ col
2032
+ );
1491
2033
  }
1492
- if (right.kind === "array") {
1493
- return { kind: "array", items: [left, ...right.items] };
2034
+ if (left.kind === "object" || right.kind === "object") {
2035
+ throw new ResolveError(
2036
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
2037
+ "",
2038
+ line,
2039
+ col
2040
+ );
1494
2041
  }
1495
- const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1496
- const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1497
- return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
2042
+ return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
1498
2043
  };
1499
2044
  const [head, ...tail] = nonSep;
1500
2045
  const folded = tail.reduce(joinPair, head);
@@ -1514,15 +2059,89 @@ var SubstitutionResolver = class {
1514
2059
  };
1515
2060
 
1516
2061
  // src/internal/resolver/resolver.ts
2062
+ function buildTree(ast, opts) {
2063
+ return new StructureBuilder(opts).build(ast);
2064
+ }
2065
+ function resolveTree(tree, opts) {
2066
+ return new SubstitutionResolver(tree, opts).resolve();
2067
+ }
2068
+ function containsPlaceholders(tree) {
2069
+ for (const val of tree.fields.values()) {
2070
+ if (valContainsPlaceholders(val)) return true;
2071
+ }
2072
+ return false;
2073
+ }
2074
+ function valContainsPlaceholders(v) {
2075
+ const rv = v;
2076
+ if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2077
+ if (isResObj(rv)) return containsPlaceholders(rv);
2078
+ const hv = v;
2079
+ if (hv?.kind === "array") {
2080
+ return hv.items.some((item) => valContainsPlaceholders(item));
2081
+ }
2082
+ return false;
2083
+ }
2084
+ function buildPartialHoconFromResObj(tree) {
2085
+ const fields = /* @__PURE__ */ new Map();
2086
+ for (const [k, v] of tree.fields) {
2087
+ if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2088
+ if (isResObj(v)) {
2089
+ fields.set(k, buildPartialHoconFromResObj(v));
2090
+ } else {
2091
+ if (!valContainsPlaceholders(v)) {
2092
+ fields.set(k, v);
2093
+ }
2094
+ }
2095
+ }
2096
+ }
2097
+ return { kind: "object", fields };
2098
+ }
2099
+ function hoconValueToResObj2(value) {
2100
+ const obj = makeResObj();
2101
+ for (const [k, v] of value.fields) {
2102
+ if (v.kind === "object") {
2103
+ obj.fields.set(k, hoconValueToResObj2(v));
2104
+ } else {
2105
+ obj.fields.set(k, v);
2106
+ }
2107
+ }
2108
+ return obj;
2109
+ }
1517
2110
  function resolve2(ast, opts) {
1518
- const root = new StructureBuilder(opts).build(ast);
1519
- return new SubstitutionResolver(root, opts).resolve();
2111
+ const tree = buildTree(ast, opts);
2112
+ return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
1520
2113
  }
1521
2114
 
1522
2115
  // src/config.ts
1523
2116
  var Config = class _Config {
1524
- constructor(root) {
2117
+ constructor(root, opts) {
1525
2118
  this.root = root;
2119
+ this._resolved = opts?.resolved ?? true;
2120
+ this._parseBaseDir = opts?.parseBaseDir;
2121
+ this._originDescription = opts?.originDescription;
2122
+ this._resObjRoot = opts?.resObjRoot;
2123
+ this._resolveOpts = opts?.resolveOpts;
2124
+ }
2125
+ /** Returns true when the Config's value tree contains no unresolved
2126
+ * substitution placeholders. Whole-config granularity (E12 decision 11). */
2127
+ isResolved() {
2128
+ return this._resolved;
2129
+ }
2130
+ /** @internal Test-only: construct a resolved Config from a HoconValue. */
2131
+ static _fromResolvedValue(root, opts) {
2132
+ return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
2133
+ }
2134
+ /** @internal Construct an unresolved Config carrying a ResObj tree.
2135
+ * Used by parseStringWithOptions (deferred path) in T6. */
2136
+ static _fromUnresolvedResObj(tree, opts) {
2137
+ const partialRoot = buildPartialHoconFromResObj(tree);
2138
+ return new _Config(partialRoot, {
2139
+ resolved: opts.resolved,
2140
+ parseBaseDir: opts.parseBaseDir,
2141
+ originDescription: opts.originDescription,
2142
+ resObjRoot: tree,
2143
+ resolveOpts: opts.resolveOpts
2144
+ });
1526
2145
  }
1527
2146
  get(path2) {
1528
2147
  const v = this.lookupNode(path2);
@@ -1561,17 +2180,34 @@ var Config = class _Config {
1561
2180
  }
1562
2181
  const result = parseBytes(v.raw, unit);
1563
2182
  if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
2183
+ if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
1564
2184
  return result;
1565
2185
  }
1566
2186
  getConfig(path2) {
1567
2187
  const v = this.lookupNode(path2);
1568
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2188
+ if (v === void 0) {
2189
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2190
+ throw new NotResolvedError(path2);
2191
+ }
2192
+ throw new ConfigError(`path not found: ${path2}`, path2);
2193
+ }
1569
2194
  if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
1570
- return new _Config(v);
2195
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2196
+ throw new NotResolvedError(path2);
2197
+ }
2198
+ return new _Config(v, { resolved: this._resolved });
1571
2199
  }
1572
2200
  getList(path2) {
1573
2201
  const v = this.lookupNode(path2);
1574
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2202
+ if (v === void 0) {
2203
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2204
+ throw new NotResolvedError(path2);
2205
+ }
2206
+ throw new ConfigError(`path not found: ${path2}`, path2);
2207
+ }
2208
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2209
+ throw new NotResolvedError(path2);
2210
+ }
1575
2211
  if (v.kind === "object") {
1576
2212
  const converted = numericObjectToArray(v);
1577
2213
  if (converted !== null) return converted.map(hoconToJs);
@@ -1586,12 +2222,138 @@ var Config = class _Config {
1586
2222
  return [...this.root.fields.keys()];
1587
2223
  }
1588
2224
  withFallback(fallback) {
1589
- const merged = deepMergeHocon(this.root, fallback.root);
1590
- return new _Config(merged);
2225
+ if (fallback == null) return this;
2226
+ const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2227
+ const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
2228
+ const merged = mergeUnresolved(selfTree, fbTree);
2229
+ const hasPlaceholders = containsPlaceholders(merged);
2230
+ const partialRoot = buildPartialHoconFromResObj(merged);
2231
+ return new _Config(partialRoot, {
2232
+ resolved: !hasPlaceholders,
2233
+ parseBaseDir: this._parseBaseDir,
2234
+ originDescription: this._originDescription,
2235
+ resObjRoot: merged,
2236
+ resolveOpts: this._resolveOpts ?? fallback._resolveOpts
2237
+ });
2238
+ }
2239
+ /**
2240
+ * Runs substitution resolution (phase 2) on the stored unresolved tree.
2241
+ * Idempotent: calling resolve() on an already-resolved Config returns an
2242
+ * equivalent resolved Config.
2243
+ *
2244
+ * opts.allowUnresolved (default false) — when true, leaves unresolved
2245
+ * non-optional placeholders in place rather than throwing.
2246
+ * opts.useSystemEnvironment (default true) — when false, env var lookups
2247
+ * are suppressed (hermetic resolution).
2248
+ *
2249
+ * E12 decision 3.
2250
+ */
2251
+ resolve(opts = {}) {
2252
+ if (this._resolved) {
2253
+ return new _Config(this.root, {
2254
+ resolved: true,
2255
+ parseBaseDir: this._parseBaseDir,
2256
+ originDescription: this._originDescription
2257
+ });
2258
+ }
2259
+ const tree = this._resObjRoot;
2260
+ if (!tree) {
2261
+ return new _Config(this.root, { resolved: true });
2262
+ }
2263
+ const resolveOpts = {
2264
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2265
+ throw new Error("no files");
2266
+ } },
2267
+ allowUnresolved: opts.allowUnresolved ?? false,
2268
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2269
+ originDescription: this._originDescription
2270
+ };
2271
+ const resolved = resolveTree(tree, resolveOpts);
2272
+ if (resolved.kind !== "object") throw new Error("resolve: expected object root");
2273
+ if (opts.allowUnresolved) {
2274
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
2275
+ return new _Config(stripped, {
2276
+ resolved: !hadPlaceholders,
2277
+ parseBaseDir: this._parseBaseDir,
2278
+ originDescription: this._originDescription,
2279
+ resObjRoot: hadPlaceholders ? tree : void 0,
2280
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2281
+ });
2282
+ }
2283
+ return new _Config(resolved, {
2284
+ resolved: true,
2285
+ parseBaseDir: this._parseBaseDir,
2286
+ originDescription: this._originDescription
2287
+ });
2288
+ }
2289
+ /**
2290
+ * Resolves receiver substitutions using source as lookup context.
2291
+ * Source's keys are NOT merged into the result — differs from
2292
+ * `this.withFallback(source).resolve()` which includes source keys.
2293
+ *
2294
+ * Precondition (E12 decision 10): source must be resolved. If
2295
+ * source.isResolved() is false, throws NotResolvedError.
2296
+ *
2297
+ * On an already-resolved receiver, resolveWith is a no-op (returns an
2298
+ * equivalent resolved Config, source keys not included).
2299
+ *
2300
+ * E12 decisions 9, 10.
2301
+ */
2302
+ resolveWith(source, opts = {}) {
2303
+ if (!source.isResolved()) {
2304
+ throw new NotResolvedError("source");
2305
+ }
2306
+ if (this._resolved) {
2307
+ return new _Config(this.root, {
2308
+ resolved: true,
2309
+ parseBaseDir: this._parseBaseDir,
2310
+ originDescription: this._originDescription
2311
+ });
2312
+ }
2313
+ const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2314
+ const srcTree = hoconValueToResObj2(source.root);
2315
+ const merged = mergeUnresolved(receiverTree, srcTree);
2316
+ const resolveOpts = {
2317
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2318
+ throw new Error("no files");
2319
+ } },
2320
+ allowUnresolved: opts.allowUnresolved ?? false,
2321
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2322
+ originDescription: this._originDescription
2323
+ };
2324
+ const resolved = resolveTree(merged, resolveOpts);
2325
+ if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
2326
+ const receiverShapeForFilter = resObjToKeyShape(receiverTree);
2327
+ const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
2328
+ if (opts.allowUnresolved) {
2329
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
2330
+ return new _Config(stripped, {
2331
+ resolved: !hadPlaceholders,
2332
+ parseBaseDir: this._parseBaseDir,
2333
+ originDescription: this._originDescription,
2334
+ resObjRoot: hadPlaceholders ? receiverTree : void 0,
2335
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2336
+ });
2337
+ }
2338
+ return new _Config(filtered, {
2339
+ resolved: true,
2340
+ parseBaseDir: this._parseBaseDir,
2341
+ originDescription: this._originDescription
2342
+ });
1591
2343
  }
1592
2344
  toObject() {
1593
2345
  return hoconToJs(this.root);
1594
2346
  }
2347
+ /**
2348
+ * @internal Test-only: renders this resolved Config's value tree as canonical
2349
+ * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
2350
+ * against Lightbend ground truth. NOT part of the public API.
2351
+ *
2352
+ * Throws if the Config is unresolved (contains placeholders).
2353
+ */
2354
+ _renderJSONForTest() {
2355
+ return renderHoconAsJSON(this.root);
2356
+ }
1595
2357
  lookupNode(path2) {
1596
2358
  const segments = splitConfigPath(path2);
1597
2359
  let current = this.root;
@@ -1603,9 +2365,40 @@ var Config = class _Config {
1603
2365
  }
1604
2366
  return current;
1605
2367
  }
2368
+ /**
2369
+ * _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
2370
+ * return true if the subtree there contains any unresolved placeholder.
2371
+ * Used by getConfig / getList (T2) to detect transitive unresolved placeholders
2372
+ * in object / array subtrees that would otherwise appear to be "resolved" in the
2373
+ * partial HoconValue root (because individual leaf fields are omitted rather than
2374
+ * the whole subtree when their value is a direct placeholder scalar, but object
2375
+ * sub-containers are retained in the partial root with the placeholder-keyed
2376
+ * fields omitted from them — so the object appears "present but empty-ish").
2377
+ */
2378
+ _resObjRootSubtreeHasPlaceholders(path2) {
2379
+ const tree = this._resObjRoot;
2380
+ if (!tree) return false;
2381
+ const segments = splitConfigPath(path2);
2382
+ let cur = tree;
2383
+ for (const seg of segments) {
2384
+ const val = cur.fields.get(seg);
2385
+ if (val === void 0) return false;
2386
+ if (isResObj(val)) {
2387
+ cur = val;
2388
+ } else {
2389
+ return valContainsPlaceholders(val);
2390
+ }
2391
+ }
2392
+ return containsPlaceholders(cur);
2393
+ }
1606
2394
  requireScalar(path2) {
1607
2395
  const v = this.lookupNode(path2);
1608
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2396
+ if (v === void 0) {
2397
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2398
+ throw new NotResolvedError(path2);
2399
+ }
2400
+ throw new ConfigError(`path not found: ${path2}`, path2);
2401
+ }
1609
2402
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
1610
2403
  return v;
1611
2404
  }
@@ -1668,28 +2461,89 @@ function hoconToJs(v) {
1668
2461
  case "array":
1669
2462
  return v.items.map(hoconToJs);
1670
2463
  case "object": {
1671
- const obj = {};
2464
+ const obj = /* @__PURE__ */ Object.create(null);
1672
2465
  for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
1673
- return obj;
2466
+ return Object.assign({}, obj);
1674
2467
  }
1675
2468
  }
1676
2469
  }
1677
- function deepMergeHocon(receiver, fallback) {
1678
- const merged = new Map(fallback.fields);
1679
- for (const [k, v] of receiver.fields) {
1680
- const fb = merged.get(k);
1681
- if (fb?.kind === "object" && v.kind === "object") {
1682
- merged.set(k, deepMergeHocon(v, fb));
2470
+ function resObjToKeyShape(tree) {
2471
+ const fields = /* @__PURE__ */ new Map();
2472
+ for (const [k, v] of tree.fields) {
2473
+ if (isSubst(v) || isConcat(v) || isAppend(v)) {
2474
+ fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2475
+ } else if (isResObj(v)) {
2476
+ fields.set(k, resObjToKeyShape(v));
1683
2477
  } else {
1684
- merged.set(k, v);
2478
+ fields.set(k, v);
1685
2479
  }
1686
2480
  }
1687
- return { kind: "object", fields: merged };
2481
+ return { kind: "object", fields };
2482
+ }
2483
+ function stripPlaceholderFields(v) {
2484
+ const fields = /* @__PURE__ */ new Map();
2485
+ let hadPlaceholders = false;
2486
+ for (const [k, val] of v.fields) {
2487
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2488
+ hadPlaceholders = true;
2489
+ } else if (val.kind === "object") {
2490
+ const inner = stripPlaceholderFields(val);
2491
+ if (inner.hadPlaceholders) hadPlaceholders = true;
2492
+ fields.set(k, inner.stripped);
2493
+ } else {
2494
+ fields.set(k, val);
2495
+ }
2496
+ }
2497
+ return { stripped: { kind: "object", fields }, hadPlaceholders };
2498
+ }
2499
+ function filterByReceiverShape(resolved, receiverShape) {
2500
+ const fields = /* @__PURE__ */ new Map();
2501
+ for (const [k, rv] of resolved.fields) {
2502
+ if (!receiverShape.fields.has(k)) continue;
2503
+ const receiverVal = receiverShape.fields.get(k);
2504
+ if (rv.kind === "object" && receiverVal.kind === "object") {
2505
+ fields.set(k, filterByReceiverShape(rv, receiverVal));
2506
+ } else {
2507
+ fields.set(k, rv);
2508
+ }
2509
+ }
2510
+ return { kind: "object", fields };
2511
+ }
2512
+ function renderHoconAsJSON(v) {
2513
+ switch (v.kind) {
2514
+ case "scalar": {
2515
+ switch (v.valueType) {
2516
+ case "null":
2517
+ return "null";
2518
+ case "boolean":
2519
+ return v.raw;
2520
+ case "number": {
2521
+ const n = Number(v.raw);
2522
+ if (Number.isFinite(n)) return v.raw;
2523
+ return JSON.stringify(v.raw);
2524
+ }
2525
+ case "string":
2526
+ return JSON.stringify(v.raw);
2527
+ }
2528
+ break;
2529
+ }
2530
+ case "array": {
2531
+ const items = v.items.map(renderHoconAsJSON);
2532
+ return `[${items.join(",")}]`;
2533
+ }
2534
+ case "object": {
2535
+ const keys = [...v.fields.keys()].sort();
2536
+ const pairs = keys.map((k) => {
2537
+ const val = v.fields.get(k);
2538
+ return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
2539
+ });
2540
+ return `{${pairs.join(",")}}`;
2541
+ }
2542
+ }
2543
+ throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
1688
2544
  }
1689
2545
 
1690
2546
  // src/parse.ts
1691
- import * as fs from "fs";
1692
- import * as path from "path";
1693
2547
  function getEnv(opts) {
1694
2548
  if (opts.env !== void 0) return opts.env;
1695
2549
  if (typeof process !== "undefined" && process.env) return process.env;
@@ -1700,12 +2554,16 @@ function defaultReadFileSync(filePath) {
1700
2554
  }
1701
2555
  function buildResolveContext(input, opts) {
1702
2556
  const tokens = tokenize(input);
2557
+ assertNonEmptyDocument(tokens, "input");
1703
2558
  const ast = parseTokens(tokens);
1704
2559
  const resolveOpts = {
1705
2560
  env: getEnv(opts),
1706
2561
  baseDir: opts.baseDir,
1707
2562
  readFileSync: opts.readFileSync ?? defaultReadFileSync,
1708
- readFile: opts.readFile
2563
+ readFile: opts.readFile,
2564
+ originDescription: opts.originDescription,
2565
+ resolveFrom: opts.resolveFrom,
2566
+ packageResolver: opts.packageResolver
1709
2567
  };
1710
2568
  return { ast, resolveOpts };
1711
2569
  }
@@ -1713,7 +2571,7 @@ function parse(input, opts = {}) {
1713
2571
  const { ast, resolveOpts } = buildResolveContext(input, opts);
1714
2572
  const value = resolve2(ast, resolveOpts);
1715
2573
  if (value.kind !== "object") throw new Error("resolved value is not an object");
1716
- return new Config(value);
2574
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
1717
2575
  }
1718
2576
  function parseFile(filePath, opts = {}) {
1719
2577
  if (typeof process === "undefined" && !opts.readFileSync) {