@o3co/ts.hocon 0.1.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,6 +11,130 @@ function coerceNumber(value) {
11
11
  if (!/^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) return void 0;
12
12
  return Number(value);
13
13
  }
14
+ var DURATION_UNITS = {
15
+ ns: 1e-6,
16
+ nanosecond: 1e-6,
17
+ nanoseconds: 1e-6,
18
+ us: 1e-3,
19
+ microsecond: 1e-3,
20
+ microseconds: 1e-3,
21
+ ms: 1,
22
+ millisecond: 1,
23
+ milliseconds: 1,
24
+ s: 1e3,
25
+ second: 1e3,
26
+ seconds: 1e3,
27
+ m: 6e4,
28
+ minute: 6e4,
29
+ minutes: 6e4,
30
+ h: 36e5,
31
+ hour: 36e5,
32
+ hours: 36e5,
33
+ d: 864e5,
34
+ day: 864e5,
35
+ days: 864e5
36
+ };
37
+ var OUTPUT_DURATION_UNITS = {
38
+ ns: 1e-6,
39
+ us: 1e-3,
40
+ ms: 1,
41
+ s: 1e3,
42
+ m: 6e4,
43
+ h: 36e5,
44
+ d: 864e5
45
+ };
46
+ function parseDuration(value, outputUnit = "ms") {
47
+ const trimmed = value.trim();
48
+ let i = 0;
49
+ while (i < trimmed.length) {
50
+ const ch = trimmed[i];
51
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
52
+ i++;
53
+ }
54
+ if (i === 0) return NaN;
55
+ const num = Number(trimmed.slice(0, i));
56
+ if (Number.isNaN(num)) return NaN;
57
+ const unit = trimmed.slice(i).trim().toLowerCase();
58
+ const mult = DURATION_UNITS[unit];
59
+ if (mult === void 0) return NaN;
60
+ const ms = num * mult;
61
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
62
+ if (divisor === void 0) return NaN;
63
+ return ms / divisor;
64
+ }
65
+ var BYTE_UNITS = {
66
+ B: 1,
67
+ byte: 1,
68
+ bytes: 1,
69
+ KB: 1e3,
70
+ kilobyte: 1e3,
71
+ kilobytes: 1e3,
72
+ KiB: 1024,
73
+ kibibyte: 1024,
74
+ kibibytes: 1024,
75
+ MB: 1e6,
76
+ megabyte: 1e6,
77
+ megabytes: 1e6,
78
+ MiB: 1048576,
79
+ mebibyte: 1048576,
80
+ mebibytes: 1048576,
81
+ GB: 1e9,
82
+ gigabyte: 1e9,
83
+ gigabytes: 1e9,
84
+ GiB: 1073741824,
85
+ gibibyte: 1073741824,
86
+ gibibytes: 1073741824,
87
+ TB: 1e12,
88
+ terabyte: 1e12,
89
+ terabytes: 1e12,
90
+ TiB: 1099511627776,
91
+ tebibyte: 1099511627776,
92
+ tebibytes: 1099511627776,
93
+ // lowercase short-form aliases
94
+ b: 1,
95
+ kb: 1e3,
96
+ kib: 1024,
97
+ mb: 1e6,
98
+ mib: 1048576,
99
+ gb: 1e9,
100
+ gib: 1073741824,
101
+ tb: 1e12,
102
+ tib: 1099511627776
103
+ };
104
+ var OUTPUT_BYTE_UNITS = {
105
+ B: 1,
106
+ KB: 1e3,
107
+ KiB: 1024,
108
+ MB: 1e6,
109
+ MiB: 1048576,
110
+ GB: 1e9,
111
+ GiB: 1073741824,
112
+ TB: 1e12,
113
+ TiB: 1099511627776
114
+ };
115
+ function parseBytes(value, outputUnit = "B") {
116
+ const trimmed = value.trim();
117
+ let i = 0;
118
+ while (i < trimmed.length) {
119
+ const ch = trimmed[i];
120
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
121
+ i++;
122
+ }
123
+ if (i === 0) return NaN;
124
+ const num = Number(trimmed.slice(0, i));
125
+ if (Number.isNaN(num)) return NaN;
126
+ const unit = trimmed.slice(i).trim();
127
+ let mult = BYTE_UNITS[unit];
128
+ if (mult === void 0) {
129
+ mult = BYTE_UNITS[unit.toLowerCase()];
130
+ }
131
+ if (mult === void 0) return NaN;
132
+ const bytes = num * mult;
133
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
134
+ if (divisor === void 0) return NaN;
135
+ const result = bytes / divisor;
136
+ return outputUnit === "B" ? Math.round(result) : result;
137
+ }
14
138
 
