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