@o3co/ts.hocon 1.0.0 → 1.2.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/index.cjs CHANGED
@@ -42,6 +42,7 @@ __export(src_exports, {
42
42
  module.exports = __toCommonJS(src_exports);
43
43
 
44
44
  // src/coerce.ts
45
+ var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
45
46
  var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
46
47
  var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
47
48
  function coerceBoolean(value) {
@@ -51,7 +52,7 @@ function coerceBoolean(value) {
51
52
  return void 0;
52
53
  }
53
54
  function coerceNumber(value) {
54
- if (!/^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) return void 0;
55
+ if (!DECIMAL_NUMBER_RE.test(value)) return void 0;
55
56
  return Number(value);
56
57
  }
57
58
  var DURATION_UNITS = {
@@ -207,6 +208,23 @@ var ConfigError = class extends Error {
207
208
  }
208
209
  };
209
210
 
211
+ // src/value/numeric-array.ts
212
+ function numericObjectToArray(value) {
213
+ if (value.kind !== "object") return null;
214
+ if (value.fields.size === 0) return null;
215
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
216
+ const eligible = [];
217
+ for (const [key, val] of value.fields) {
218
+ if (!canonicalInt.test(key)) continue;
219
+ const n = Number(key);
220
+ if (!Number.isInteger(n) || n > 2147483647) continue;
221
+ eligible.push({ n, v: val });
222
+ }
223
+ if (eligible.length === 0) return null;
224
+ eligible.sort((a, b) => a.n - b.n);
225
+ return eligible.map((e) => e.v);
226
+ }
227
+
210
228
  // src/config.ts
211
229
  var Config = class _Config {
212
230
  constructor(root) {
@@ -219,39 +237,36 @@ var Config = class _Config {
219
237
  }
220
238
  getString(path2) {
221
239
  const v = this.requireScalar(path2);
222
- if (typeof v !== "string") throw new ConfigError(`expected string at ${path2}, got ${typeof v}`, path2);
223
- return v;
240
+ return v.raw;
224
241
  }
225
242
  getNumber(path2) {
226
243
  const v = this.requireScalar(path2);
227
- if (typeof v === "number") return v;
228
- if (typeof v === "string") {
229
- const coerced = coerceNumber(v);
230
- if (coerced !== void 0) return coerced;
231
- }
232
- throw new ConfigError(`expected number at ${path2}, got ${typeof v}`, path2);
244
+ const coerced = coerceNumber(v.raw);
245
+ if (coerced !== void 0) return coerced;
246
+ throw new ConfigError(`expected number at ${path2}, got ${v.valueType}`, path2);
233
247
  }
234
248
  getBoolean(path2) {
235
249
  const v = this.requireScalar(path2);
236
- if (typeof v === "boolean") return v;
237
- if (typeof v === "string") {
238
- const coerced = coerceBoolean(v);
239
- if (coerced !== void 0) return coerced;
240
- }
241
- throw new ConfigError(`expected boolean at ${path2}, got ${typeof v}`, path2);
250
+ const coerced = coerceBoolean(v.raw);
251
+ if (coerced !== void 0) return coerced;
252
+ throw new ConfigError(`expected boolean at ${path2}, got ${v.valueType}`, path2);
242
253
  }
243
254
  getDuration(path2, unit) {
244
255
  const v = this.requireScalar(path2);
245
- if (typeof v !== "string") throw new ConfigError(`expected duration string at ${path2}, got ${typeof v}`, path2);
246
- const result = parseDuration(v, unit);
247
- if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v)}`, path2);
256
+ if (v.valueType !== "string" && v.valueType !== "number") {
257
+ throw new ConfigError(`expected duration at ${path2}, got ${v.valueType}`, path2);
258
+ }
259
+ const result = parseDuration(v.raw, unit);
260
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v.raw)}`, path2);
248
261
  return result;
249
262
  }
250
263
  getBytes(path2, unit) {
251
264
  const v = this.requireScalar(path2);
252
- if (typeof v !== "string") throw new ConfigError(`expected byte size string at ${path2}, got ${typeof v}`, path2);
253
- const result = parseBytes(v, unit);
254
- if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v)}`, path2);
265
+ if (v.valueType !== "string" && v.valueType !== "number") {
266
+ throw new ConfigError(`expected byte size at ${path2}, got ${v.valueType}`, path2);
267
+ }
268
+ const result = parseBytes(v.raw, unit);
269
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
255
270
  return result;
256
271
  }
257
272
  getConfig(path2) {
@@ -263,6 +278,10 @@ var Config = class _Config {
263
278
  getList(path2) {
264
279
  const v = this.lookupNode(path2);
265
280
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
281
+ if (v.kind === "object") {
282
+ const converted = numericObjectToArray(v);
283
+ if (converted !== null) return converted.map(hoconToJs);
284
+ }
266
285
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
267
286
  return v.items.map(hoconToJs);
268
287
  }
@@ -294,7 +313,7 @@ var Config = class _Config {
294
313
  const v = this.lookupNode(path2);
295
314
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
296
315
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
297
- return v.value;
316
+ return v;
298
317
  }
299
318
  };
300
319
  function splitConfigPath(path2) {
@@ -336,10 +355,22 @@ function splitConfigPath(path2) {
336
355
  }
337
356
  return segments;
338
357
  }
358
+ function scalarToJs(raw, valueType) {
359
+ switch (valueType) {
360
+ case "null":
361
+ return null;
362
+ case "boolean":
363
+ return raw === "true";
364
+ case "number":
365
+ return Number(raw);
366
+ case "string":
367
+ return raw;
368
+ }
369
+ }
339
370
  function hoconToJs(v) {
340
371
  switch (v.kind) {
341
372
  case "scalar":
342
- return v.value;
373
+ return scalarToJs(v.raw, v.valueType);
343
374
  case "array":
344
375
  return v.items.map(hoconToJs);
345
376
  case "object": {
@@ -363,281 +394,494 @@ function deepMergeHocon(receiver, fallback) {
363
394
  }
364
395
 
365
396
  // src/internal/lexer/lexer.ts
366
- function tokenize(input) {
367
- if (input.charCodeAt(0) === 65279) input = input.slice(1);
368
- const tokens = [];
369
- let pos = 0;
370
- let line = 1;
371
- let col = 1;
372
- let hadSpace = false;
373
- function peek(offset = 0) {
374
- return input[pos + offset] ?? "";
375
- }
376
- function advance() {
377
- const ch = input[pos++] ?? "";
378
- if (ch === "\n") {
379
- line++;
380
- col = 1;
381
- } else {
382
- col++;
383
- }
384
- return ch;
385
- }
386
- function push(kind, value, l, c, isQuoted = false) {
387
- tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: hadSpace });
388
- hadSpace = false;
389
- }
390
- while (pos < input.length) {
391
- const sl = line, sc = col;
392
- const ch = peek();
393
- if (ch === " " || ch === " " || ch === "\r") {
394
- advance();
395
- hadSpace = true;
396
- continue;
397
- }
398
- if (ch === "\n") {
399
- advance();
400
- if (tokens[tokens.length - 1]?.kind !== "newline") {
401
- push("newline", "\n", sl, sc);
397
+ function isHoconWhitespace(ch) {
398
+ const cp = ch.codePointAt(0) ?? -1;
399
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
400
+ if (cp >= 28 && cp <= 31) return true;
401
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
402
+ if (cp === 5760) return true;
403
+ if (cp >= 8192 && cp <= 8202) return true;
404
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
405
+ if (cp === 12288) return true;
406
+ return false;
407
+ }
408
+ function isHoconNewline(ch) {
409
+ return ch === "\n";
410
+ }
411
+ var SINGLE_CHAR_TOKENS = {
412
+ "{": "lbrace",
413
+ "}": "rbrace",
414
+ "[": "lbracket",
415
+ "]": "rbracket",
416
+ ",": "comma",
417
+ ":": "colon"
418
+ };
419
+ var Lexer = class {
420
+ constructor(input) {
421
+ this.input = input;
422
+ this.pos = 0;
423
+ this.line = 1;
424
+ this.col = 1;
425
+ this.hadSpace = false;
426
+ this.tokens = [];
427
+ if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
428
+ }
429
+ tokenize() {
430
+ while (this.pos < this.input.length) {
431
+ const sl = this.line, sc = this.col;
432
+ const ch = this.peek();
433
+ if (isHoconNewline(ch)) {
434
+ this.advance();
435
+ if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
436
+ this.push("newline", "\n", sl, sc);
437
+ }
438
+ continue;
402
439
  }
403
- continue;
404
- }
405
- if (ch === "/" && peek(1) === "/") {
406
- while (pos < input.length && peek() !== "\n") advance();
407
- hadSpace = true;
408
- continue;
409
- }
410
- if (ch === "#") {
411
- while (pos < input.length && peek() !== "\n") advance();
412
- hadSpace = true;
413
- continue;
414
- }
415
- const single = { "{": "lbrace", "}": "rbrace", "[": "lbracket", "]": "rbracket", ",": "comma", ":": "colon" };
416
- if (ch in single) {
417
- advance();
418
- push(single[ch], ch, sl, sc);
419
- continue;
420
- }
421
- if (ch === "=") {
422
- advance();
423
- push("equals", "=", sl, sc);
424
- continue;
425
- }
426
- if (ch === "+" && peek(1) === "=") {
427
- advance();
428
- advance();
429
- push("plus_equals", "+=", sl, sc);
430
- continue;
431
- }
432
- if (ch === "$" && peek(1) === "{") {
433
- advance();
434
- advance();
435
- const optional = peek() === "?";
436
- if (optional) advance();
437
- let path2 = "";
438
- while (pos < input.length && peek() !== "}") {
439
- if (peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
440
- path2 += advance();
440
+ if (isHoconWhitespace(ch)) {
441
+ this.advance();
442
+ this.hadSpace = true;
443
+ continue;
441
444
  }
442
- if (peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
443
- advance();
444
- push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
445
- continue;
446
- }
447
- if (ch === '"' && peek(1) === '"' && peek(2) === '"') {
448
- advance();
449
- advance();
450
- advance();
451
- let value = "";
452
- let closed = false;
453
- while (pos < input.length) {
454
- if (peek() === '"') {
455
- let quoteCount = 0;
456
- while (pos < input.length && peek() === '"') {
457
- quoteCount++;
458
- advance();
445
+ if (ch === "/" && this.peek(1) === "/") {
446
+ while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
447
+ this.hadSpace = true;
448
+ continue;
449
+ }
450
+ if (ch === "#") {
451
+ while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
452
+ this.hadSpace = true;
453
+ continue;
454
+ }
455
+ if (ch in SINGLE_CHAR_TOKENS) {
456
+ this.advance();
457
+ this.push(SINGLE_CHAR_TOKENS[ch], ch, sl, sc);
458
+ continue;
459
+ }
460
+ if (ch === "=") {
461
+ this.advance();
462
+ this.push("equals", "=", sl, sc);
463
+ continue;
464
+ }
465
+ if (ch === "+" && this.peek(1) === "=") {
466
+ this.advance();
467
+ this.advance();
468
+ this.push("plus_equals", "+=", sl, sc);
469
+ continue;
470
+ }
471
+ if (ch === "$" && this.peek(1) === "{") {
472
+ this.advance();
473
+ this.advance();
474
+ const payload = this.parseSubstBody(sl, sc);
475
+ const value = payload.segments.map((s) => {
476
+ const t = s.text;
477
+ if (t === "" || t.includes(".") || t.includes(" ") || t.includes(" ") || t.includes('"') || t.includes("\\") || t !== t.trim()) {
478
+ const escaped = t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
479
+ return `"${escaped}"`;
459
480
  }
460
- if (quoteCount >= 3) {
461
- for (let i = 0; i < quoteCount - 3; i++) value += '"';
462
- closed = true;
463
- break;
481
+ return t;
482
+ }).join(".");
483
+ this.pushSubst(payload, value, sl, sc);
484
+ continue;
485
+ }
486
+ if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
487
+ this.advance();
488
+ this.advance();
489
+ this.advance();
490
+ let value = "";
491
+ let closed = false;
492
+ while (this.pos < this.input.length) {
493
+ if (this.peek() === '"') {
494
+ let quoteCount = 0;
495
+ while (this.pos < this.input.length && this.peek() === '"') {
496
+ quoteCount++;
497
+ this.advance();
498
+ }
499
+ if (quoteCount >= 3) {
500
+ for (let i = 0; i < quoteCount - 3; i++) value += '"';
501
+ closed = true;
502
+ break;
503
+ }
504
+ for (let i = 0; i < quoteCount; i++) value += '"';
505
+ continue;
464
506
  }
465
- for (let i = 0; i < quoteCount; i++) value += '"';
466
- continue;
507
+ value += this.advance();
508
+ }
509
+ if (!closed) {
510
+ throw new ParseError("unterminated triple-quoted string", sl, sc);
467
511
  }
468
- value += advance();
512
+ if (value.startsWith("\n")) value = value.slice(1);
513
+ this.push("triple_string", value, sl, sc, true);
514
+ continue;
469
515
  }
470
- if (!closed) {
471
- throw new ParseError("unterminated triple-quoted string", sl, sc);
516
+ if (ch === '"') {
517
+ this.advance();
518
+ const value = this.readQuotedStringBody(sl, sc);
519
+ this.push("string", value, sl, sc, true);
520
+ continue;
472
521
  }
473
- if (value.startsWith("\n")) value = value.slice(1);
474
- push("triple_string", value, sl, sc, true);
475
- continue;
476
- }
477
- if (ch === '"') {
478
- advance();
479
- let value = "";
480
- while (pos < input.length && peek() !== '"') {
481
- if (peek() === "\n") throw new ParseError("unterminated string", sl, sc);
482
- if (peek() === "\\") {
483
- advance();
484
- const esc = advance();
485
- switch (esc) {
486
- case "n":
487
- value += "\n";
488
- break;
489
- case "t":
490
- value += " ";
491
- break;
492
- case "r":
493
- value += "\r";
494
- break;
495
- case '"':
496
- value += '"';
497
- break;
498
- case "\\":
499
- value += "\\";
500
- break;
501
- case "/":
502
- value += "/";
503
- break;
504
- case "b":
505
- value += "\b";
506
- break;
507
- case "f":
508
- value += "\f";
509
- break;
510
- case "u": {
511
- if (pos + 4 > input.length) {
512
- throw new ParseError("Invalid unicode escape: not enough characters", line, col);
513
- }
514
- const hex = input.slice(pos, pos + 4);
515
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
516
- throw new ParseError(`Invalid unicode escape: \\u${hex}`, line, col);
517
- }
518
- value += String.fromCharCode(parseInt(hex, 16));
519
- for (let i = 0; i < 4; i++) advance();
520
- break;
522
+ if (isUnquotedStart(ch)) {
523
+ if (ch === "-" && !isDecimalDigit(this.peek(1))) {
524
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
525
+ throw new ParseError(
526
+ `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
527
+ sl,
528
+ sc
529
+ );
530
+ }
531
+ let value = "";
532
+ while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
533
+ value += this.advance();
534
+ }
535
+ this.push("unquoted", value.trimEnd(), sl, sc);
536
+ continue;
537
+ }
538
+ throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
539
+ }
540
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
541
+ return this.tokens;
542
+ }
543
+ peek(offset = 0) {
544
+ return this.input[this.pos + offset] ?? "";
545
+ }
546
+ advance() {
547
+ const ch = this.input[this.pos++] ?? "";
548
+ if (ch === "\n") {
549
+ this.line++;
550
+ this.col = 1;
551
+ } else {
552
+ this.col++;
553
+ }
554
+ return ch;
555
+ }
556
+ push(kind, value, l, c, isQuoted = false) {
557
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
558
+ this.hadSpace = false;
559
+ }
560
+ pushSubst(payload, value, l, c) {
561
+ this.tokens.push({ kind: "subst", value, line: l, col: c, isQuoted: false, precedingSpace: this.hadSpace, subst: payload });
562
+ this.hadSpace = false;
563
+ }
564
+ /**
565
+ * Read body of a quoted string. Opening `"` must already be consumed.
566
+ * Returns decoded string. Throws ParseError on unterminated/invalid-escape.
567
+ * openLine/openCol are the position of the opening `"` for error reporting.
568
+ */
569
+ readQuotedStringBody(openLine, openCol) {
570
+ let value = "";
571
+ while (this.pos < this.input.length && this.peek() !== '"') {
572
+ if (this.peek() === "\n") throw new ParseError("unterminated string", openLine, openCol);
573
+ if (this.peek() === "\\") {
574
+ const escCol = this.col;
575
+ this.advance();
576
+ if (this.pos >= this.input.length) {
577
+ throw new ParseError("unterminated string", openLine, openCol);
578
+ }
579
+ const esc = this.advance();
580
+ switch (esc) {
581
+ case "n":
582
+ value += "\n";
583
+ break;
584
+ case "t":
585
+ value += " ";
586
+ break;
587
+ case "r":
588
+ value += "\r";
589
+ break;
590
+ case '"':
591
+ value += '"';
592
+ break;
593
+ case "\\":
594
+ value += "\\";
595
+ break;
596
+ case "/":
597
+ value += "/";
598
+ break;
599
+ case "b":
600
+ value += "\b";
601
+ break;
602
+ case "f":
603
+ value += "\f";
604
+ break;
605
+ case "u": {
606
+ if (this.pos + 4 > this.input.length) {
607
+ throw new ParseError("invalid unicode escape", openLine, escCol);
521
608
  }
522
- default:
523
- throw new ParseError(`Unknown escape sequence: \\${esc}`, line, col);
609
+ const hex = this.input.slice(this.pos, this.pos + 4);
610
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
611
+ throw new ParseError("invalid unicode escape", openLine, escCol);
612
+ }
613
+ const code = parseInt(hex, 16);
614
+ value += String.fromCharCode(code);
615
+ for (let i = 0; i < 4; i++) this.advance();
616
+ break;
524
617
  }
525
- } else {
526
- value += advance();
618
+ default:
619
+ throw new ParseError(`unknown escape sequence: \\${esc}`, openLine, escCol);
527
620
  }
621
+ } else {
622
+ value += this.advance();
528
623
  }
529
- if (peek() !== '"') throw new ParseError("unterminated string", sl, sc);
530
- advance();
531
- push("string", value, sl, sc, true);
532
- continue;
533
- }
534
- if (isUnquotedStart(ch)) {
535
- let value = "";
536
- while (pos < input.length && isUnquotedContinue(peek(), () => peek(1))) {
537
- value += advance();
624
+ }
625
+ if (this.pos >= this.input.length || this.peek() !== '"') {
626
+ throw new ParseError("unterminated string", openLine, openCol);
627
+ }
628
+ this.advance();
629
+ return value;
630
+ }
631
+ /**
632
+ * Parse the body of a `${...}` substitution (called after `${` has been consumed).
633
+ * Implements Appendix A state machine from the cross-language spec.
634
+ */
635
+ parseSubstBody(startLine, startCol) {
636
+ let optional = false;
637
+ if (this.peek() === "?") {
638
+ this.advance();
639
+ optional = true;
640
+ }
641
+ let curText = "";
642
+ let curStarted = false;
643
+ let curLine = 0;
644
+ let curCol = 0;
645
+ let pendingWs = "";
646
+ const segments = [];
647
+ let lastDot = null;
648
+ while (true) {
649
+ if (this.pos >= this.input.length) {
650
+ throw new ParseError("unterminated substitution", startLine, startCol);
651
+ }
652
+ const ch = this.peek();
653
+ if (ch === "}") {
654
+ this.advance();
655
+ pendingWs = "";
656
+ break;
657
+ } else if (ch === '"') {
658
+ const qLine = startLine;
659
+ const qCol = this.col;
660
+ if (curStarted) {
661
+ curText += pendingWs;
662
+ }
663
+ pendingWs = "";
664
+ this.advance();
665
+ const decoded = this.readQuotedStringBody(qLine, qCol);
666
+ curText += decoded;
667
+ if (!curStarted) {
668
+ curLine = qLine;
669
+ curCol = qCol;
670
+ curStarted = true;
671
+ }
672
+ } else if (isUnquotedSubstChar(ch)) {
673
+ if (ch === "-" && !curStarted && !isDecimalDigit(this.peek(1))) {
674
+ const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
675
+ throw new ParseError(
676
+ `unquoted path segment cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
677
+ startLine,
678
+ this.col
679
+ );
680
+ }
681
+ const uCol = this.col;
682
+ if (curStarted) {
683
+ curText += pendingWs;
684
+ }
685
+ pendingWs = "";
686
+ if (!curStarted) {
687
+ curLine = startLine;
688
+ curCol = uCol;
689
+ curStarted = true;
690
+ }
691
+ while (this.pos < this.input.length && isUnquotedSubstChar(this.peek())) {
692
+ curText += this.advance();
693
+ }
694
+ } else if (ch === ".") {
695
+ const dotCol = this.col;
696
+ pendingWs = "";
697
+ if (!curStarted) {
698
+ throw new ParseError("empty segment in path", startLine, dotCol);
699
+ }
700
+ segments.push({ text: curText, line: curLine, col: curCol });
701
+ curText = "";
702
+ curStarted = false;
703
+ curLine = 0;
704
+ curCol = 0;
705
+ lastDot = [startLine, dotCol];
706
+ this.advance();
707
+ } else if (isHoconNewline(ch)) {
708
+ throw new ParseError("unterminated substitution", startLine, startCol);
709
+ } else if (isHoconWhitespace(ch)) {
710
+ pendingWs += ch;
711
+ this.advance();
712
+ } else {
713
+ throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
538
714
  }
539
- push("unquoted", value.trimEnd(), sl, sc);
540
- continue;
541
715
  }
542
- throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
716
+ if (curStarted) {
717
+ segments.push({ text: curText, line: curLine, col: curCol });
718
+ } else if (segments.length === 0) {
719
+ throw new ParseError("empty substitution path", startLine, startCol);
720
+ } else {
721
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
722
+ throw new ParseError("empty segment in path", errLine, errCol);
723
+ }
724
+ return { segments, optional };
543
725
  }
544
- tokens.push({ kind: "eof", value: "", line, col, isQuoted: false, precedingSpace: false });
545
- return tokens;
726
+ };
727
+ function tokenize(input) {
728
+ return new Lexer(input).tokenize();
729
+ }
730
+ function isUnquotedSubstChar(ch) {
731
+ if (ch === "" || isHoconWhitespace(ch)) return false;
732
+ if ('"\\'.includes(ch)) return false;
733
+ if ("{}[]".includes(ch)) return false;
734
+ if (":=,+#`^?!@*&$.".includes(ch)) return false;
735
+ return true;
736
+ }
737
+ function isDecimalDigit(ch) {
738
+ return ch >= "0" && ch <= "9";
546
739
  }
547
740
  function isUnquotedStart(ch) {
548
- return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
741
+ if (ch === "" || isHoconWhitespace(ch)) return false;
742
+ if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
743
+ return true;
549
744
  }
550
745
  function isUnquotedContinue(ch, nextFn) {
551
- if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
746
+ if (ch === "" || isHoconWhitespace(ch)) return false;
747
+ if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
552
748
  if (ch === "+" && nextFn() === "=") return false;
553
749
  if (ch === "/" && nextFn() === "/") return false;
554
750
  return true;
555
751
  }
556
752
 
557
753
  // src/internal/parser/parser.ts
558
- function parseTokens(tokens) {
559
- let pos = 0;
560
- function peek() {
561
- return tokens[pos] ?? { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
754
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false, subst: void 0 };
755
+ var Parser = class {
756
+ constructor(tokens) {
757
+ this.tokens = tokens;
758
+ this.pos = 0;
759
+ }
760
+ parse() {
761
+ this.skip("newline");
762
+ const t = this.peek();
763
+ if (t.kind === "lbrace") {
764
+ this.advance();
765
+ const first = this.parseObject(true);
766
+ const allFields = first.kind === "object" ? [...first.fields] : [];
767
+ while (true) {
768
+ this.skip("newline");
769
+ if (this.peek().kind === "eof") break;
770
+ if (this.peek().kind === "lbrace") {
771
+ this.advance();
772
+ const extra = this.parseObject(true);
773
+ if (extra.kind === "object") allFields.push(...extra.fields);
774
+ } else {
775
+ const rest = this.parseObject(false);
776
+ if (rest.kind === "object") allFields.push(...rest.fields);
777
+ break;
778
+ }
779
+ }
780
+ this.skip("newline");
781
+ const remaining = this.peek();
782
+ if (remaining.kind !== "eof") {
783
+ throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
784
+ }
785
+ return { kind: "object", fields: allFields, pos: first.pos };
786
+ }
787
+ return this.parseObject(false);
788
+ }
789
+ peek(offset = 0) {
790
+ return this.tokens[this.pos + offset] ?? EOF_TOKEN;
562
791
  }
563
- function advance() {
564
- const t2 = tokens[pos];
565
- if (pos < tokens.length) pos++;
566
- return t2 ?? { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
792
+ advance() {
793
+ const t = this.tokens[this.pos];
794
+ if (this.pos < this.tokens.length) this.pos++;
795
+ return t ?? EOF_TOKEN;
567
796
  }
568
- function skip(...kinds) {
569
- while (kinds.includes(peek().kind)) advance();
797
+ skip(...kinds) {
798
+ while (kinds.includes(this.peek().kind)) this.advance();
570
799
  }
571
- function currentPos() {
572
- const t2 = peek();
573
- return { line: t2.line, col: t2.col };
800
+ currentPos() {
801
+ const t = this.peek();
802
+ return { line: t.line, col: t.col };
574
803
  }
575
- function parseObject(expectClosingBrace) {
576
- const p = currentPos();
804
+ parseObject(expectClosingBrace) {
805
+ const p = this.currentPos();
577
806
  const fields = [];
578
807
  while (true) {
579
- skip("newline");
580
- const t2 = peek();
581
- if (t2.kind === "eof" || t2.kind === "rbrace") break;
582
- if (t2.kind === "unquoted" && t2.value === "include") {
583
- advance();
584
- fields.push(parseInclude());
585
- skip("newline");
586
- if (peek().kind === "comma") advance();
587
- skip("newline");
808
+ this.skip("newline");
809
+ const t = this.peek();
810
+ if (t.kind === "eof" || t.kind === "rbrace") break;
811
+ if (t.kind === "unquoted" && t.value === "include") {
812
+ this.advance();
813
+ fields.push(this.parseInclude());
814
+ this.skip("newline");
815
+ if (this.peek().kind === "comma") this.advance();
816
+ this.skip("newline");
588
817
  continue;
589
818
  }
590
- const keyPos = currentPos();
591
- const key = parseKey();
592
- skip("newline");
819
+ const keyPos = this.currentPos();
820
+ const key = this.parseKey();
821
+ this.skip("newline");
593
822
  let append = false;
594
- const sep = peek();
595
- if (sep.kind === "equals") advance();
823
+ const sep = this.peek();
824
+ if (sep.kind === "equals") this.advance();
596
825
  else if (sep.kind === "plus_equals") {
597
- advance();
826
+ this.advance();
598
827
  append = true;
599
- } else if (sep.kind === "colon") advance();
828
+ } else if (sep.kind === "colon") this.advance();
600
829
  else if (sep.kind === "lbrace") {
601
830
  } else if (sep.kind !== "newline" && sep.kind !== "eof") {
602
831
  throw new ParseError(`unexpected token after key: ${sep.kind}`, sep.line, sep.col);
603
832
  }
604
- skip("newline");
605
- const value = parseValue();
833
+ this.skip("newline");
834
+ const value = this.parseValue();
606
835
  fields.push({ key, value, append, pos: keyPos });
607
- skip("newline");
608
- if (peek().kind === "comma") advance();
609
- skip("newline");
836
+ this.skip("newline");
837
+ if (this.peek().kind === "comma") this.advance();
838
+ this.skip("newline");
610
839
  }
611
840
  if (expectClosingBrace) {
612
- const t2 = peek();
613
- if (t2.kind !== "rbrace") throw new ParseError("expected }", t2.line, t2.col);
614
- advance();
841
+ const t = this.peek();
842
+ if (t.kind !== "rbrace") throw new ParseError("expected }", t.line, t.col);
843
+ this.advance();
615
844
  }
616
845
  return { kind: "object", fields, pos: p };
617
846
  }
618
- function parseKey() {
847
+ parseKey() {
619
848
  const segments = [];
620
849
  let trailingDot = false;
621
850
  while (true) {
622
- const t2 = peek();
623
- if (t2.kind === "string") {
624
- advance();
625
- segments.push(t2.value);
851
+ const t = this.peek();
852
+ if (t.kind === "string") {
853
+ this.advance();
854
+ segments.push(t.value);
626
855
  trailingDot = false;
627
- } else if (t2.kind === "unquoted") {
628
- advance();
629
- const parts = t2.value.split(".");
856
+ } else if (t.kind === "unquoted") {
857
+ this.advance();
858
+ const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
859
+ const parts = raw.split(".");
630
860
  const filtered = parts.filter((s) => s.length > 0);
861
+ for (const seg of filtered) {
862
+ if (seg[0] === "-" && !(seg.length >= 2 && seg[1] >= "0" && seg[1] <= "9")) {
863
+ const after = seg.length >= 2 ? JSON.stringify(seg[1]) : "EOF";
864
+ throw new ParseError(
865
+ `unquoted key segment cannot begin with '-' unless followed by a digit (got '-' then ${after} in ${JSON.stringify(seg)}, HOCON.md L270-276)`,
866
+ t.line,
867
+ t.col
868
+ );
869
+ }
870
+ }
631
871
  segments.push(...filtered);
632
- trailingDot = t2.value.endsWith(".");
872
+ trailingDot = raw.endsWith(".");
633
873
  } else {
634
- if (segments.length === 0) throw new ParseError(`expected key, got ${t2.kind}`, t2.line, t2.col);
874
+ if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
635
875
  break;
636
876
  }
637
877
  if (trailingDot) continue;
638
- const next = peek();
878
+ const next = this.peek();
639
879
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
640
- advance();
880
+ this.advance();
881
+ trailingDot = true;
882
+ continue;
883
+ }
884
+ if (next.kind === "unquoted" && next.value.startsWith(".") && !next.precedingSpace) {
641
885
  trailingDot = true;
642
886
  continue;
643
887
  }
@@ -645,147 +889,230 @@ function parseTokens(tokens) {
645
889
  }
646
890
  return segments;
647
891
  }
648
- function parseInclude() {
649
- const p = currentPos();
650
- skip("newline");
651
- const t2 = peek();
892
+ parseInclude() {
893
+ const p = this.currentPos();
894
+ this.skip("newline");
895
+ const t = this.peek();
652
896
  let path2;
653
897
  let required = false;
654
- if (t2.kind === "unquoted" && (t2.value === "required(" || t2.value === "required" || t2.value.startsWith("required("))) {
655
- if (t2.value === "required") {
656
- const next2 = tokens[pos + 1] ?? { kind: "eof", value: "" };
898
+ let isFile = false;
899
+ if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
900
+ if (t.value === "required") {
901
+ const next2 = this.peek(1);
657
902
  if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
658
- throw new ParseError("include required must be followed by (", t2.line, t2.col);
903
+ throw new ParseError("include required must be followed by (", t.line, t.col);
659
904
  }
660
905
  }
661
906
  required = true;
662
- advance();
663
- const innerPrefix = t2.value.startsWith("required(") ? t2.value.slice("required(".length) : "";
907
+ this.advance();
908
+ const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
664
909
  if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
665
- throw new ParseError("include url(...) and classpath(...) are not supported", t2.line, t2.col);
910
+ throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
666
911
  }
667
- const next = peek();
912
+ if (innerPrefix.startsWith("file")) {
913
+ isFile = true;
914
+ }
915
+ const next = this.peek();
668
916
  if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
669
917
  throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
670
918
  }
671
- while (peek().kind !== "string" && peek().kind !== "eof") advance();
672
- if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
673
- path2 = advance().value;
674
- while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
675
- } else if (t2.kind === "string") {
676
- path2 = advance().value;
677
- } else if (t2.kind === "unquoted" && (t2.value === "file(" || t2.value === "file")) {
678
- advance();
679
- while (peek().kind !== "string" && peek().kind !== "eof") advance();
680
- if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
681
- path2 = advance().value;
682
- while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
683
- } else if (t2.kind === "unquoted" && (t2.value === "url" || t2.value.startsWith("url("))) {
684
- throw new ParseError("include url(...) is not supported", t2.line, t2.col);
685
- } else if (t2.kind === "unquoted" && (t2.value === "classpath" || t2.value.startsWith("classpath("))) {
686
- throw new ParseError("include classpath(...) is not supported", t2.line, t2.col);
919
+ if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
920
+ isFile = true;
921
+ }
922
+ while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
923
+ if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
924
+ path2 = this.advance().value;
925
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
926
+ } else if (t.kind === "string") {
927
+ path2 = this.advance().value;
928
+ } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
929
+ isFile = true;
930
+ this.advance();
931
+ while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
932
+ if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
933
+ path2 = this.advance().value;
934
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
935
+ } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
936
+ throw new ParseError("include url(...) is not supported", t.line, t.col);
937
+ } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
938
+ throw new ParseError("include classpath(...) is not supported", t.line, t.col);
687
939
  } else {
688
- throw new ParseError(`expected include path, got ${t2.kind}`, t2.line, t2.col);
940
+ throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
689
941
  }
690
942
  return {
691
943
  key: [],
692
- value: { kind: "include", path: path2, required, pos: p },
944
+ value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
693
945
  append: false,
694
946
  pos: p
695
947
  };
696
948
  }
697
- function parseValue() {
698
- const p = currentPos();
949
+ parseValue() {
950
+ const p = this.currentPos();
699
951
  const parts = [];
700
952
  while (true) {
701
- const t2 = peek();
702
- if (t2.kind === "eof" || t2.kind === "newline" || t2.kind === "rbrace" || t2.kind === "rbracket" || t2.kind === "comma") break;
703
- const hadSpace = t2.precedingSpace && parts.length > 0;
953
+ const t = this.peek();
954
+ if (t.kind === "eof" || t.kind === "newline" || t.kind === "rbrace" || t.kind === "rbracket" || t.kind === "comma") break;
955
+ const hadSpace = t.precedingSpace && parts.length > 0;
704
956
  let node;
705
- if (t2.kind === "lbrace") {
706
- advance();
707
- node = parseObject(true);
708
- } else if (t2.kind === "lbracket") {
709
- advance();
710
- node = parseArray();
711
- } else if (t2.kind === "subst" || t2.kind === "opt_subst") {
712
- advance();
713
- node = { kind: "subst", path: t2.value, optional: t2.kind === "opt_subst", pos: { line: t2.line, col: t2.col } };
714
- } else if (t2.kind === "string" || t2.kind === "triple_string") {
715
- advance();
716
- node = { kind: "scalar", value: t2.value, pos: { line: t2.line, col: t2.col } };
717
- } else if (t2.kind === "unquoted") {
718
- advance();
719
- node = { kind: "scalar", value: parseScalarValue(t2.value), pos: { line: t2.line, col: t2.col } };
720
- } else if ((t2.kind === "colon" || t2.kind === "equals") && parts.length > 0) {
721
- advance();
722
- node = { kind: "scalar", value: t2.value, pos: { line: t2.line, col: t2.col } };
957
+ if (t.kind === "lbrace") {
958
+ this.advance();
959
+ node = this.parseObject(true);
960
+ } else if (t.kind === "lbracket") {
961
+ this.advance();
962
+ node = this.parseArray();
963
+ } else if (t.kind === "subst") {
964
+ this.advance();
965
+ if (!t.subst) {
966
+ throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
967
+ }
968
+ const payload = t.subst;
969
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
970
+ } else if (t.kind === "string" || t.kind === "triple_string") {
971
+ this.advance();
972
+ node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
973
+ } else if (t.kind === "unquoted") {
974
+ this.advance();
975
+ node = { kind: "scalar", raw: t.value, valueType: this.scalarValueType(t.value), pos: { line: t.line, col: t.col } };
976
+ } else if ((t.kind === "colon" || t.kind === "equals") && parts.length > 0) {
977
+ this.advance();
978
+ node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
723
979
  } else {
724
980
  break;
725
981
  }
726
982
  if (hadSpace) {
727
- parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col }, _separator: true });
983
+ parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
728
984
  }
729
985
  parts.push(node);
730
986
  }
731
- if (parts.length === 0) throw new ParseError("expected value", peek().line, peek().col);
987
+ if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
732
988
  if (parts.length === 1) return parts[0];
733
989
  return { kind: "concat", nodes: parts, pos: p };
734
990
  }
735
- function parseArray() {
736
- const p = currentPos();
991
+ parseArray() {
992
+ const p = this.currentPos();
737
993
  const items = [];
738
994
  while (true) {
739
- skip("newline");
740
- if (peek().kind === "rbracket" || peek().kind === "eof") break;
741
- items.push(parseValue());
742
- skip("newline");
743
- if (peek().kind === "comma") advance();
744
- skip("newline");
745
- }
746
- const t2 = peek();
747
- if (t2.kind !== "rbracket") throw new ParseError("expected ]", t2.line, t2.col);
748
- advance();
995
+ this.skip("newline");
996
+ if (this.peek().kind === "rbracket" || this.peek().kind === "eof") break;
997
+ items.push(this.parseValue());
998
+ this.skip("newline");
999
+ if (this.peek().kind === "comma") this.advance();
1000
+ this.skip("newline");
1001
+ }
1002
+ const t = this.peek();
1003
+ if (t.kind !== "rbracket") throw new ParseError("expected ]", t.line, t.col);
1004
+ this.advance();
749
1005
  return { kind: "array", items, pos: p };
750
1006
  }
751
- function parseScalarValue(raw) {
752
- if (raw === "true") return true;
753
- if (raw === "false") return false;
754
- if (raw === "null") return null;
755
- const n = Number(raw);
756
- if (!isNaN(n) && raw.trim() !== "") return n;
757
- return raw;
758
- }
759
- skip("newline");
760
- const t = peek();
761
- if (t.kind === "lbrace") {
762
- advance();
763
- const first = parseObject(true);
764
- const allFields = first.kind === "object" ? [...first.fields] : [];
765
- while (true) {
766
- skip("newline");
767
- if (peek().kind === "eof") break;
768
- if (peek().kind === "lbrace") {
769
- advance();
770
- const extra = parseObject(true);
771
- if (extra.kind === "object") allFields.push(...extra.fields);
772
- } else {
773
- const rest = parseObject(false);
774
- if (rest.kind === "object") allFields.push(...rest.fields);
775
- break;
776
- }
1007
+ scalarValueType(raw) {
1008
+ if (raw === "true" || raw === "false") return "boolean";
1009
+ if (raw === "null") return "null";
1010
+ if (raw.length > 0 && (raw.charCodeAt(0) >= 48 && raw.charCodeAt(0) <= 57 || raw.charCodeAt(0) === 45)) {
1011
+ if (DECIMAL_NUMBER_RE.test(raw)) return "number";
1012
+ }
1013
+ return "string";
1014
+ }
1015
+ };
1016
+ function parseTokens(tokens) {
1017
+ return new Parser(tokens).parse();
1018
+ }
1019
+
1020
+ // src/internal/resolver/types.ts
1021
+ var separatorValues = /* @__PURE__ */ new WeakSet();
1022
+ function isSubst(v) {
1023
+ return v._kind === "subst-placeholder";
1024
+ }
1025
+ function isConcat(v) {
1026
+ return v._kind === "concat-placeholder";
1027
+ }
1028
+ function isAppend(v) {
1029
+ return v._kind === "append-placeholder";
1030
+ }
1031
+ function isResObj(v) {
1032
+ return v._kind === "res-obj";
1033
+ }
1034
+ function makeResObj() {
1035
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
1036
+ }
1037
+
1038
+ // src/internal/resolver/utils.ts
1039
+ function segmentsToKey(segments) {
1040
+ return segments.map((s) => {
1041
+ const t = s.text;
1042
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
1043
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1044
+ }
1045
+ return t;
1046
+ }).join(".");
1047
+ }
1048
+ function lookupPath(root, segments) {
1049
+ const [head, ...tail] = segments;
1050
+ if (head === void 0) return void 0;
1051
+ const key = head.text;
1052
+ const val = root.fields.get(key);
1053
+ if (val === void 0) return void 0;
1054
+ if (tail.length === 0) return val;
1055
+ if (isResObj(val)) return lookupPath(val, tail);
1056
+ return void 0;
1057
+ }
1058
+ function lookupResObj(root, segments) {
1059
+ let cur = root;
1060
+ for (const seg of segments) {
1061
+ const val = cur.fields.get(seg.text);
1062
+ if (val === void 0 || !isResObj(val)) return void 0;
1063
+ cur = val;
1064
+ }
1065
+ return cur;
1066
+ }
1067
+ function deepMergeHoconValues(base, overlay) {
1068
+ const merged = new Map(base.fields);
1069
+ for (const [k, v] of overlay.fields) {
1070
+ const existing = merged.get(k);
1071
+ if (existing?.kind === "object" && v.kind === "object") {
1072
+ merged.set(k, deepMergeHoconValues(existing, v));
1073
+ } else {
1074
+ merged.set(k, v);
777
1075
  }
778
- skip("newline");
779
- const remaining = peek();
780
- if (remaining.kind !== "eof") {
781
- throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
1076
+ }
1077
+ return { kind: "object", fields: merged };
1078
+ }
1079
+ function deepMergeResObjInto(dst, src) {
1080
+ for (const [k, srcVal] of src.fields) {
1081
+ const dstVal = dst.fields.get(k);
1082
+ if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
1083
+ deepMergeResObjInto(dstVal, srcVal);
1084
+ } else {
1085
+ if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
1086
+ dst.fields.set(k, srcVal);
1087
+ }
1088
+ }
1089
+ for (const [k, srcPrior] of src.priorValues) {
1090
+ if (!dst.priorValues.has(k)) {
1091
+ dst.priorValues.set(k, srcPrior);
782
1092
  }
783
- return { kind: "object", fields: allFields, pos: first.pos };
784
1093
  }
785
- return parseObject(false);
1094
+ }
1095
+ function hoconValueToResObj(hv) {
1096
+ const obj = makeResObj();
1097
+ if (hv.kind !== "object") return obj;
1098
+ for (const [key, val] of hv.fields) {
1099
+ if (val.kind === "object") {
1100
+ obj.fields.set(key, hoconValueToResObj(val));
1101
+ } else {
1102
+ obj.fields.set(key, val);
1103
+ }
1104
+ }
1105
+ return obj;
1106
+ }
1107
+ function isFileNotFoundError(e) {
1108
+ if (!(e instanceof Error)) return false;
1109
+ const code = e.code;
1110
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1111
+ const msg = e.message.toLowerCase();
1112
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
786
1113
  }
787
1114
 
788
- // src/internal/resolver/resolver.ts
1115
+ // src/internal/resolver/include-loader.ts
789
1116
  var nodePath = __toESM(require("path"), 1);
790
1117
 
791
1118
  // src/internal/properties/properties.ts
@@ -832,7 +1159,7 @@ function recordToHoconValue(obj) {
832
1159
  const fields = /* @__PURE__ */ new Map();
833
1160
  for (const [key, val] of Object.entries(obj)) {
834
1161
  if (typeof val === "string") {
835
- fields.set(key, { kind: "scalar", value: val });
1162
+ fields.set(key, { kind: "scalar", raw: val, valueType: "string" });
836
1163
  } else if (val !== null && typeof val === "object") {
837
1164
  fields.set(key, recordToHoconValue(val));
838
1165
  }
@@ -840,497 +1167,571 @@ function recordToHoconValue(obj) {
840
1167
  return { kind: "object", fields };
841
1168
  }
842
1169
 
843
- // src/internal/resolver/resolver.ts
844
- var separatorValues = /* @__PURE__ */ new WeakSet();
845
- function isSubst(v) {
846
- return v._kind === "subst-placeholder";
847
- }
848
- function isConcat(v) {
849
- return v._kind === "concat-placeholder";
850
- }
851
- function isAppend(v) {
852
- return v._kind === "append-placeholder";
853
- }
854
- function isResObj(v) {
855
- return v._kind === "res-obj";
856
- }
857
- function makeResObj() {
858
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
859
- }
860
- function resolve2(ast, opts) {
861
- const root = buildResObj(ast, opts);
862
- const resolving = /* @__PURE__ */ new Set();
863
- const resolvedCache = /* @__PURE__ */ new Map();
864
- return resolveResObj(root, root, resolving, resolvedCache, opts);
865
- }
866
- async function resolveAsync(ast, opts) {
867
- const root = await buildResObjAsync(ast, opts);
868
- const resolving = /* @__PURE__ */ new Set();
869
- const resolvedCache = /* @__PURE__ */ new Map();
870
- return resolveResObj(root, root, resolving, resolvedCache, opts);
871
- }
872
- function buildResObj(ast, opts) {
873
- if (ast.kind !== "object") {
874
- throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
1170
+ // src/internal/resolver/include-loader.ts
1171
+ var IncludeLoader = class {
1172
+ constructor(opts) {
1173
+ this.opts = opts;
1174
+ }
1175
+ /**
1176
+ * Resolve an include path to an absolute path.
1177
+ * - file() includes resolve relative to CWD (or as absolute paths),
1178
+ * NOT relative to the including file's directory.
1179
+ * - Bare includes resolve relative to the including file's directory (baseDir).
1180
+ */
1181
+ resolveIncludePath(includePath, baseDir, isFile) {
1182
+ return isFile ? nodePath.resolve(includePath) : baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
1183
+ }
1184
+ load(includePath, required, isFile) {
1185
+ const { baseDir, includeStack = [] } = this.opts;
1186
+ const absPath = this.resolveIncludePath(includePath, baseDir, !!isFile);
1187
+ if (includeStack.includes(absPath)) {
1188
+ throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
1189
+ }
1190
+ if (includeStack.length >= 50) {
1191
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
1192
+ }
1193
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1194
+ if (hasExplicitExt) {
1195
+ const result = this.loadSingle(absPath);
1196
+ if (result !== void 0) return result;
1197
+ if (required) {
1198
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1199
+ }
1200
+ return makeResObj();
1201
+ }
1202
+ const merged = makeResObj();
1203
+ let foundAny = false;
1204
+ const probeExts = [".properties", ".json", ".conf"];
1205
+ for (const ext of probeExts) {
1206
+ const obj = this.loadSingle(`${absPath}${ext}`);
1207
+ if (obj !== void 0) {
1208
+ deepMergeResObjInto(merged, obj);
1209
+ foundAny = true;
1210
+ }
1211
+ }
1212
+ if (!foundAny && required) {
1213
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1214
+ }
1215
+ return merged;
875
1216
  }
876
- const obj = makeResObj();
877
- for (const field of ast.fields) {
878
- applyField(obj, field, opts);
1217
+ async loadAsync(includePath, required, isFile) {
1218
+ const { baseDir, includeStack = [] } = this.opts;
1219
+ const absPath = this.resolveIncludePath(includePath, baseDir, !!isFile);
1220
+ if (includeStack.includes(absPath)) {
1221
+ throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
1222
+ }
1223
+ if (includeStack.length >= 50) {
1224
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
1225
+ }
1226
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1227
+ if (hasExplicitExt) {
1228
+ const result = await this.loadSingleAsync(absPath);
1229
+ if (result !== void 0) return result;
1230
+ if (required) {
1231
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1232
+ }
1233
+ return makeResObj();
1234
+ }
1235
+ const merged = makeResObj();
1236
+ let foundAny = false;
1237
+ const probeExts = [".properties", ".json", ".conf"];
1238
+ for (const ext of probeExts) {
1239
+ const obj = await this.loadSingleAsync(`${absPath}${ext}`);
1240
+ if (obj !== void 0) {
1241
+ deepMergeResObjInto(merged, obj);
1242
+ foundAny = true;
1243
+ }
1244
+ }
1245
+ if (!foundAny && required) {
1246
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1247
+ }
1248
+ return merged;
879
1249
  }
880
- return obj;
881
- }
882
- function applyField(obj, field, opts) {
883
- if (field.key.length === 0 && field.value.kind === "include") {
884
- const included = loadInclude(field.value.path, field.value.required, opts);
885
- deepMergeResObjInto(obj, included);
886
- return;
887
- }
888
- const [head, ...tail] = field.key;
889
- if (!head) return;
890
- if (tail.length > 0) {
891
- const syntheticAst = {
892
- kind: "object",
893
- fields: [{ key: tail, value: field.value, append: field.append, pos: field.pos }],
894
- pos: field.pos
895
- };
896
- applyField(obj, { key: [head], value: syntheticAst, append: false, pos: field.pos }, opts);
897
- return;
898
- }
899
- if (field.append) {
900
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
901
- obj.priorValues.set(head, existing2);
902
- obj.fields.set(head, {
903
- _kind: "append-placeholder",
904
- existing: existing2,
905
- elem: astToResolverValue(field.value, opts)
1250
+ loadSingle(candidate) {
1251
+ const { readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1252
+ if (includeStack.includes(candidate)) {
1253
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1254
+ }
1255
+ let content;
1256
+ try {
1257
+ content = readFileSync2(candidate);
1258
+ } catch (e) {
1259
+ if (isFileNotFoundError(e)) return void 0;
1260
+ throw e;
1261
+ }
1262
+ if (candidate.endsWith(".properties")) {
1263
+ return hoconValueToResObj(propertiesToHoconValue(content));
1264
+ }
1265
+ const ast = parseTokens(tokenize(content));
1266
+ return this.onBuildResObj(ast, {
1267
+ env,
1268
+ baseDir: nodePath.dirname(candidate),
1269
+ readFileSync: readFileSync2,
1270
+ includeStack: [...includeStack, candidate]
906
1271
  });
907
- return;
908
1272
  }
909
- const existing = obj.fields.get(head);
910
- const newVal = astToResolverValue(field.value, opts);
911
- if (existing !== void 0) {
912
- obj.priorValues.set(head, existing);
913
- }
914
- if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
915
- deepMergeResObjInto(existing, newVal);
916
- return;
917
- }
918
- obj.fields.set(head, newVal);
919
- }
920
- function astToResolverValue(ast, opts) {
921
- switch (ast.kind) {
922
- case "scalar": {
923
- const sv = { kind: "scalar", value: ast.value };
924
- if (ast._separator) separatorValues.add(sv);
925
- return sv;
1273
+ async loadSingleAsync(candidate) {
1274
+ const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1275
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1276
+ if (includeStack.includes(candidate)) {
1277
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
926
1278
  }
927
- case "array":
928
- return { kind: "array", items: ast.items.map((i) => astToResolverValue(i, opts)) };
929
- case "object": {
930
- const inner = buildResObj(ast, opts);
931
- return inner;
1279
+ let content;
1280
+ try {
1281
+ content = await read(candidate);
1282
+ } catch (e) {
1283
+ if (isFileNotFoundError(e)) return void 0;
1284
+ throw e;
1285
+ }
1286
+ if (candidate.endsWith(".properties")) {
1287
+ return hoconValueToResObj(propertiesToHoconValue(content));
932
1288
  }
933
- case "subst":
934
- return { _kind: "subst-placeholder", path: ast.path, optional: ast.optional, line: ast.pos.line, col: ast.pos.col };
935
- case "concat":
936
- return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => astToResolverValue(n, opts)) };
937
- case "include":
938
- return { kind: "scalar", value: null };
1289
+ const ast = parseTokens(tokenize(content));
1290
+ return this.onBuildResObjAsync(ast, {
1291
+ env,
1292
+ baseDir: nodePath.dirname(candidate),
1293
+ readFileSync: readFileSync2,
1294
+ readFile,
1295
+ includeStack: [...includeStack, candidate]
1296
+ });
939
1297
  }
940
- }
941
- function hoconValueToResObj(hv) {
942
- const obj = makeResObj();
943
- if (hv.kind !== "object") return obj;
944
- for (const [key, val] of hv.fields) {
945
- if (val.kind === "object") {
946
- obj.fields.set(key, hoconValueToResObj(val));
947
- } else {
948
- obj.fields.set(key, val);
1298
+ };
1299
+
1300
+ // src/internal/resolver/structure-builder.ts
1301
+ var StructureBuilder = class _StructureBuilder {
1302
+ constructor(opts) {
1303
+ this.opts = opts;
1304
+ this.loader = new IncludeLoader(opts);
1305
+ this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1306
+ this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
1307
+ }
1308
+ build(ast, pathPrefix = []) {
1309
+ if (ast.kind !== "object") {
1310
+ throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
1311
+ }
1312
+ const obj = makeResObj();
1313
+ for (const field of ast.fields) {
1314
+ this.applyField(obj, field, pathPrefix);
949
1315
  }
1316
+ return obj;
950
1317
  }
951
- return obj;
952
- }
953
- function deepMergeResObjInto(dst, src) {
954
- for (const [k, srcVal] of src.fields) {
955
- const dstVal = dst.fields.get(k);
956
- if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
957
- deepMergeResObjInto(dstVal, srcVal);
958
- } else {
959
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
960
- dst.fields.set(k, srcVal);
1318
+ async buildAsync(ast, pathPrefix = []) {
1319
+ if (ast.kind !== "object") {
1320
+ throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
961
1321
  }
1322
+ const obj = makeResObj();
1323
+ for (const field of ast.fields) {
1324
+ await this.applyFieldAsync(obj, field, pathPrefix);
1325
+ }
1326
+ return obj;
962
1327
  }
963
- }
964
- function resolveResObj(obj, root, resolving, resolvedCache, opts) {
965
- const result = /* @__PURE__ */ new Map();
966
- for (const [key, val] of obj.fields) {
967
- const resolved = resolveVal(val, obj, root, resolving, resolvedCache, opts);
968
- if (resolved !== void 0) {
969
- result.set(key, resolved);
970
- } else {
971
- const prior = obj.priorValues.get(key);
972
- if (prior !== void 0) {
973
- const priorResolved = resolveVal(prior, obj, root, resolving, resolvedCache, opts);
974
- if (priorResolved !== void 0) result.set(key, priorResolved);
1328
+ applyField(obj, field, pathPrefix) {
1329
+ if (field.key.length === 0 && field.value.kind === "include") {
1330
+ const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
1331
+ if (pathPrefix.length > 0) {
1332
+ this.relativizeResObj(included, pathPrefix);
975
1333
  }
1334
+ deepMergeResObjInto(obj, included);
1335
+ return;
976
1336
  }
1337
+ const [head, ...tail] = field.key;
1338
+ if (head === void 0) return;
1339
+ if (tail.length > 0) {
1340
+ const syntheticAst = {
1341
+ kind: "object",
1342
+ fields: [{ key: tail, value: field.value, append: field.append, pos: field.pos }],
1343
+ pos: field.pos
1344
+ };
1345
+ this.applyField(obj, { key: [head], value: syntheticAst, append: false, pos: field.pos }, pathPrefix);
1346
+ return;
1347
+ }
1348
+ const childPrefix = [...pathPrefix, head];
1349
+ if (field.append) {
1350
+ const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1351
+ obj.priorValues.set(head, existing2);
1352
+ obj.fields.set(head, {
1353
+ _kind: "append-placeholder",
1354
+ existing: existing2,
1355
+ elem: this.astToResolverValue(field.value, childPrefix)
1356
+ });
1357
+ return;
1358
+ }
1359
+ const existing = obj.fields.get(head);
1360
+ const newVal = this.astToResolverValue(field.value, childPrefix);
1361
+ if (existing !== void 0) {
1362
+ obj.priorValues.set(head, existing);
1363
+ }
1364
+ if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1365
+ deepMergeResObjInto(existing, newVal);
1366
+ return;
1367
+ }
1368
+ obj.fields.set(head, newVal);
977
1369
  }
978
- return { kind: "object", fields: result };
979
- }
980
- function resolveVal(v, scope, root, resolving, resolvedCache, opts) {
981
- if (isSubst(v)) return resolveSubst(v, scope, root, resolving, resolvedCache, opts);
982
- if (isConcat(v)) return resolveConcat(v.nodes, scope, root, resolving, resolvedCache, opts);
983
- if (isAppend(v)) return resolveAppend(v, scope, root, resolving, resolvedCache, opts);
984
- if (isResObj(v)) return resolveResObj(v, root, resolving, resolvedCache, opts);
985
- const hv = v;
986
- if (hv.kind === "array") {
987
- return {
988
- kind: "array",
989
- items: hv.items.map(
990
- (item) => resolveVal(item, scope, root, resolving, resolvedCache, opts) ?? { kind: "scalar", value: null }
991
- )
992
- };
993
- }
994
- return hv;
995
- }
996
- function resolveSubst(s, scope, root, resolving, resolvedCache, opts) {
997
- if (resolvedCache.has(s.path)) return resolvedCache.get(s.path);
998
- if (resolving.has(s.path)) {
999
- const rootSeg = parseSubstPath(s.path)[0] ?? "";
1000
- const prior = scope.priorValues.get(rootSeg) ?? root.priorValues.get(rootSeg);
1001
- if (prior !== void 0) {
1002
- return resolveVal(prior, scope, root, new Set(resolving), resolvedCache, opts);
1003
- }
1004
- if (s.optional) return void 0;
1005
- throw new ResolveError(`circular substitution: ${s.path}`, s.path, s.line, s.col);
1006
- }
1007
- resolving.add(s.path);
1008
- try {
1009
- const found = lookupPath(root, parseSubstPath(s.path));
1010
- if (found !== void 0) {
1011
- if (isSubst(found) || isConcat(found)) {
1012
- const rootSeg = parseSubstPath(s.path)[0] ?? "";
1013
- const prior = scope.priorValues.get(rootSeg) ?? root.priorValues.get(rootSeg);
1014
- if (prior !== void 0) {
1015
- const result2 = resolveVal(prior, scope, root, resolving, resolvedCache, opts);
1016
- if (result2 !== void 0) resolvedCache.set(s.path, result2);
1017
- return result2;
1018
- }
1370
+ async applyFieldAsync(obj, field, pathPrefix) {
1371
+ if (field.key.length === 0 && field.value.kind === "include") {
1372
+ const included = await this.loader.loadAsync(field.value.path, field.value.required, field.value.isFile);
1373
+ if (pathPrefix.length > 0) {
1374
+ this.relativizeResObj(included, pathPrefix);
1019
1375
  }
1020
- const result = resolveVal(found, scope, root, resolving, resolvedCache, opts);
1021
- if (result !== void 0) resolvedCache.set(s.path, result);
1022
- return result;
1376
+ deepMergeResObjInto(obj, included);
1377
+ return;
1378
+ }
1379
+ const [head, ...tail] = field.key;
1380
+ if (head === void 0) return;
1381
+ if (tail.length > 0) {
1382
+ const syntheticAst = {
1383
+ kind: "object",
1384
+ fields: [{ key: tail, value: field.value, append: field.append, pos: field.pos }],
1385
+ pos: field.pos
1386
+ };
1387
+ await this.applyFieldAsync(obj, { key: [head], value: syntheticAst, append: false, pos: field.pos }, pathPrefix);
1388
+ return;
1023
1389
  }
1024
- const envVal = opts.env[s.path];
1025
- if (envVal !== void 0) {
1026
- const result = { kind: "scalar", value: envVal };
1027
- resolvedCache.set(s.path, result);
1028
- return result;
1390
+ const childPrefix = [...pathPrefix, head];
1391
+ if (field.append) {
1392
+ const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1393
+ obj.priorValues.set(head, existing2);
1394
+ obj.fields.set(head, {
1395
+ _kind: "append-placeholder",
1396
+ existing: existing2,
1397
+ elem: await this.astToResolverValueAsync(field.value, childPrefix)
1398
+ });
1399
+ return;
1400
+ }
1401
+ const existing = obj.fields.get(head);
1402
+ const newVal = await this.astToResolverValueAsync(field.value, childPrefix);
1403
+ if (existing !== void 0) {
1404
+ obj.priorValues.set(head, existing);
1405
+ }
1406
+ if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1407
+ deepMergeResObjInto(existing, newVal);
1408
+ return;
1409
+ }
1410
+ obj.fields.set(head, newVal);
1411
+ }
1412
+ astToResolverValue(ast, pathPrefix) {
1413
+ switch (ast.kind) {
1414
+ case "scalar": {
1415
+ const sv = { kind: "scalar", raw: ast.raw, valueType: ast.valueType };
1416
+ if (ast._separator) separatorValues.add(sv);
1417
+ return sv;
1418
+ }
1419
+ case "array":
1420
+ return { kind: "array", items: ast.items.map((i) => this.astToResolverValue(i, pathPrefix)) };
1421
+ case "object": {
1422
+ const inner = this.build(ast, pathPrefix);
1423
+ return inner;
1424
+ }
1425
+ case "subst":
1426
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1427
+ case "concat":
1428
+ return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1429
+ case "include":
1430
+ return { kind: "scalar", raw: "null", valueType: "null" };
1029
1431
  }
1030
- if (s.optional) return void 0;
1031
- throw new ResolveError(`could not resolve substitution: \${${s.path}}`, s.path, s.line, s.col);
1032
- } finally {
1033
- resolving.delete(s.path);
1034
1432
  }
1035
- }
1036
- function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
1037
- const resolved = nodes.map((n) => resolveVal(n, scope, root, resolving, resolvedCache, opts)).filter((v) => v !== void 0);
1038
- if (resolved.length === 0) return { kind: "scalar", value: null };
1039
- if (resolved.length === 1) return resolved[0];
1040
- const nonSep = resolved.filter((v) => !separatorValues.has(v));
1041
- if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1042
- const merged = /* @__PURE__ */ new Map();
1043
- for (const v of nonSep) {
1044
- if (v.kind === "object") {
1045
- for (const [k, val] of v.fields) {
1046
- const existing = merged.get(k);
1047
- if (existing?.kind === "object" && val.kind === "object") {
1048
- merged.set(k, deepMergeHoconValues(existing, val));
1049
- } else {
1050
- merged.set(k, val);
1051
- }
1433
+ async astToResolverValueAsync(ast, pathPrefix) {
1434
+ switch (ast.kind) {
1435
+ case "scalar": {
1436
+ const sv = { kind: "scalar", raw: ast.raw, valueType: ast.valueType };
1437
+ if (ast._separator) separatorValues.add(sv);
1438
+ return sv;
1439
+ }
1440
+ case "array": {
1441
+ const items = [];
1442
+ for (const i of ast.items) {
1443
+ items.push(await this.astToResolverValueAsync(i, pathPrefix));
1052
1444
  }
1445
+ return { kind: "array", items };
1053
1446
  }
1447
+ case "object":
1448
+ return await this.buildAsync(ast, pathPrefix);
1449
+ case "subst":
1450
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1451
+ case "concat": {
1452
+ const nodes = [];
1453
+ for (const n of ast.nodes) {
1454
+ nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1455
+ }
1456
+ return { _kind: "concat-placeholder", nodes };
1457
+ }
1458
+ case "include":
1459
+ return { kind: "scalar", raw: "null", valueType: "null" };
1054
1460
  }
1055
- return { kind: "object", fields: merged };
1056
1461
  }
1057
- if (resolved.some((v) => v.kind === "array")) {
1058
- const items = [];
1059
- for (const v of resolved) {
1060
- if (v.kind === "array") items.push(...v.items);
1061
- else items.push(v);
1462
+ // ---- Relativize substitution paths for nested includes ----
1463
+ relativizeSubstPaths(val, prefixSegments) {
1464
+ if (isSubst(val)) {
1465
+ const prefixAsSegments = prefixSegments.map((text) => ({ text, line: 0, col: 0 }));
1466
+ val.segments = [...prefixAsSegments, ...val.segments];
1467
+ val.prefixLen += prefixSegments.length;
1468
+ return;
1062
1469
  }
1063
- return { kind: "array", items };
1064
- }
1065
- const str = resolved.map((v) => v.kind === "scalar" ? String(v.value) : JSON.stringify(v)).join("");
1066
- return { kind: "scalar", value: str };
1067
- }
1068
- function deepMergeHoconValues(base, overlay) {
1069
- const merged = new Map(base.fields);
1070
- for (const [k, v] of overlay.fields) {
1071
- const existing = merged.get(k);
1072
- if (existing?.kind === "object" && v.kind === "object") {
1073
- merged.set(k, deepMergeHoconValues(existing, v));
1074
- } else {
1075
- merged.set(k, v);
1470
+ if (isConcat(val)) {
1471
+ for (const node of val.nodes) {
1472
+ this.relativizeSubstPaths(node, prefixSegments);
1473
+ }
1474
+ return;
1076
1475
  }
1077
- }
1078
- return { kind: "object", fields: merged };
1079
- }
1080
- function resolveAppend(a, scope, root, resolving, resolvedCache, opts) {
1081
- const existing = resolveVal(a.existing, scope, root, resolving, resolvedCache, opts) ?? { kind: "array", items: [] };
1082
- const elem = resolveVal(a.elem, scope, root, resolving, resolvedCache, opts);
1083
- const items = existing.kind === "array" ? [...existing.items] : [existing];
1084
- if (elem !== void 0) items.push(elem);
1085
- return { kind: "array", items };
1086
- }
1087
- function lookupPath(root, segments) {
1088
- const [head, ...tail] = segments;
1089
- if (head === void 0 || head === "") {
1090
- if (segments.length > 0) {
1091
- const val2 = root.fields.get("");
1092
- if (val2 === void 0) return void 0;
1093
- if (tail.length === 0) return val2;
1094
- if (isResObj(val2)) return lookupPath(val2, tail);
1095
- return void 0;
1476
+ if (isAppend(val)) {
1477
+ this.relativizeSubstPaths(val.existing, prefixSegments);
1478
+ this.relativizeSubstPaths(val.elem, prefixSegments);
1479
+ return;
1096
1480
  }
1097
- return void 0;
1098
- }
1099
- const val = root.fields.get(head);
1100
- if (val === void 0) return void 0;
1101
- if (tail.length === 0) return val;
1102
- if (isResObj(val)) return lookupPath(val, tail);
1103
- return void 0;
1104
- }
1105
- function parseSubstPath(raw) {
1106
- const segments = [];
1107
- let i = 0;
1108
- while (i < raw.length) {
1109
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
1110
- if (i >= raw.length) break;
1111
- if (raw[i] === '"') {
1112
- i++;
1113
- let seg = "";
1114
- while (i < raw.length && raw[i] !== '"') {
1115
- seg += raw[i];
1116
- i++;
1117
- }
1118
- if (i < raw.length) i++;
1119
- segments.push(seg);
1120
- while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
1121
- if (i < raw.length && raw[i] === ".") i++;
1122
- } else if (raw[i] === ".") {
1123
- segments.push("");
1124
- i++;
1125
- } else {
1126
- let seg = "";
1127
- while (i < raw.length && raw[i] !== ".") {
1128
- seg += raw[i];
1129
- i++;
1481
+ if (isResObj(val)) {
1482
+ this.relativizeResObj(val, prefixSegments);
1483
+ return;
1484
+ }
1485
+ const hv = val;
1486
+ if (hv.kind === "array") {
1487
+ for (const item of hv.items) {
1488
+ this.relativizeSubstPaths(item, prefixSegments);
1130
1489
  }
1131
- segments.push(seg.trim());
1132
- if (i < raw.length && raw[i] === ".") i++;
1133
1490
  }
1134
1491
  }
1135
- return segments;
1136
- }
1137
- function isFileNotFoundError(e) {
1138
- if (!(e instanceof Error)) return false;
1139
- const code = e.code;
1140
- if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1141
- const msg = e.message.toLowerCase();
1142
- return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
1143
- }
1144
- function loadSingleInclude(candidate, opts) {
1145
- const { readFileSync: readFileSync2, includeStack = [], env } = opts;
1146
- if (includeStack.includes(candidate)) {
1147
- throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1148
- }
1149
- let content;
1150
- try {
1151
- content = readFileSync2(candidate);
1152
- } catch (e) {
1153
- if (isFileNotFoundError(e)) return void 0;
1154
- throw e;
1155
- }
1156
- if (candidate.endsWith(".properties")) {
1157
- return hoconValueToResObj(propertiesToHoconValue(content));
1158
- }
1159
- const ast = parseTokens(tokenize(content));
1160
- return buildResObj(ast, {
1161
- env,
1162
- baseDir: nodePath.dirname(candidate),
1163
- readFileSync: readFileSync2,
1164
- includeStack: [...includeStack, candidate]
1165
- });
1166
- }
1167
- function loadInclude(includePath, required, opts) {
1168
- const { baseDir, includeStack = [] } = opts;
1169
- const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
1170
- if (includeStack.includes(absPath)) {
1171
- throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
1172
- }
1173
- if (includeStack.length >= 50) {
1174
- throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
1175
- }
1176
- const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1177
- if (hasExplicitExt) {
1178
- const result = loadSingleInclude(absPath, opts);
1179
- if (result !== void 0) return result;
1180
- if (required) {
1181
- throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1492
+ relativizeResObj(obj, prefixSegments) {
1493
+ for (const val of obj.fields.values()) {
1494
+ this.relativizeSubstPaths(val, prefixSegments);
1182
1495
  }
1183
- return makeResObj();
1184
- }
1185
- const merged = makeResObj();
1186
- let foundAny = false;
1187
- const probeExts = [".properties", ".json", ".conf"];
1188
- for (const ext of probeExts) {
1189
- const obj = loadSingleInclude(`${absPath}${ext}`, opts);
1190
- if (obj !== void 0) {
1191
- deepMergeResObjInto(merged, obj);
1192
- foundAny = true;
1496
+ for (const val of obj.priorValues.values()) {
1497
+ this.relativizeSubstPaths(val, prefixSegments);
1193
1498
  }
1194
1499
  }
1195
- if (!foundAny && required) {
1196
- throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1197
- }
1198
- return merged;
1199
- }
1200
- async function buildResObjAsync(ast, opts) {
1201
- if (ast.kind !== "object") {
1202
- throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
1203
- }
1204
- const obj = makeResObj();
1205
- for (const field of ast.fields) {
1206
- await applyFieldAsync(obj, field, opts);
1207
- }
1208
- return obj;
1209
- }
1210
- async function applyFieldAsync(obj, field, opts) {
1211
- if (field.key.length === 0 && field.value.kind === "include") {
1212
- const included = await loadIncludeAsync(field.value.path, field.value.required, opts);
1213
- deepMergeResObjInto(obj, included);
1214
- return;
1215
- }
1216
- const [head, ...tail] = field.key;
1217
- if (!head) return;
1218
- if (tail.length > 0) {
1219
- const syntheticAst = {
1220
- kind: "object",
1221
- fields: [{ key: tail, value: field.value, append: field.append, pos: field.pos }],
1222
- pos: field.pos
1223
- };
1224
- await applyFieldAsync(obj, { key: [head], value: syntheticAst, append: false, pos: field.pos }, opts);
1225
- return;
1226
- }
1227
- if (field.append) {
1228
- const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1229
- obj.priorValues.set(head, existing2);
1230
- obj.fields.set(head, {
1231
- _kind: "append-placeholder",
1232
- existing: existing2,
1233
- elem: await astToResolverValueAsync(field.value, opts)
1234
- });
1235
- return;
1236
- }
1237
- const existing = obj.fields.get(head);
1238
- const newVal = await astToResolverValueAsync(field.value, opts);
1239
- if (existing !== void 0) {
1240
- obj.priorValues.set(head, existing);
1241
- }
1242
- if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1243
- deepMergeResObjInto(existing, newVal);
1244
- return;
1245
- }
1246
- obj.fields.set(head, newVal);
1247
- }
1248
- async function astToResolverValueAsync(ast, opts) {
1249
- switch (ast.kind) {
1250
- case "scalar": {
1251
- const sv = { kind: "scalar", value: ast.value };
1252
- if (ast._separator) separatorValues.add(sv);
1253
- return sv;
1254
- }
1255
- case "array": {
1256
- const items = [];
1257
- for (const i of ast.items) {
1258
- items.push(await astToResolverValueAsync(i, opts));
1500
+ };
1501
+
1502
+ // src/internal/resolver/substitution-resolver.ts
1503
+ var SubstitutionResolver = class {
1504
+ constructor(root, opts) {
1505
+ this.root = root;
1506
+ this.opts = opts;
1507
+ this.resolving = /* @__PURE__ */ new Set();
1508
+ this.cache = /* @__PURE__ */ new Map();
1509
+ }
1510
+ resolve() {
1511
+ return this.resolveResObj(this.root);
1512
+ }
1513
+ resolveResObj(obj) {
1514
+ const result = /* @__PURE__ */ new Map();
1515
+ for (const [key, val] of obj.fields) {
1516
+ const resolved = this.resolveVal(val, obj);
1517
+ if (resolved !== void 0) {
1518
+ if (resolved.kind === "object") {
1519
+ const prior = obj.priorValues.get(key);
1520
+ if (prior !== void 0) {
1521
+ const priorResolved = this.resolveVal(prior, obj);
1522
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
1523
+ result.set(
1524
+ key,
1525
+ deepMergeHoconValues(
1526
+ priorResolved,
1527
+ resolved
1528
+ )
1529
+ );
1530
+ continue;
1531
+ }
1532
+ }
1533
+ }
1534
+ result.set(key, resolved);
1535
+ } else {
1536
+ const prior = obj.priorValues.get(key);
1537
+ if (prior !== void 0) {
1538
+ const priorResolved = this.resolveVal(prior, obj);
1539
+ if (priorResolved !== void 0) result.set(key, priorResolved);
1540
+ }
1541
+ }
1542
+ }
1543
+ return { kind: "object", fields: result };
1544
+ }
1545
+ resolveVal(v, scope) {
1546
+ if (isSubst(v)) return this.resolveSubst(v, scope);
1547
+ if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1548
+ if (isAppend(v)) return this.resolveAppend(v, scope);
1549
+ if (isResObj(v)) return this.resolveResObj(v);
1550
+ const hv = v;
1551
+ if (hv.kind === "array") {
1552
+ return {
1553
+ kind: "array",
1554
+ items: hv.items.map(
1555
+ (item) => this.resolveVal(item, scope) ?? { kind: "scalar", raw: "null", valueType: "null" }
1556
+ )
1557
+ };
1558
+ }
1559
+ return hv;
1560
+ }
1561
+ segmentsEqual(a, b) {
1562
+ return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1563
+ }
1564
+ resolveSubst(s, scope) {
1565
+ const key = segmentsToKey(s.segments);
1566
+ if (this.cache.has(key)) return this.cache.get(key);
1567
+ if (this.resolving.has(key)) {
1568
+ let prior;
1569
+ if (s.prefixLen > 0) {
1570
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1571
+ const parentScope = lookupResObj(
1572
+ this.root,
1573
+ s.segments.slice(0, s.segments.length - 1)
1574
+ );
1575
+ prior = parentScope?.priorValues.get(leafSeg);
1576
+ } else {
1577
+ const rootSeg = s.segments[0]?.text ?? "";
1578
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1259
1579
  }
1260
- return { kind: "array", items };
1261
- }
1262
- case "object":
1263
- return await buildResObjAsync(ast, opts);
1264
- case "subst":
1265
- return { _kind: "subst-placeholder", path: ast.path, optional: ast.optional, line: ast.pos.line, col: ast.pos.col };
1266
- case "concat": {
1267
- const nodes = [];
1268
- for (const n of ast.nodes) {
1269
- nodes.push(await astToResolverValueAsync(n, opts));
1580
+ if (prior !== void 0) {
1581
+ const saved = this.resolving;
1582
+ this.resolving = new Set(saved);
1583
+ try {
1584
+ return this.resolveVal(prior, scope);
1585
+ } finally {
1586
+ this.resolving = saved;
1587
+ }
1270
1588
  }
1271
- return { _kind: "concat-placeholder", nodes };
1589
+ if (s.optional) return void 0;
1590
+ throw new ResolveError(
1591
+ `circular substitution: ${key}`,
1592
+ key,
1593
+ s.line,
1594
+ s.col
1595
+ );
1272
1596
  }
1273
- case "include":
1274
- return { kind: "scalar", value: null };
1275
- }
1276
- }
1277
- async function loadSingleIncludeAsync(candidate, opts) {
1278
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = opts;
1279
- const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1280
- if (includeStack.includes(candidate)) {
1281
- throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1282
- }
1283
- let content;
1284
- try {
1285
- content = await read(candidate);
1286
- } catch (e) {
1287
- if (isFileNotFoundError(e)) return void 0;
1288
- throw e;
1289
- }
1290
- if (candidate.endsWith(".properties")) {
1291
- return hoconValueToResObj(propertiesToHoconValue(content));
1292
- }
1293
- const ast = parseTokens(tokenize(content));
1294
- return buildResObjAsync(ast, {
1295
- env,
1296
- baseDir: nodePath.dirname(candidate),
1297
- readFileSync: readFileSync2,
1298
- readFile,
1299
- includeStack: [...includeStack, candidate]
1300
- });
1301
- }
1302
- async function loadIncludeAsync(includePath, required, opts) {
1303
- const { baseDir, includeStack = [] } = opts;
1304
- const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
1305
- if (includeStack.includes(absPath)) {
1306
- throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
1307
- }
1308
- if (includeStack.length >= 50) {
1309
- throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
1310
- }
1311
- const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1312
- if (hasExplicitExt) {
1313
- const result = await loadSingleIncludeAsync(absPath, opts);
1314
- if (result !== void 0) return result;
1315
- if (required) {
1316
- throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1597
+ this.resolving.add(key);
1598
+ try {
1599
+ const found = lookupPath(this.root, s.segments);
1600
+ if (found !== void 0) {
1601
+ if (isSubst(found) || isConcat(found)) {
1602
+ const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1603
+ (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1604
+ );
1605
+ if (isSelfRef) {
1606
+ let prior;
1607
+ if (s.prefixLen > 0) {
1608
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1609
+ const parentScope = lookupResObj(
1610
+ this.root,
1611
+ s.segments.slice(0, s.segments.length - 1)
1612
+ );
1613
+ prior = parentScope?.priorValues.get(leafSeg);
1614
+ } else {
1615
+ const rootSeg = s.segments[0]?.text ?? "";
1616
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1617
+ }
1618
+ if (prior !== void 0) {
1619
+ const result2 = this.resolveVal(prior, scope);
1620
+ if (result2 !== void 0) this.cache.set(key, result2);
1621
+ return result2;
1622
+ }
1623
+ }
1624
+ }
1625
+ let result = this.resolveVal(found, scope);
1626
+ const effectiveLen = s.segments.length - s.prefixLen;
1627
+ if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1628
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1629
+ let prior;
1630
+ if (s.prefixLen > 0) {
1631
+ const parentScope = lookupResObj(
1632
+ this.root,
1633
+ s.segments.slice(0, s.segments.length - 1)
1634
+ );
1635
+ prior = parentScope?.priorValues.get(leafSeg);
1636
+ } else {
1637
+ prior = scope.priorValues.get(leafSeg) ?? this.root.priorValues.get(leafSeg);
1638
+ }
1639
+ if (prior !== void 0) {
1640
+ const priorResolved = this.resolveVal(prior, scope);
1641
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
1642
+ result = deepMergeHoconValues(
1643
+ priorResolved,
1644
+ result
1645
+ );
1646
+ }
1647
+ }
1648
+ }
1649
+ if (result !== void 0) this.cache.set(key, result);
1650
+ return result;
1651
+ }
1652
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1653
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1654
+ if (envVal !== void 0) {
1655
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1656
+ this.cache.set(key, result);
1657
+ return result;
1658
+ }
1659
+ if (s.optional) return void 0;
1660
+ throw new ResolveError(
1661
+ `could not resolve substitution: \${${key}}`,
1662
+ key,
1663
+ s.line,
1664
+ s.col
1665
+ );
1666
+ } finally {
1667
+ this.resolving.delete(key);
1317
1668
  }
1318
- return makeResObj();
1319
1669
  }
1320
- const merged = makeResObj();
1321
- let foundAny = false;
1322
- const probeExts = [".properties", ".json", ".conf"];
1323
- for (const ext of probeExts) {
1324
- const obj = await loadSingleIncludeAsync(`${absPath}${ext}`, opts);
1325
- if (obj !== void 0) {
1326
- deepMergeResObjInto(merged, obj);
1327
- foundAny = true;
1670
+ resolveConcat(nodes, scope) {
1671
+ const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1672
+ if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1673
+ if (resolved.length === 1) return resolved[0];
1674
+ const nonSep = resolved.filter((v) => !separatorValues.has(v));
1675
+ if (nonSep.length === 0) {
1676
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1677
+ return { kind: "scalar", raw: str, valueType: "string" };
1328
1678
  }
1679
+ const joinPair = (left, right) => {
1680
+ if (left.kind === "object" && right.kind === "object") {
1681
+ return deepMergeHoconValues(left, right);
1682
+ }
1683
+ if (left.kind === "array" && right.kind === "object") {
1684
+ const converted = numericObjectToArray(right);
1685
+ if (converted !== null) {
1686
+ return { kind: "array", items: [...left.items, ...converted] };
1687
+ }
1688
+ return { kind: "array", items: [...left.items, right] };
1689
+ }
1690
+ if (left.kind === "object" && right.kind === "array") {
1691
+ const converted = numericObjectToArray(left);
1692
+ if (converted !== null) {
1693
+ return { kind: "array", items: [...converted, ...right.items] };
1694
+ }
1695
+ return { kind: "array", items: [left, ...right.items] };
1696
+ }
1697
+ if (left.kind === "array" && right.kind === "array") {
1698
+ return { kind: "array", items: [...left.items, ...right.items] };
1699
+ }
1700
+ if (left.kind === "array") {
1701
+ return { kind: "array", items: [...left.items, right] };
1702
+ }
1703
+ if (right.kind === "array") {
1704
+ return { kind: "array", items: [left, ...right.items] };
1705
+ }
1706
+ const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1707
+ const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1708
+ return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
1709
+ };
1710
+ const [head, ...tail] = nonSep;
1711
+ const folded = tail.reduce(joinPair, head);
1712
+ if (folded.kind === "scalar") {
1713
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1714
+ return { kind: "scalar", raw: str, valueType: "string" };
1715
+ }
1716
+ return folded;
1329
1717
  }
1330
- if (!foundAny && required) {
1331
- throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1718
+ resolveAppend(a, scope) {
1719
+ const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
1720
+ const elem = this.resolveVal(a.elem, scope);
1721
+ const items = existing.kind === "array" ? [...existing.items] : [existing];
1722
+ if (elem !== void 0) items.push(elem);
1723
+ return { kind: "array", items };
1332
1724
  }
1333
- return merged;
1725
+ };
1726
+
1727
+ // src/internal/resolver/resolver.ts
1728
+ function resolve2(ast, opts) {
1729
+ const root = new StructureBuilder(opts).build(ast);
1730
+ return new SubstitutionResolver(root, opts).resolve();
1731
+ }
1732
+ async function resolveAsync(ast, opts) {
1733
+ const root = await new StructureBuilder(opts).buildAsync(ast);
1734
+ return new SubstitutionResolver(root, opts).resolve();
1334
1735
  }
1335
1736
 
1336
1737
  // src/parse.ts