15
139
  // src/errors.ts
16
140
  var ParseError = class extends Error {
@@ -73,6 +197,20 @@ var Config = class _Config {
73
197
  }
74
198
  throw new ConfigError(`expected boolean at ${path2}, got ${typeof v}`, path2);
75
199
  }
200
+ getDuration(path2, unit) {
201
+ const v = this.requireScalar(path2);
202
+ if (typeof v !== "string") throw new ConfigError(`expected duration string at ${path2}, got ${typeof v}`, path2);
203
+ const result = parseDuration(v, unit);
204
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v)}`, path2);
205
+ return result;
206
+ }
207
+ getBytes(path2, unit) {
208
+ const v = this.requireScalar(path2);
209
+ if (typeof v !== "string") throw new ConfigError(`expected byte size string at ${path2}, got ${typeof v}`, path2);
210
+ const result = parseBytes(v, unit);
211
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v)}`, path2);
212
+ return result;
213
+ }
76
214
  getConfig(path2) {
77
215
  const v = this.lookupNode(path2);
78
216
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
@@ -99,7 +237,7 @@ var Config = class _Config {
99
237
  return hoconToJs(this.root);
100
238
  }
101
239
  lookupNode(path2) {
102
- const segments = path2.split(".");
240
+ const segments = splitConfigPath(path2);
103
241
  let current = this.root;
104
242
  for (const seg of segments) {
105
243
  if (current.kind !== "object") return void 0;
@@ -116,6 +254,45 @@ var Config = class _Config {
116
254
  return v.value;
117
255
  }
118
256
  };
257
+ function splitConfigPath(path2) {
258
+ const segments = [];
259
+ let i = 0;
260
+ while (i < path2.length) {
261
+ if (path2[i] === '"') {
262
+ i++;
263
+ let segment = "";
264
+ let closed = false;
265
+ while (i < path2.length) {
266
+ const ch = path2[i];
267
+ if (ch === "\\" && i + 1 < path2.length) {
268
+ const next = path2[i + 1];
269
+ segment += next;
270
+ i += 2;
271
+ continue;
272
+ }
273
+ if (ch === '"') {
274
+ closed = true;
275
+ i++;
276
+ break;
277
+ }
278
+ segment += ch;
279
+ i++;
280
+ }
281
+ if (!closed) throw new ConfigError(`unterminated quoted path segment: ${path2}`, path2);
282
+ segments.push(segment);
283
+ if (i < path2.length && path2[i] === ".") i++;
284
+ } else {
285
+ const dot = path2.indexOf(".", i);
286
+ if (dot === -1) {
287
+ segments.push(path2.slice(i));
288
+ break;
289
+ }
290
+ segments.push(path2.slice(i, dot));
291
+ i = dot + 1;
292
+ }
293
+ }
294
+ return segments;
295
+ }
119
296
  function hoconToJs(v) {
120
297
  switch (v.kind) {
121
298
  case "scalar":
@@ -229,6 +406,7 @@ function tokenize(input) {
229
406
  advance();
230
407
  advance();
231
408
  let value = "";
409
+ let closed = false;
232
410
  while (pos < input.length) {
233
411
  if (peek() === '"') {
234
412
  let quoteCount = 0;
@@ -238,6 +416,7 @@ function tokenize(input) {
238
416
  }
239
417
  if (quoteCount >= 3) {
240
418
  for (let i = 0; i < quoteCount - 3; i++) value += '"';
419
+ closed = true;
241
420
  break;
242
421
  }
243
422
  for (let i = 0; i < quoteCount; i++) value += '"';
@@ -245,6 +424,9 @@ function tokenize(input) {
245
424
  }
246
425
  value += advance();
247
426
  }
427
+ if (!closed) {
428
+ throw new ParseError("unterminated triple-quoted string", sl, sc);
429
+ }
248
430
  if (value.startsWith("\n")) value = value.slice(1);
249
431
  push("triple_string", value, sl, sc, true);
250
432
  continue;
@@ -283,13 +465,19 @@ function tokenize(input) {
283
465
  value += "\f";
284
466
  break;
285
467
  case "u": {
468
+ if (pos + 4 > input.length) {
469
+ throw new ParseError("Invalid unicode escape: not enough characters", line, col);
470
+ }
286
471
  const hex = input.slice(pos, pos + 4);
472
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
473
+ throw new ParseError(`Invalid unicode escape: \\u${hex}`, line, col);
474
+ }
287
475
  value += String.fromCharCode(parseInt(hex, 16));
288
476
  for (let i = 0; i < 4; i++) advance();
289
477
  break;
290
478
  }
291
479
  default:
292
- value += esc;
480
+ throw new ParseError(`Unknown escape sequence: \\${esc}`, line, col);
293
481
  }
294
482
  } else {
295
483
  value += advance();
@@ -314,10 +502,10 @@ function tokenize(input) {
314
502
  return tokens;
315
503
  }
316
504
  function isUnquotedStart(ch) {
317
- return ch !== "" && !'{}[],:=+#\n\r "$'.includes(ch);
505
+ return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
318
506
  }
319
507
  function isUnquotedContinue(ch, nextFn) {
320
- if (ch === "" || '{}[],:=\n\r #"$'.includes(ch) || ch === " ") return false;
508
+ if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
321
509
  if (ch === "+" && nextFn() === "=") return false;
322
510
  if (ch === "/" && nextFn() === "/") return false;
323
511
  return true;
@@ -419,20 +607,46 @@ function parseTokens(tokens) {
419
607
  skip("newline");
420
608
  const t2 = peek();
421
609
  let path2;
422
- if (t2.kind === "string") {
610
+ let required = false;
611
+ if (t2.kind === "unquoted" && (t2.value === "required(" || t2.value === "required" || t2.value.startsWith("required("))) {
612
+ if (t2.value === "required") {
613
+ const next2 = tokens[pos + 1] ?? { kind: "eof", value: "" };
614
+ if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
615
+ throw new ParseError("include required must be followed by (", t2.line, t2.col);
616
+ }
617
+ }
618
+ required = true;
619
+ advance();
620
+ const innerPrefix = t2.value.startsWith("required(") ? t2.value.slice("required(".length) : "";
621
+ if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
622
+ throw new ParseError("include url(...) and classpath(...) are not supported", t2.line, t2.col);
623
+ }
624
+ const next = peek();
625
+ if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
626
+ throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
627
+ }
628
+ while (peek().kind !== "string" && peek().kind !== "eof") advance();
629
+ if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
630
+ path2 = advance().value;
631
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
632
+ } else if (t2.kind === "string") {
423
633
  path2 = advance().value;
424
634
  } else if (t2.kind === "unquoted" && (t2.value === "file(" || t2.value === "file")) {
425
635
  advance();
426
636
  while (peek().kind !== "string" && peek().kind !== "eof") advance();
427
637
  if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
428
638
  path2 = advance().value;
429
- while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof") advance();
639
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
640
+ } else if (t2.kind === "unquoted" && (t2.value === "url" || t2.value.startsWith("url("))) {
641
+ throw new ParseError("include url(...) is not supported", t2.line, t2.col);
642
+ } else if (t2.kind === "unquoted" && (t2.value === "classpath" || t2.value.startsWith("classpath("))) {
643
+ throw new ParseError("include classpath(...) is not supported", t2.line, t2.col);
430
644
  } else {
431
645
  throw new ParseError(`expected include path, got ${t2.kind}`, t2.line, t2.col);
432
646
  }
433
647
  return {
434
648
  key: [],
435
- value: { kind: "include", path: path2, pos: p },
649
+ value: { kind: "include", path: path2, required, pos: p },
436
650
  append: false,
437
651
  pos: p
438
652
  };
@@ -467,7 +681,7 @@ function parseTokens(tokens) {
467
681
  break;
468
682
  }
469
683
  if (hadSpace) {
470
- parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col } });
684
+ parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col }, _separator: true });
471
685
  }
