@o3co/ts.hocon 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 = {
@@ -176,39 +177,36 @@ var Config = class _Config {
176
177
  }
177
178
  getString(path2) {
178
179
  const v = this.requireScalar(path2);
179
- if (typeof v !== "string") throw new ConfigError(`expected string at ${path2}, got ${typeof v}`, path2);
180
- return v;
180
+ return v.raw;
181
181
  }
182
182
  getNumber(path2) {
183
183
  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);
184
+ const coerced = coerceNumber(v.raw);
185
+ if (coerced !== void 0) return coerced;
186
+ throw new ConfigError(`expected number at ${path2}, got ${v.valueType}`, path2);
190
187
  }
191
188
  getBoolean(path2) {
192
189
  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);
190
+ const coerced = coerceBoolean(v.raw);
191
+ if (coerced !== void 0) return coerced;
192
+ throw new ConfigError(`expected boolean at ${path2}, got ${v.valueType}`, path2);
199
193
  }
200
194
  getDuration(path2, unit) {
201
195
  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);
196
+ if (v.valueType !== "string" && v.valueType !== "number") {
197
+ throw new ConfigError(`expected duration at ${path2}, got ${v.valueType}`, path2);
198
+ }
199
+ const result = parseDuration(v.raw, unit);
200
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v.raw)}`, path2);
205
201
  return result;
206
202
  }
207
203
  getBytes(path2, unit) {
208
204
  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);
205
+ if (v.valueType !== "string" && v.valueType !== "number") {
206
+ throw new ConfigError(`expected byte size at ${path2}, got ${v.valueType}`, path2);
207
+ }
208
+ const result = parseBytes(v.raw, unit);
209
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
212
210
  return result;
213
211
  }
214
212
  getConfig(path2) {
@@ -251,7 +249,7 @@ var Config = class _Config {
251
249
  const v = this.lookupNode(path2);
252
250
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
253
251
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
254
- return v.value;
252
+ return v;
255
253
  }
256
254
  };
257
255
  function splitConfigPath(path2) {
@@ -293,10 +291,22 @@ function splitConfigPath(path2) {
293
291
  }
294
292
  return segments;
295
293
  }
294
+ function scalarToJs(raw, valueType) {
295
+ switch (valueType) {
296
+ case "null":
297
+ return null;
298
+ case "boolean":
299
+ return raw === "true";
300
+ case "number":
301
+ return Number(raw);
302
+ case "string":
303
+ return raw;
304
+ }
305
+ }
296
306
  function hoconToJs(v) {
297
307
  switch (v.kind) {
298
308
  case "scalar":
299
- return v.value;
309
+ return scalarToJs(v.raw, v.valueType);
300
310
  case "array":
301
311
  return v.items.map(hoconToJs);
302
312
  case "object": {
@@ -320,186 +330,201 @@ function deepMergeHocon(receiver, fallback) {
320
330
  }
321
331
 
322
332
  // 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);
359
- }
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();
333
+ var SINGLE_CHAR_TOKENS = {
334
+ "{": "lbrace",
335
+ "}": "rbrace",
336
+ "[": "lbracket",
337
+ "]": "rbracket",
338
+ ",": "comma",
339
+ ":": "colon"
340
+ };
341
+ var Lexer = class {
342
+ constructor(input) {
343
+ this.input = input;
344
+ this.pos = 0;
345
+ this.line = 1;
346
+ this.col = 1;
347
+ this.hadSpace = false;
348
+ this.tokens = [];
349
+ if (input.charCodeAt(0) === 65279) this.input = input.slice(1);
350
+ }
351
+ tokenize() {
352
+ while (this.pos < this.input.length) {
353
+ const sl = this.line, sc = this.col;
354
+ const ch = this.peek();
355
+ if (ch === " " || ch === " " || ch === "\r") {
356
+ this.advance();
357
+ this.hadSpace = true;
358
+ continue;
398
359
  }
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();
416
- }
417
- if (quoteCount >= 3) {
418
- for (let i = 0; i < quoteCount - 3; i++) value += '"';
419
- closed = true;
420
- break;
421
- }
422
- for (let i = 0; i < quoteCount; i++) value += '"';
423
- continue;
360
+ if (ch === "\n") {
361
+ this.advance();
362
+ if (this.tokens[this.tokens.length - 1]?.kind !== "newline") {
363
+ this.push("newline", "\n", sl, sc);
424
364
  }
425
- value += advance();
365
+ continue;
426
366
  }
427
- if (!closed) {
428
- throw new ParseError("unterminated triple-quoted string", sl, sc);
367
+ if (ch === "/" && this.peek(1) === "/") {
368
+ while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
369
+ this.hadSpace = true;
370
+ continue;
429
371
  }
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";
372
+ if (ch === "#") {
373
+ while (this.pos < this.input.length && this.peek() !== "\n") this.advance();
374
+ this.hadSpace = true;
375
+ continue;
376
+ }
377
+ if (ch in SINGLE_CHAR_TOKENS) {
378
+ this.advance();
379
+ this.push(SINGLE_CHAR_TOKENS[ch], ch, sl, sc);
380
+ continue;
381
+ }
382
+ if (ch === "=") {
383
+ this.advance();
384
+ this.push("equals", "=", sl, sc);
385
+ continue;
386
+ }
387
+ if (ch === "+" && this.peek(1) === "=") {
388
+ this.advance();
389
+ this.advance();
390
+ this.push("plus_equals", "+=", sl, sc);
391
+ continue;
392
+ }
393
+ if (ch === "$" && this.peek(1) === "{") {
394
+ this.advance();
395
+ this.advance();
396
+ const optional = this.peek() === "?";
397
+ if (optional) this.advance();
398
+ let path2 = "";
399
+ while (this.pos < this.input.length && this.peek() !== "}") {
400
+ if (this.peek() === "\n") throw new ParseError("unterminated substitution", sl, sc);
401
+ path2 += this.advance();
402
+ }
403
+ if (this.peek() !== "}") throw new ParseError("unterminated substitution", sl, sc);
404
+ this.advance();
405
+ this.push(optional ? "opt_subst" : "subst", path2.trim(), sl, sc);
406
+ continue;
407
+ }
408
+ if (ch === '"' && this.peek(1) === '"' && this.peek(2) === '"') {
409
+ this.advance();
410
+ this.advance();
411
+ this.advance();
412
+ let value = "";
413
+ let closed = false;
414
+ while (this.pos < this.input.length) {
415
+ if (this.peek() === '"') {
416
+ let quoteCount = 0;
417
+ while (this.pos < this.input.length && this.peek() === '"') {
418
+ quoteCount++;
419
+ this.advance();
420
+ }
421
+ if (quoteCount >= 3) {
422
+ for (let i = 0; i < quoteCount - 3; i++) value += '"';
423
+ closed = true;
466
424
  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);
425
+ }
426
+ for (let i = 0; i < quoteCount; i++) value += '"';
427
+ continue;
428
+ }
429
+ value += this.advance();
430
+ }
431
+ if (!closed) {
432
+ throw new ParseError("unterminated triple-quoted string", sl, sc);
433
+ }
434
+ if (value.startsWith("\n")) value = value.slice(1);
435
+ this.push("triple_string", value, sl, sc, true);
436
+ continue;
437
+ }
438
+ if (ch === '"') {
439
+ this.advance();
440
+ let value = "";
441
+ while (this.pos < this.input.length && this.peek() !== '"') {
442
+ if (this.peek() === "\n") throw new ParseError("unterminated string", sl, sc);
443
+ if (this.peek() === "\\") {
444
+ this.advance();
445
+ const esc = this.advance();
446
+ switch (esc) {
447
+ case "n":
448
+ value += "\n";
449
+ break;
450
+ case "t":
451
+ value += " ";
452
+ break;
453
+ case "r":
454
+ value += "\r";
455
+ break;
456
+ case '"':
457
+ value += '"';
458
+ break;
459
+ case "\\":
460
+ value += "\\";
461
+ break;
462
+ case "/":
463
+ value += "/";
464
+ break;
465
+ case "b":
466
+ value += "\b";
467
+ break;
468
+ case "f":
469
+ value += "\f";
470
+ break;
471
+ case "u": {
472
+ if (this.pos + 4 > this.input.length) {
473
+ throw new ParseError("Invalid unicode escape: not enough characters", this.line, this.col);
474
+ }
475
+ const hex = this.input.slice(this.pos, this.pos + 4);
476
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
477
+ throw new ParseError(`Invalid unicode escape: \\u${hex}`, this.line, this.col);
478
+ }
479
+ value += String.fromCharCode(parseInt(hex, 16));
480
+ for (let i = 0; i < 4; i++) this.advance();
481
+ break;
474
482
  }
475
- value += String.fromCharCode(parseInt(hex, 16));
476
- for (let i = 0; i < 4; i++) advance();
477
- break;
483
+ default:
484
+ throw new ParseError(`Unknown escape sequence: \\${esc}`, this.line, this.col);
478
485
  }
479
- default:
480
- throw new ParseError(`Unknown escape sequence: \\${esc}`, line, col);
486
+ } else {
487
+ value += this.advance();
481
488
  }
482
- } else {
483
- value += advance();
484
489
  }
490
+ if (this.peek() !== '"') throw new ParseError("unterminated string", sl, sc);
491
+ this.advance();
492
+ this.push("string", value, sl, sc, true);
493
+ continue;
485
494
  }
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();
495
+ if (isUnquotedStart(ch)) {
496
+ let value = "";
497
+ while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
498
+ value += this.advance();
499
+ }
500
+ this.push("unquoted", value.trimEnd(), sl, sc);
501
+ continue;
495
502
  }
496
- push("unquoted", value.trimEnd(), sl, sc);
497
- continue;
503
+ throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
504
+ }
505
+ this.tokens.push({ kind: "eof", value: "", line: this.line, col: this.col, isQuoted: false, precedingSpace: false });
506
+ return this.tokens;
507
+ }
508
+ peek(offset = 0) {
509
+ return this.input[this.pos + offset] ?? "";
510
+ }
511
+ advance() {
512
+ const ch = this.input[this.pos++] ?? "";
513
+ if (ch === "\n") {
514
+ this.line++;
515
+ this.col = 1;
516
+ } else {
517
+ this.col++;
498
518
  }
499
- throw new ParseError(`unexpected character: ${JSON.stringify(ch)}`, sl, sc);
519
+ return ch;
500
520
  }
501
- tokens.push({ kind: "eof", value: "", line, col, isQuoted: false, precedingSpace: false });
502
- return tokens;
521
+ push(kind, value, l, c, isQuoted = false) {
522
+ this.tokens.push({ kind, value, line: l, col: c, isQuoted, precedingSpace: this.hadSpace });
523
+ this.hadSpace = false;
524
+ }
525
+ };
526
+ function tokenize(input) {
527
+ return new Lexer(input).tokenize();
503
528
  }
504
529
  function isUnquotedStart(ch) {
505
530
  return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
@@ -512,89 +537,122 @@ function isUnquotedContinue(ch, nextFn) {
512
537
  }
513
538
 
514
539
  // 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 };
540
+ var EOF_TOKEN = { kind: "eof", value: "", line: 0, col: 0, isQuoted: false, precedingSpace: false };
541
+ var Parser = class {
542
+ constructor(tokens) {
543
+ this.tokens = tokens;
544
+ this.pos = 0;
545
+ }
546
+ parse() {
547
+ this.skip("newline");
548
+ const t = this.peek();
549
+ if (t.kind === "lbrace") {
550
+ this.advance();
551
+ const first = this.parseObject(true);
552
+ const allFields = first.kind === "object" ? [...first.fields] : [];
553
+ while (true) {
554
+ this.skip("newline");
555
+ if (this.peek().kind === "eof") break;
556
+ if (this.peek().kind === "lbrace") {
557
+ this.advance();
558
+ const extra = this.parseObject(true);
559
+ if (extra.kind === "object") allFields.push(...extra.fields);
560
+ } else {
561
+ const rest = this.parseObject(false);
562
+ if (rest.kind === "object") allFields.push(...rest.fields);
563
+ break;
564
+ }
565
+ }
566
+ this.skip("newline");
567
+ const remaining = this.peek();
568
+ if (remaining.kind !== "eof") {
569
+ throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
570
+ }
571
+ return { kind: "object", fields: allFields, pos: first.pos };
572
+ }
573
+ return this.parseObject(false);
574
+ }
575
+ peek(offset = 0) {
576
+ return this.tokens[this.pos + offset] ?? EOF_TOKEN;
519
577
  }
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 };
578
+ advance() {
579
+ const t = this.tokens[this.pos];
580
+ if (this.pos < this.tokens.length) this.pos++;
581
+ return t ?? EOF_TOKEN;
524
582
  }
525
- function skip(...kinds) {
526
- while (kinds.includes(peek().kind)) advance();
583
+ skip(...kinds) {
584
+ while (kinds.includes(this.peek().kind)) this.advance();
527
585
  }
528
- function currentPos() {
529
- const t2 = peek();
530
- return { line: t2.line, col: t2.col };
586
+ currentPos() {
587
+ const t = this.peek();
588
+ return { line: t.line, col: t.col };
531
589
  }
532
- function parseObject(expectClosingBrace) {
533
- const p = currentPos();
590
+ parseObject(expectClosingBrace) {
591
+ const p = this.currentPos();
534
592
  const fields = [];
535
593
  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");
594
+ this.skip("newline");
595
+ const t = this.peek();
596
+ if (t.kind === "eof" || t.kind === "rbrace") break;
597
+ if (t.kind === "unquoted" && t.value === "include") {
598
+ this.advance();
599
+ fields.push(this.parseInclude());
600
+ this.skip("newline");
601
+ if (this.peek().kind === "comma") this.advance();
602
+ this.skip("newline");
545
603
  continue;
546
604
  }
547
- const keyPos = currentPos();
548
- const key = parseKey();
549
- skip("newline");
605
+ const keyPos = this.currentPos();
606
+ const key = this.parseKey();
607
+ this.skip("newline");
550
608
  let append = false;
551
- const sep = peek();
552
- if (sep.kind === "equals") advance();
609
+ const sep = this.peek();
610
+ if (sep.kind === "equals") this.advance();
553
611
  else if (sep.kind === "plus_equals") {
554
- advance();
612
+ this.advance();
555
613
  append = true;
556
- } else if (sep.kind === "colon") advance();
614
+ } else if (sep.kind === "colon") this.advance();
557
615
  else if (sep.kind === "lbrace") {
558
616
  } else if (sep.kind !== "newline" && sep.kind !== "eof") {
559
617
  throw new ParseError(`unexpected token after key: ${sep.kind}`, sep.line, sep.col);
560
618
  }
561
- skip("newline");
562
- const value = parseValue();
619
+ this.skip("newline");
620
+ const value = this.parseValue();
563
621
  fields.push({ key, value, append, pos: keyPos });
564
- skip("newline");
565
- if (peek().kind === "comma") advance();
566
- skip("newline");
622
+ this.skip("newline");
623
+ if (this.peek().kind === "comma") this.advance();
624
+ this.skip("newline");
567
625
  }
568
626
  if (expectClosingBrace) {
569
- const t2 = peek();
570
- if (t2.kind !== "rbrace") throw new ParseError("expected }", t2.line, t2.col);
571
- advance();
627
+ const t = this.peek();
628
+ if (t.kind !== "rbrace") throw new ParseError("expected }", t.line, t.col);
629
+ this.advance();
572
630
  }
573
631
  return { kind: "object", fields, pos: p };
574
632
  }
575
- function parseKey() {
633
+ parseKey() {
576
634
  const segments = [];
577
635
  let trailingDot = false;
578
636
  while (true) {
579
- const t2 = peek();
580
- if (t2.kind === "string") {
581
- advance();
582
- segments.push(t2.value);
637
+ const t = this.peek();
638
+ if (t.kind === "string") {
639
+ this.advance();
640
+ segments.push(t.value);
583
641
  trailingDot = false;
584
- } else if (t2.kind === "unquoted") {
585
- advance();
586
- const parts = t2.value.split(".");
642
+ } else if (t.kind === "unquoted") {
643
+ this.advance();
644
+ const parts = t.value.split(".");
587
645
  const filtered = parts.filter((s) => s.length > 0);
588
646
  segments.push(...filtered);
589
- trailingDot = t2.value.endsWith(".");
647
+ trailingDot = t.value.endsWith(".");
590
648
  } else {
591
- if (segments.length === 0) throw new ParseError(`expected key, got ${t2.kind}`, t2.line, t2.col);
649
+ if (segments.length === 0) throw new ParseError(`expected key, got ${t.kind}`, t.line, t.col);
592
650
  break;
593
651
  }
594
652
  if (trailingDot) continue;
595
- const next = peek();
653
+ const next = this.peek();
596
654
  if (next.kind === "unquoted" && next.value === "." && !next.precedingSpace) {
597
- advance();
655
+ this.advance();
598
656
  trailingDot = true;
599
657
  continue;
600
658
  }
@@ -602,147 +660,270 @@ function parseTokens(tokens) {
602
660
  }
603
661
  return segments;
604
662
  }
605
- function parseInclude() {
606
- const p = currentPos();
607
- skip("newline");
608
- const t2 = peek();
663
+ parseInclude() {
664
+ const p = this.currentPos();
665
+ this.skip("newline");
666
+ const t = this.peek();
609
667
  let path2;
610
668
  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: "" };
669
+ let isFile = false;
670
+ if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
671
+ if (t.value === "required") {
672
+ const next2 = this.peek(1);
614
673
  if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
615
- throw new ParseError("include required must be followed by (", t2.line, t2.col);
674
+ throw new ParseError("include required must be followed by (", t.line, t.col);
616
675
  }
617
676
  }
618
677
  required = true;
619
- advance();
620
- const innerPrefix = t2.value.startsWith("required(") ? t2.value.slice("required(".length) : "";
678
+ this.advance();
679
+ const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
621
680
  if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
622
- throw new ParseError("include url(...) and classpath(...) are not supported", t2.line, t2.col);
681
+ throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
682
+ }
683
+ if (innerPrefix.startsWith("file")) {
684
+ isFile = true;
623
685
  }
624
- const next = peek();
686
+ const next = this.peek();
625
687
  if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
626
688
  throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
627
689
  }
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);
690
+ if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
691
+ isFile = true;
692
+ }
693
+ while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
694
+ if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
695
+ path2 = this.advance().value;
696
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
697
+ } else if (t.kind === "string") {
698
+ path2 = this.advance().value;
699
+ } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
700
+ isFile = true;
701
+ this.advance();
702
+ while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
703
+ if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
704
+ path2 = this.advance().value;
705
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
706
+ } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
707
+ throw new ParseError("include url(...) is not supported", t.line, t.col);
708
+ } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
709
+ throw new ParseError("include classpath(...) is not supported", t.line, t.col);
644
710
  } else {
645
- throw new ParseError(`expected include path, got ${t2.kind}`, t2.line, t2.col);
711
+ throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
646
712
  }
647
713
  return {
648
714
  key: [],
649
- value: { kind: "include", path: path2, required, pos: p },
715
+ value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
650
716
  append: false,
651
717
  pos: p
652
718
  };
653
719
  }
654
- function parseValue() {
655
- const p = currentPos();
720
+ parseValue() {
721
+ const p = this.currentPos();
656
722
  const parts = [];
657
723
  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;
724
+ const t = this.peek();
725
+ if (t.kind === "eof" || t.kind === "newline" || t.kind === "rbrace" || t.kind === "rbracket" || t.kind === "comma") break;
726
+ const hadSpace = t.precedingSpace && parts.length > 0;
661
727
  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 } };
728
+ if (t.kind === "lbrace") {
729
+ this.advance();
730
+ node = this.parseObject(true);
731
+ } else if (t.kind === "lbracket") {
732
+ this.advance();
733
+ node = this.parseArray();
734
+ } else if (t.kind === "subst" || t.kind === "opt_subst") {
735
+ this.advance();
736
+ node = { kind: "subst", path: t.value, optional: t.kind === "opt_subst", pos: { line: t.line, col: t.col } };
737
+ } else if (t.kind === "string" || t.kind === "triple_string") {
738
+ this.advance();
739
+ node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
740
+ } else if (t.kind === "unquoted") {
741
+ this.advance();
742
+ node = { kind: "scalar", raw: t.value, valueType: this.scalarValueType(t.value), pos: { line: t.line, col: t.col } };
743
+ } else if ((t.kind === "colon" || t.kind === "equals") && parts.length > 0) {
744
+ this.advance();
745
+ node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
680
746
  } else {
681
747
  break;
682
748
  }
683
749
  if (hadSpace) {
684
- parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col }, _separator: true });
750
+ parts.push({ kind: "scalar", raw: " ", valueType: "string", pos: { line: t.line, col: t.col }, _separator: true });
685
751
  }
686
752
  parts.push(node);
687
753
  }
688
- if (parts.length === 0) throw new ParseError("expected value", peek().line, peek().col);
754
+ if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
689
755
  if (parts.length === 1) return parts[0];
690
756
  return { kind: "concat", nodes: parts, pos: p };
691
757
  }
692
- function parseArray() {
693
- const p = currentPos();
758
+ parseArray() {
759
+ const p = this.currentPos();
694
760
  const items = [];
695
761
  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();
762
+ this.skip("newline");
763
+ if (this.peek().kind === "rbracket" || this.peek().kind === "eof") break;
764
+ items.push(this.parseValue());
765
+ this.skip("newline");
766
+ if (this.peek().kind === "comma") this.advance();
767
+ this.skip("newline");
768
+ }
769
+ const t = this.peek();
770
+ if (t.kind !== "rbracket") throw new ParseError("expected ]", t.line, t.col);
771
+ this.advance();
706
772
  return { kind: "array", items, pos: p };
707
773
  }
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;
774
+ scalarValueType(raw) {
775
+ if (raw === "true" || raw === "false") return "boolean";
776
+ if (raw === "null") return "null";
777
+ if (raw.length > 0 && (raw.charCodeAt(0) >= 48 && raw.charCodeAt(0) <= 57 || raw.charCodeAt(0) === 45)) {
778
+ if (DECIMAL_NUMBER_RE.test(raw)) return "number";
779
+ }
780
+ return "string";
781
+ }
782
+ };
783
+ function parseTokens(tokens) {
784
+ return new Parser(tokens).parse();
785
+ }
786
+
787
+ // src/internal/resolver/types.ts
788
+ var separatorValues = /* @__PURE__ */ new WeakSet();
789
+ function isSubst(v) {
790
+ return v._kind === "subst-placeholder";
791
+ }
792
+ function isConcat(v) {
793
+ return v._kind === "concat-placeholder";
794
+ }
795
+ function isAppend(v) {
796
+ return v._kind === "append-placeholder";
797
+ }
798
+ function isResObj(v) {
799
+ return v._kind === "res-obj";
800
+ }
801
+ function makeResObj() {
802
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
803
+ }
804
+
805
+ // src/internal/resolver/utils.ts
806
+ function parseSubstPath(raw) {
807
+ const segments = [];
808
+ let i = 0;
809
+ while (i < raw.length) {
810
+ while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
811
+ if (i >= raw.length) break;
812
+ if (raw[i] === '"') {
813
+ i++;
814
+ let seg = "";
815
+ while (i < raw.length && raw[i] !== '"') {
816
+ if (raw[i] === "\\" && i + 1 < raw.length && (raw[i + 1] === '"' || raw[i + 1] === "\\")) {
817
+ seg += raw[i + 1];
818
+ i += 2;
819
+ } else {
820
+ seg += raw[i];
821
+ i++;
822
+ }
823
+ }
824
+ if (i < raw.length) i++;
825
+ segments.push(seg);
826
+ while (i < raw.length && (raw[i] === " " || raw[i] === " ")) i++;
827
+ if (i < raw.length && raw[i] === ".") i++;
828
+ } else if (raw[i] === ".") {
829
+ segments.push("");
830
+ i++;
831
+ } else {
832
+ let seg = "";
833
+ while (i < raw.length && raw[i] !== ".") {
834
+ seg += raw[i];
835
+ i++;
733
836
  }
837
+ segments.push(seg.trim());
838
+ if (i < raw.length && raw[i] === ".") i++;
734
839
  }
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);
840
+ }
841
+ return segments;
842
+ }
843
+ function segmentsToKey(segments) {
844
+ return segments.map((s) => {
845
+ if (s === "" || /[^a-zA-Z0-9\-_]/.test(s)) {
846
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
739
847
  }
740
- return { kind: "object", fields: allFields, pos: first.pos };
848
+ return s;
849
+ }).join(".");
850
+ }
851
+ function lookupPath(root, segments) {
852
+ const [head, ...tail] = segments;
853
+ if (head === void 0 || head === "") {
854
+ if (segments.length > 0) {
855
+ const val2 = root.fields.get("");
856
+ if (val2 === void 0) return void 0;
857
+ if (tail.length === 0) return val2;
858
+ if (isResObj(val2)) return lookupPath(val2, tail);
859
+ return void 0;
860
+ }
861
+ return void 0;
741
862
  }
742
- return parseObject(false);
863
+ const val = root.fields.get(head);
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);
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);
914
+ }
915
+ }
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");
743
924
  }
744
925
 
745
- // src/internal/resolver/resolver.ts
926
+ // src/internal/resolver/include-loader.ts
746
927
  import * as nodePath from "path";
747
928
 
748
929
  // src/internal/properties/properties.ts
@@ -789,7 +970,7 @@ function recordToHoconValue(obj) {
789
970
  const fields = /* @__PURE__ */ new Map();
790
971
  for (const [key, val] of Object.entries(obj)) {
791
972
  if (typeof val === "string") {
792
- fields.set(key, { kind: "scalar", value: val });
973
+ fields.set(key, { kind: "scalar", raw: val, valueType: "string" });
793
974
  } else if (val !== null && typeof val === "object") {
794
975
  fields.set(key, recordToHoconValue(val));
795
976
  }
@@ -797,497 +978,554 @@ function recordToHoconValue(obj) {
797
978
  return { kind: "object", fields };
798
979
  }
799
980
 
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);
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;
832
1027
  }
833
- const obj = makeResObj();
834
- for (const field of ast.fields) {
835
- 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;
836
1060
  }
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)
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]
863
1082
  });
864
- return;
865
1083
  }
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;
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);
883
1089
  }
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;
1090
+ let content;
1091
+ try {
1092
+ content = await read(candidate);
1093
+ } catch (e) {
1094
+ if (isFileNotFoundError(e)) return void 0;
1095
+ throw e;
889
1096
  }
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 };
1097
+ if (candidate.endsWith(".properties")) {
1098
+ return hoconValueToResObj(propertiesToHoconValue(content));
1099
+ }
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
+ });
896
1108
  }
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);
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);
906
1126
  }
1127
+ return obj;
907
1128
  }
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);
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);
918
1136
  }
1137
+ return obj;
919
1138
  }
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);
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);
932
1144
  }
1145
+ deepMergeResObjInto(obj, included);
1146
+ return;
933
1147
  }
1148
+ const [head, ...tail] = field.key;
1149
+ if (!head) 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);
934
1180
  }
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
- }
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);
976
1186
  }
977
- const result = resolveVal(found, scope, root, resolving, resolvedCache, opts);
978
- if (result !== void 0) resolvedCache.set(s.path, result);
979
- return result;
1187
+ deepMergeResObjInto(obj, included);
1188
+ return;
980
1189
  }
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;
1190
+ const [head, ...tail] = field.key;
1191
+ if (!head) 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: parseSubstPath(ast.path), 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" };
986
1242
  }
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
1243
  }
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
- }
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));
1009
1255
  }
1256
+ return { kind: "array", items };
1010
1257
  }
1258
+ case "object":
1259
+ return await this.buildAsync(ast, pathPrefix);
1260
+ case "subst":
1261
+ return { _kind: "subst-placeholder", segments: parseSubstPath(ast.path), 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));
1266
+ }
1267
+ return { _kind: "concat-placeholder", nodes };
1268
+ }
1269
+ case "include":
1270
+ return { kind: "scalar", raw: "null", valueType: "null" };
1011
1271
  }
1012
- return { kind: "object", fields: merged };
1013
1272
  }
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);
1273
+ // ---- Relativize substitution paths for nested includes ----
1274
+ relativizeSubstPaths(val, prefixSegments) {
1275
+ if (isSubst(val)) {
1276
+ val.segments = [...prefixSegments, ...val.segments];
1277
+ val.prefixLen += prefixSegments.length;
1278
+ return;
1019
1279
  }
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);
1280
+ if (isConcat(val)) {
1281
+ for (const node of val.nodes) {
1282
+ this.relativizeSubstPaths(node, prefixSegments);
1283
+ }
1284
+ return;
1033
1285
  }
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;
1286
+ if (isAppend(val)) {
1287
+ this.relativizeSubstPaths(val.existing, prefixSegments);
1288
+ this.relativizeSubstPaths(val.elem, prefixSegments);
1289
+ return;
1053
1290
  }
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++;
1291
+ if (isResObj(val)) {
1292
+ this.relativizeResObj(val, prefixSegments);
1293
+ return;
1294
+ }
1295
+ const hv = val;
1296
+ if (hv.kind === "array") {
1297
+ for (const item of hv.items) {
1298
+ this.relativizeSubstPaths(item, prefixSegments);
1087
1299
  }
1088
- segments.push(seg.trim());
1089
- if (i < raw.length && raw[i] === ".") i++;
1090
1300
  }
1091
1301
  }
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);
1302
+ relativizeResObj(obj, prefixSegments) {
1303
+ for (const val of obj.fields.values()) {
1304
+ this.relativizeSubstPaths(val, prefixSegments);
1139
1305
  }
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;
1306
+ for (const val of obj.priorValues.values()) {
1307
+ this.relativizeSubstPaths(val, prefixSegments);
1150
1308
  }
1151
1309
  }
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));
1310
+ };
1311
+
1312
+ // src/internal/resolver/substitution-resolver.ts
1313
+ var SubstitutionResolver = class {
1314
+ constructor(root, opts) {
1315
+ this.root = root;
1316
+ this.opts = opts;
1317
+ this.resolving = /* @__PURE__ */ new Set();
1318
+ this.cache = /* @__PURE__ */ new Map();
1319
+ }
1320
+ resolve() {
1321
+ return this.resolveResObj(this.root);
1322
+ }
1323
+ resolveResObj(obj) {
1324
+ const result = /* @__PURE__ */ new Map();
1325
+ for (const [key, val] of obj.fields) {
1326
+ const resolved = this.resolveVal(val, obj);
1327
+ if (resolved !== void 0) {
1328
+ if (resolved.kind === "object") {
1329
+ const prior = obj.priorValues.get(key);
1330
+ if (prior !== void 0) {
1331
+ const priorResolved = this.resolveVal(prior, obj);
1332
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
1333
+ result.set(
1334
+ key,
1335
+ deepMergeHoconValues(
1336
+ priorResolved,
1337
+ resolved
1338
+ )
1339
+ );
1340
+ continue;
1341
+ }
1342
+ }
1343
+ }
1344
+ result.set(key, resolved);
1345
+ } else {
1346
+ const prior = obj.priorValues.get(key);
1347
+ if (prior !== void 0) {
1348
+ const priorResolved = this.resolveVal(prior, obj);
1349
+ if (priorResolved !== void 0) result.set(key, priorResolved);
1350
+ }
1216
1351
  }
1217
- return { kind: "array", items };
1218
1352
  }
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));
1353
+ return { kind: "object", fields: result };
1354
+ }
1355
+ resolveVal(v, scope) {
1356
+ if (isSubst(v)) return this.resolveSubst(v, scope);
1357
+ if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1358
+ if (isAppend(v)) return this.resolveAppend(v, scope);
1359
+ if (isResObj(v)) return this.resolveResObj(v);
1360
+ const hv = v;
1361
+ if (hv.kind === "array") {
1362
+ return {
1363
+ kind: "array",
1364
+ items: hv.items.map(
1365
+ (item) => this.resolveVal(item, scope) ?? { kind: "scalar", raw: "null", valueType: "null" }
1366
+ )
1367
+ };
1368
+ }
1369
+ return hv;
1370
+ }
1371
+ segmentsEqual(a, b) {
1372
+ return a.length === b.length && a.every((seg, i) => seg === b[i]);
1373
+ }
1374
+ resolveSubst(s, scope) {
1375
+ const key = segmentsToKey(s.segments);
1376
+ if (this.cache.has(key)) return this.cache.get(key);
1377
+ if (this.resolving.has(key)) {
1378
+ let prior;
1379
+ if (s.prefixLen > 0) {
1380
+ const leafSeg = s.segments[s.segments.length - 1] ?? "";
1381
+ const parentScope = lookupResObj(
1382
+ this.root,
1383
+ s.segments.slice(0, s.segments.length - 1)
1384
+ );
1385
+ prior = parentScope?.priorValues.get(leafSeg);
1386
+ } else {
1387
+ const rootSeg = s.segments[0] ?? "";
1388
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1389
+ }
1390
+ if (prior !== void 0) {
1391
+ const saved = this.resolving;
1392
+ this.resolving = new Set(saved);
1393
+ try {
1394
+ return this.resolveVal(prior, scope);
1395
+ } finally {
1396
+ this.resolving = saved;
1397
+ }
1227
1398
  }
1228
- return { _kind: "concat-placeholder", nodes };
1399
+ if (s.optional) return void 0;
1400
+ throw new ResolveError(
1401
+ `circular substitution: ${key}`,
1402
+ key,
1403
+ s.line,
1404
+ s.col
1405
+ );
1229
1406
  }
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);
1407
+ this.resolving.add(key);
1408
+ try {
1409
+ const found = lookupPath(this.root, s.segments);
1410
+ if (found !== void 0) {
1411
+ if (isSubst(found) || isConcat(found)) {
1412
+ const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1413
+ (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1414
+ );
1415
+ if (isSelfRef) {
1416
+ let prior;
1417
+ if (s.prefixLen > 0) {
1418
+ const leafSeg = s.segments[s.segments.length - 1] ?? "";
1419
+ const parentScope = lookupResObj(
1420
+ this.root,
1421
+ s.segments.slice(0, s.segments.length - 1)
1422
+ );
1423
+ prior = parentScope?.priorValues.get(leafSeg);
1424
+ } else {
1425
+ const rootSeg = s.segments[0] ?? "";
1426
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1427
+ }
1428
+ if (prior !== void 0) {
1429
+ const result2 = this.resolveVal(prior, scope);
1430
+ if (result2 !== void 0) this.cache.set(key, result2);
1431
+ return result2;
1432
+ }
1433
+ }
1434
+ }
1435
+ let result = this.resolveVal(found, scope);
1436
+ const effectiveLen = s.segments.length - s.prefixLen;
1437
+ if (effectiveLen === 1 && result !== void 0 && result.kind === "object") {
1438
+ const leafSeg = s.segments[s.segments.length - 1] ?? "";
1439
+ let prior;
1440
+ if (s.prefixLen > 0) {
1441
+ const parentScope = lookupResObj(
1442
+ this.root,
1443
+ s.segments.slice(0, s.segments.length - 1)
1444
+ );
1445
+ prior = parentScope?.priorValues.get(leafSeg);
1446
+ } else {
1447
+ prior = scope.priorValues.get(leafSeg) ?? this.root.priorValues.get(leafSeg);
1448
+ }
1449
+ if (prior !== void 0) {
1450
+ const priorResolved = this.resolveVal(prior, scope);
1451
+ if (priorResolved !== void 0 && priorResolved.kind === "object") {
1452
+ result = deepMergeHoconValues(
1453
+ priorResolved,
1454
+ result
1455
+ );
1456
+ }
1457
+ }
1458
+ }
1459
+ if (result !== void 0) this.cache.set(key, result);
1460
+ return result;
1461
+ }
1462
+ const envKey = s.segments.join(".");
1463
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).join(".")] : void 0);
1464
+ if (envVal !== void 0) {
1465
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1466
+ this.cache.set(key, result);
1467
+ return result;
1468
+ }
1469
+ if (s.optional) return void 0;
1470
+ throw new ResolveError(
1471
+ `could not resolve substitution: \${${key}}`,
1472
+ key,
1473
+ s.line,
1474
+ s.col
1475
+ );
1476
+ } finally {
1477
+ this.resolving.delete(key);
1274
1478
  }
1275
- return makeResObj();
1276
1479
  }
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;
1480
+ resolveConcat(nodes, scope) {
1481
+ const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1482
+ if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1483
+ if (resolved.length === 1) return resolved[0];
1484
+ const nonSep = resolved.filter((v) => !separatorValues.has(v));
1485
+ if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
1486
+ const merged = /* @__PURE__ */ new Map();
1487
+ for (const v of nonSep) {
1488
+ if (v.kind === "object") {
1489
+ for (const [k, val] of v.fields) {
1490
+ const existing = merged.get(k);
1491
+ if (existing?.kind === "object" && val.kind === "object") {
1492
+ merged.set(k, deepMergeHoconValues(existing, val));
1493
+ } else {
1494
+ merged.set(k, val);
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ return { kind: "object", fields: merged };
1285
1500
  }
1501
+ if (resolved.some((v) => v.kind === "array")) {
1502
+ const items = [];
1503
+ for (const v of resolved) {
1504
+ if (v.kind === "array") items.push(...v.items);
1505
+ else items.push(v);
1506
+ }
1507
+ return { kind: "array", items };
1508
+ }
1509
+ const str = resolved.map((v) => v.kind === "scalar" ? v.raw : JSON.stringify(v)).join("");
1510
+ return { kind: "scalar", raw: str, valueType: "string" };
1511
+ }
1512
+ resolveAppend(a, scope) {
1513
+ const existing = this.resolveVal(a.existing, scope) ?? { kind: "array", items: [] };
1514
+ const elem = this.resolveVal(a.elem, scope);
1515
+ const items = existing.kind === "array" ? [...existing.items] : [existing];
1516
+ if (elem !== void 0) items.push(elem);
1517
+ return { kind: "array", items };
1286
1518
  }
1287
- if (!foundAny && required) {
1288
- throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1289
- }
1290
- return merged;
1519
+ };
1520
+
1521
+ // src/internal/resolver/resolver.ts
1522
+ function resolve2(ast, opts) {
1523
+ const root = new StructureBuilder(opts).build(ast);
1524
+ return new SubstitutionResolver(root, opts).resolve();
1525
+ }
1526
+ async function resolveAsync(ast, opts) {
1527
+ const root = await new StructureBuilder(opts).buildAsync(ast);
1528
+ return new SubstitutionResolver(root, opts).resolve();
1291
1529
  }
1292
1530
 
1293
1531
  // src/parse.ts