@o3co/ts.hocon 1.0.0 → 1.2.0

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