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