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