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