472
686
  parts.push(node);
473
687
  }
@@ -503,13 +717,88 @@ function parseTokens(tokens) {
503
717
  const t = peek();
504
718
  if (t.kind === "lbrace") {
505
719
  advance();
506
- return parseObject(true);
720
+ const first = parseObject(true);
721
+ const allFields = first.kind === "object" ? [...first.fields] : [];
722
+ while (true) {
723
+ skip("newline");
724
+ if (peek().kind === "eof") break;
725
+ if (peek().kind === "lbrace") {
726
+ advance();
727
+ const extra = parseObject(true);
728
+ if (extra.kind === "object") allFields.push(...extra.fields);
729
+ } else {
730
+ const rest = parseObject(false);
731
+ if (rest.kind === "object") allFields.push(...rest.fields);
732
+ break;
733
+ }
734
+ }
735
+ skip("newline");
736
+ const remaining = peek();
737
+ if (remaining.kind !== "eof") {
738
+ throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
739
+ }
740
+ return { kind: "object", fields: allFields, pos: first.pos };
507
741
  }
508
742
  return parseObject(false);
509
743
  }
510
744
 
511
745
  // src/internal/resolver/resolver.ts
512
746
  import * as nodePath from "path";
747
+
748
+ // src/internal/properties/properties.ts
749
+ function parseProperties(input) {
750
+ const root = /* @__PURE__ */ Object.create(null);
751
+ for (const line of input.split("\n")) {
752
+ const trimmed = line.trim();
753
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
754
+ const sepIdx = findSeparator(trimmed);
755
+ if (sepIdx === -1) continue;
756
+ const key = trimmed.slice(0, sepIdx).trim();
757
+ const value = trimmed.slice(sepIdx + 1).trim();
758
+ if (key === "") continue;
759
+ setNested(root, key.split("."), value);
760
+ }
761
+ return root;
762
+ }
763
+ function findSeparator(line) {
764
+ for (let i = 0; i < line.length; i++) {
765
+ if (line[i] === "=" || line[i] === ":") return i;
766
+ }
767
+ return -1;
768
+ }
769
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
770
+ function setNested(obj, segments, value) {
771
+ let current = obj;
772
+ for (let i = 0; i < segments.length - 1; i++) {
773
+ const seg = segments[i];
774
+ if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
775
+ if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
776
+ current[seg] = /* @__PURE__ */ Object.create(null);
777
+ }
778
+ current = current[seg];
779
+ }
780
+ const last = segments[segments.length - 1];
781
+ if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
782
+ current[last] = value;
783
+ }
784
+ function propertiesToHoconValue(input) {
785
+ const parsed = parseProperties(input);
786
+ return recordToHoconValue(parsed);
787
+ }
788
+ function recordToHoconValue(obj) {
789
+ const fields = /* @__PURE__ */ new Map();
790
+ for (const [key, val] of Object.entries(obj)) {
791
+ if (typeof val === "string") {
792
+ fields.set(key, { kind: "scalar", value: val });
793
+ } else if (val !== null && typeof val === "object") {
794
+ fields.set(key, recordToHoconValue(val));
795
+ }
796
+ }
797
+ return { kind: "object", fields };
798
+ }
799
+
800
+ // src/internal/resolver/resolver.ts
801
+ var separatorValues = /* @__PURE__ */ new WeakSet();
513
802
  function isSubst(v) {
514
803
  return v._kind === "subst-placeholder";
515
804
  }
