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