@o3co/ts.hocon 0.1.5 → 1.1.0

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