@@ -531,6 +820,12 @@ function resolve2(ast, opts) {
531
820
  const resolvedCache = /* @__PURE__ */ new Map();
532
821
  return resolveResObj(root, root, resolving, resolvedCache, opts);
533
822
  }
823
+ async function resolveAsync(ast, opts) {
824
+ const root = await buildResObjAsync(ast, opts);
825
+ const resolving = /* @__PURE__ */ new Set();
826
+ const resolvedCache = /* @__PURE__ */ new Map();
827
+ return resolveResObj(root, root, resolving, resolvedCache, opts);
828
+ }
534
829
  function buildResObj(ast, opts) {
535
830
  if (ast.kind !== "object") {
536
831
  throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
@@ -543,7 +838,7 @@ function buildResObj(ast, opts) {
543
838
  }
544
839
  function applyField(obj, field, opts) {
545
840
  if (field.key.length === 0 && field.value.kind === "include") {
546
- const included = loadInclude(field.value.path, opts);
841
+ const included = loadInclude(field.value.path, field.value.required, opts);
547
842
  deepMergeResObjInto(obj, included);
548
843
  return;
549
844
  }
@@ -581,8 +876,11 @@ function applyField(obj, field, opts) {
581
876
  }
582
877
  function astToResolverValue(ast, opts) {
583
878
  switch (ast.kind) {
584
- case "scalar":
585
- return { kind: "scalar", value: ast.value };
879
+ case "scalar": {
880
+ const sv = { kind: "scalar", value: ast.value };
881
+ if (ast._separator) separatorValues.add(sv);
882
+ return sv;
883
+ }
586
884
  case "array":
587
885
  return { kind: "array", items: ast.items.map((i) => astToResolverValue(i, opts)) };
588
886
  case "object": {
@@ -597,6 +895,18 @@ function astToResolverValue(ast, opts) {
597
895
  return { kind: "scalar", value: null };
598
896
  }
599
897
  }
898
+ function hoconValueToResObj(hv) {
899
+ const obj = makeResObj();
900
+ if (hv.kind !== "object") return obj;
901
+ for (const [key, val] of hv.fields) {
902
+ if (val.kind === "object") {
903
+ obj.fields.set(key, hoconValueToResObj(val));
904
+ } else {
905
+ obj.fields.set(key, val);
906
+ }
907
+ }
908
+ return obj;
909
+ }
600
910
  function deepMergeResObjInto(dst, src) {
601
911
  for (const [k, srcVal] of src.fields) {
602
912
  const dstVal = dst.fields.get(k);
@@ -684,12 +994,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
684
994
  const resolved = nodes.map((n) => resolveVal(n, scope, root, resolving, resolvedCache, opts)).filter((v) => v !== void 0);
685
995
  if (resolved.length === 0) return { kind: "scalar", value: null };
686
996
  if (resolved.length === 1) return resolved[0];
687
- if (resolved.every((v) => v.kind === "object")) {
997
+ const nonSep = resolved.filter((v) => !separatorValues.has(v));
998
+ if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
688
999
  const merged = /* @__PURE__ */ new Map();
689
- for (const v of resolved) {
1000
+ for (const v of nonSep) {
690
1001
  if (v.kind === "object") {
691
1002
  for (const [k, val] of v.fields) {
692
- merged.set(k, val);
1003
+ const existing = merged.get(k);
1004
+ if (existing?.kind === "object" && val.kind === "object") {
1005
+ merged.set(k, deepMergeHoconValues(existing, val));
1006
+ } else {
1007
+ merged.set(k, val);
1008
+ }
693
1009
  }
694
1010
  }
695
1011
  }
@@ -706,6 +1022,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
706
1022
  const str = resolved.map((v) => v.kind === "scalar" ? String(v.value) : JSON.stringify(v)).join("");
707
1023
  return { kind: "scalar", value: str };
708
1024
  }
1025
+ function deepMergeHoconValues(base, overlay) {
1026
+ const merged = new Map(base.fields);
1027
+ for (const [k, v] of overlay.fields) {
1028
+ const existing = merged.get(k);
1029
+ if (existing?.kind === "object" && v.kind === "object") {
1030
+ merged.set(k, deepMergeHoconValues(existing, v));
1031
+ } else {
1032
+ merged.set(k, v);
1033
+ }
1034
+ }
1035
+ return { kind: "object", fields: merged };
1036
+ }
709
1037
  function resolveAppend(a, scope, root, resolving, resolvedCache, opts) {
710
1038
  const existing = resolveVal(a.existing, scope, root, resolving, resolvedCache, opts) ?? { kind: "array", items: [] };
711
1039
  const elem = resolveVal(a.elem, scope, root, resolving, resolvedCache, opts);
@@ -763,35 +1091,203 @@ function parseSubstPath(raw) {
763
1091
  }
764
1092
  return segments;
765
1093
  }
766
- function loadInclude(includePath, opts) {
767
- const { baseDir, readFileSync: readFileSync2, includeStack = [], env } = opts;
1094
+ function isFileNotFoundError(e) {
1095
+ if (!(e instanceof Error)) return false;
1096
+ const code = e.code;
1097
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1098
+ const msg = e.message.toLowerCase();
1099
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
1100
+ }
1101
+ function loadSingleInclude(candidate, opts) {
1102
+ const { readFileSync: readFileSync2, includeStack = [], env } = opts;
1103
+ if (includeStack.includes(candidate)) {
1104
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1105
+ }
1106
+ let content;
1107
+ try {
1108
+ content = readFileSync2(candidate);
1109
+ } catch (e) {
1110
+ if (isFileNotFoundError(e)) return void 0;
1111
+ throw e;
1112
+ }
1113
+ if (candidate.endsWith(".properties")) {
1114
+ return hoconValueToResObj(propertiesToHoconValue(content));
1115
+ }
1116
+ const ast = parseTokens(tokenize(content));
1117
+ return buildResObj(ast, {
1118
+ env,
1119
+ baseDir: nodePath.dirname(candidate),
1120
+ readFileSync: readFileSync2,
1121
+ includeStack: [...includeStack, candidate]
1122
+ });
1123
+ }
1124
+ function loadInclude(includePath, required, opts) {
1125
+ const { baseDir, includeStack = [] } = opts;
768
1126
  const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
769
1127
  if (includeStack.includes(absPath)) {
770
1128
  throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
771
1129
  }
772
- const candidates = [absPath];
773
- if (!absPath.endsWith(".conf") && !absPath.endsWith(".json") && !absPath.endsWith(".properties")) {
774
- candidates.push(absPath + ".conf", absPath + ".json");
1130
+ if (includeStack.length >= 50) {
1131
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
775
1132
  }
776
- for (const candidate of candidates) {
777
- let content;
778
- try {
779
- content = readFileSync2(candidate);
780
- } catch {
781
- continue;
1133
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1134
+ if (hasExplicitExt) {
1135
+ const result = loadSingleInclude(absPath, opts);
1136
+ if (result !== void 0) return result;
1137
+ if (required) {
1138
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
782
1139
  }
783
- if (includeStack.includes(candidate)) {
784
- throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1140
+ return makeResObj();
1141
+ }
1142
+ const merged = makeResObj();
1143
+ let foundAny = false;
1144
+ const probeExts = [".properties", ".json", ".conf"];
1145
+ for (const ext of probeExts) {
1146
+ const obj = loadSingleInclude(`${absPath}${ext}`, opts);
1147
+ if (obj !== void 0) {
1148
+ deepMergeResObjInto(merged, obj);
1149
+ foundAny = true;
785
1150
  }
786
- const ast = parseTokens(tokenize(content));
787
- return buildResObj(ast, {
788
- env,
789
- baseDir: nodePath.dirname(candidate),
790
- readFileSync: readFileSync2,
791
- includeStack: [...includeStack, candidate]
1151
+ }
1152
+ if (!foundAny && required) {
1153
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1154
+ }
1155
+ return merged;
1156
+ }
1157
+ async function buildResObjAsync(ast, opts) {
1158
+ if (ast.kind !== "object") {
1159
+ throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
1160
+ }
1161
+ const obj = makeResObj();
1162
+ for (const field of ast.fields) {
1163
+ await applyFieldAsync(obj, field, opts);
1164
+ }
1165
+ return obj;
1166
+ }
1167
+ async function applyFieldAsync(obj, field, opts) {
1168
+ if (field.key.length === 0 && field.value.kind === "include") {
1169
+ const included = await loadIncludeAsync(field.value.path, field.value.required, opts);
1170
+ deepMergeResObjInto(obj, included);
1171
+ return;
1172
+ }
1173
+ const [head, ...tail] = field.key;
1174
+ if (!head) return;
1175
+ if (tail.length > 0) {
1176
+ const syntheticAst = {
1177
+ kind: "object",
1178
+ fields: [{ key: tail, value: field.value, append: field.append, pos: field.pos }],
1179
+ pos: field.pos
1180
+ };
1181
+ await applyFieldAsync(obj, { key: [head], value: syntheticAst, append: false, pos: field.pos }, opts);
1182
+ return;
1183
+ }
1184
+ if (field.append) {
1185
+ const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1186
+ obj.priorValues.set(head, existing2);
1187
+ obj.fields.set(head, {
1188
+ _kind: "append-placeholder",
1189
+ existing: existing2,
1190
+ elem: await astToResolverValueAsync(field.value, opts)
792
1191
  });
1192
+ return;
1193
+ }
1194
+ const existing = obj.fields.get(head);
1195
+ const newVal = await astToResolverValueAsync(field.value, opts);
1196
+ if (existing !== void 0) {
1197
+ obj.priorValues.set(head, existing);
1198
+ }
1199
+ if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1200
+ deepMergeResObjInto(existing, newVal);
1201
+ return;
1202
+ }
1203
+ obj.fields.set(head, newVal);
1204
+ }
1205
+ async function astToResolverValueAsync(ast, opts) {
1206
+ switch (ast.kind) {
1207
+ case "scalar": {
1208
+ const sv = { kind: "scalar", value: ast.value };
1209
+ if (ast._separator) separatorValues.add(sv);
1210
+ return sv;
1211
+ }
1212
+ case "array": {
1213
+ const items = [];
1214
+ for (const i of ast.items) {
1215
+ items.push(await astToResolverValueAsync(i, opts));
1216
+ }
1217
+ return { kind: "array", items };
1218
+ }
1219
+ case "object":
1220
+ return await buildResObjAsync(ast, opts);
1221
+ case "subst":
1222
+ return { _kind: "subst-placeholder", path: ast.path, optional: ast.optional, line: ast.pos.line, col: ast.pos.col };
1223
+ case "concat": {
1224
+ const nodes = [];
1225
+ for (const n of ast.nodes) {
1226
+ nodes.push(await astToResolverValueAsync(n, opts));
1227
+ }
1228
+ return { _kind: "concat-placeholder", nodes };
1229
+ }
1230
+ case "include":
1231
+ return { kind: "scalar", value: null };
1232
+ }
1233
+ }
1234
+ async function loadSingleIncludeAsync(candidate, opts) {
1235
+ const { readFile, readFileSync: readFileSync2, includeStack = [], env } = opts;
1236
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1237
+ if (includeStack.includes(candidate)) {
1238
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1239
+ }
1240
+ let content;
1241
+ try {
1242
+ content = await read(candidate);
1243
+ } catch (e) {
1244
+ if (isFileNotFoundError(e)) return void 0;
1245
+ throw e;
1246
+ }
1247
+ if (candidate.endsWith(".properties")) {
1248
+ return hoconValueToResObj(propertiesToHoconValue(content));
793
1249
  }
794
- return makeResObj();
1250
+ const ast = parseTokens(tokenize(content));
1251
+ return buildResObjAsync(ast, {
1252
+ env,
1253
+ baseDir: nodePath.dirname(candidate),
1254
+ readFileSync: readFileSync2,
1255
+ readFile,
1256
+ includeStack: [...includeStack, candidate]
1257
+ });
1258
+ }
1259
+ async function loadIncludeAsync(includePath, required, opts) {
1260
+ const { baseDir, includeStack = [] } = opts;
1261
+ const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
1262
+ if (includeStack.includes(absPath)) {
1263
+ throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
1264
+ }
1265
+ if (includeStack.length >= 50) {
1266
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
1267
+ }
1268
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1269
+ if (hasExplicitExt) {
1270
+ const result = await loadSingleIncludeAsync(absPath, opts);
1271
+ if (result !== void 0) return result;
1272
+ if (required) {
1273
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1274
+ }
1275
+ return makeResObj();
1276
+ }
1277
+ const merged = makeResObj();
1278
+ let foundAny = false;
1279
+ const probeExts = [".properties", ".json", ".conf"];
1280
+ for (const ext of probeExts) {
1281
+ const obj = await loadSingleIncludeAsync(`${absPath}${ext}`, opts);
1282
+ if (obj !== void 0) {
1283
+ deepMergeResObjInto(merged, obj);
1284
+ foundAny = true;
1285
+ }
1286
+ }
1287
+ if (!foundAny && required) {
1288
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1289
+ }
1290
+ return merged;
795
1291
  }
796
1292
 
797
1293
  // src/parse.ts
@@ -808,20 +1304,28 @@ function defaultReadFileSync(filePath) {
808
1304
  async function defaultReadFile(filePath) {
809
1305
  return fs.promises.readFile(filePath, "utf-8");
810
1306
  }
811
- function parse(input, opts = {}) {
1307
+ function buildResolveContext(input, opts) {
812
1308
  const tokens = tokenize(input);
813
1309
  const ast = parseTokens(tokens);
814
1310
  const resolveOpts = {
815
1311
  env: getEnv(opts),
816
1312
  baseDir: opts.baseDir,
817
- readFileSync: opts.readFileSync ?? defaultReadFileSync
1313
+ readFileSync: opts.readFileSync ?? defaultReadFileSync,
1314
+ readFile: opts.readFile
818
1315
  };
1316
+ return { ast, resolveOpts };
1317
+ }
1318
+ function parse(input, opts = {}) {
1319
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
819
1320
  const value = resolve2(ast, resolveOpts);
820
1321
  if (value.kind !== "object") throw new Error("resolved value is not an object");
821
1322
  return new Config(value);
822
1323
  }
823
1324
  async function parseAsync(input, opts = {}) {
824
- return Promise.resolve(parse(input, opts));
1325
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
1326
+ const value = await resolveAsync(ast, resolveOpts);
1327
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
1328
+ return new Config(value);
825
1329
  }
826
1330
  function parseFile(filePath, opts = {}) {
827
1331
  if (typeof process === "undefined" && !opts.readFileSync) {
@@ -843,9 +1347,10 @@ async function parseFileAsync(filePath, opts = {}) {
843
1347
  const resolvedPath = path.resolve(filePath);
844
1348
  const readFile = opts.readFile ?? defaultReadFile;
845
1349
  const input = await readFile(resolvedPath);
846
- return parse(input, {
1350
+ return parseAsync(input, {
847
1351
  ...opts,
848
1352
  baseDir: opts.baseDir ?? path.dirname(resolvedPath),
1353
+ readFile,
849
1354
  readFileSync: opts.readFileSync ?? defaultReadFileSync
850
1355
  });
851
1356
  }