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