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