@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.cjs CHANGED
@@ -54,6 +54,130 @@ function coerceNumber(value) {
54
54
  if (!/^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) return void 0;
55
55
  return Number(value);
56
56
  }
57
+ var DURATION_UNITS = {
58
+ ns: 1e-6,
59
+ nanosecond: 1e-6,
60
+ nanoseconds: 1e-6,
61
+ us: 1e-3,
62
+ microsecond: 1e-3,
63
+ microseconds: 1e-3,
64
+ ms: 1,
65
+ millisecond: 1,
66
+ milliseconds: 1,
67
+ s: 1e3,
68
+ second: 1e3,
69
+ seconds: 1e3,
70
+ m: 6e4,
71
+ minute: 6e4,
72
+ minutes: 6e4,
73
+ h: 36e5,
74
+ hour: 36e5,
75
+ hours: 36e5,
76
+ d: 864e5,
77
+ day: 864e5,
78
+ days: 864e5
79
+ };
80
+ var OUTPUT_DURATION_UNITS = {
81
+ ns: 1e-6,
82
+ us: 1e-3,
83
+ ms: 1,
84
+ s: 1e3,
85
+ m: 6e4,
86
+ h: 36e5,
87
+ d: 864e5
88
+ };
89
+ function parseDuration(value, outputUnit = "ms") {
90
+ const trimmed = value.trim();
91
+ let i = 0;
92
+ while (i < trimmed.length) {
93
+ const ch = trimmed[i];
94
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
95
+ i++;
96
+ }
97
+ if (i === 0) return NaN;
98
+ const num = Number(trimmed.slice(0, i));
99
+ if (Number.isNaN(num)) return NaN;
100
+ const unit = trimmed.slice(i).trim().toLowerCase();
101
+ const mult = DURATION_UNITS[unit];
102
+ if (mult === void 0) return NaN;
103
+ const ms = num * mult;
104
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
105
+ if (divisor === void 0) return NaN;
106
+ return ms / divisor;
107
+ }
108
+ var BYTE_UNITS = {
109
+ B: 1,
110
+ byte: 1,
111
+ bytes: 1,
112
+ KB: 1e3,
113
+ kilobyte: 1e3,
114
+ kilobytes: 1e3,
115
+ KiB: 1024,
116
+ kibibyte: 1024,
117
+ kibibytes: 1024,
118
+ MB: 1e6,
119
+ megabyte: 1e6,
120
+ megabytes: 1e6,
121
+ MiB: 1048576,
122
+ mebibyte: 1048576,
123
+ mebibytes: 1048576,
124
+ GB: 1e9,
125
+ gigabyte: 1e9,
126
+ gigabytes: 1e9,
127
+ GiB: 1073741824,
128
+ gibibyte: 1073741824,
129
+ gibibytes: 1073741824,
130
+ TB: 1e12,
131
+ terabyte: 1e12,
132
+ terabytes: 1e12,
133
+ TiB: 1099511627776,
134
+ tebibyte: 1099511627776,
135
+ tebibytes: 1099511627776,
136
+ // lowercase short-form aliases
137
+ b: 1,
138
+ kb: 1e3,
139
+ kib: 1024,
140
+ mb: 1e6,
141
+ mib: 1048576,
142
+ gb: 1e9,
143
+ gib: 1073741824,
144
+ tb: 1e12,
145
+ tib: 1099511627776
146
+ };
147
+ var OUTPUT_BYTE_UNITS = {
148
+ B: 1,
149
+ KB: 1e3,
150
+ KiB: 1024,
151
+ MB: 1e6,
152
+ MiB: 1048576,
153
+ GB: 1e9,
154
+ GiB: 1073741824,
155
+ TB: 1e12,
156
+ TiB: 1099511627776
157
+ };
158
+ function parseBytes(value, outputUnit = "B") {
159
+ const trimmed = value.trim();
160
+ let i = 0;
161
+ while (i < trimmed.length) {
162
+ const ch = trimmed[i];
163
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
164
+ i++;
165
+ }
166
+ if (i === 0) return NaN;
167
+ const num = Number(trimmed.slice(0, i));
168
+ if (Number.isNaN(num)) return NaN;
169
+ const unit = trimmed.slice(i).trim();
170
+ let mult = BYTE_UNITS[unit];
171
+ if (mult === void 0) {
172
+ mult = BYTE_UNITS[unit.toLowerCase()];
173
+ }
174
+ if (mult === void 0) return NaN;
175
+ const bytes = num * mult;
176
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
177
+ if (divisor === void 0) return NaN;
178
+ const result = bytes / divisor;
179
+ return outputUnit === "B" ? Math.round(result) : result;
180
+ }
57
181
 
58
182
  // src/errors.ts
59
183
  var ParseError = class extends Error {
@@ -116,6 +240,20 @@ var Config = class _Config {
116
240
  }
117
241
  throw new ConfigError(`expected boolean at ${path2}, got ${typeof v}`, path2);
118
242
  }
243
+ getDuration(path2, unit) {
244
+ const v = this.requireScalar(path2);
245
+ if (typeof v !== "string") throw new ConfigError(`expected duration string at ${path2}, got ${typeof v}`, path2);
246
+ const result = parseDuration(v, unit);
247
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v)}`, path2);
248
+ return result;
249
+ }
250
+ getBytes(path2, unit) {
251
+ const v = this.requireScalar(path2);
252
+ if (typeof v !== "string") throw new ConfigError(`expected byte size string at ${path2}, got ${typeof v}`, path2);
253
+ const result = parseBytes(v, unit);
254
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v)}`, path2);
255
+ return result;
256
+ }
119
257
  getConfig(path2) {
120
258
  const v = this.lookupNode(path2);
121
259
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
@@ -142,7 +280,7 @@ var Config = class _Config {
142
280
  return hoconToJs(this.root);
143
281
  }
144
282
  lookupNode(path2) {
145
- const segments = path2.split(".");
283
+ const segments = splitConfigPath(path2);
146
284
  let current = this.root;
147
285
  for (const seg of segments) {
148
286
  if (current.kind !== "object") return void 0;
@@ -159,6 +297,45 @@ var Config = class _Config {
159
297
  return v.value;
160
298
  }
161
299
  };
300
+ function splitConfigPath(path2) {
301
+ const segments = [];
302
+ let i = 0;
303
+ while (i < path2.length) {
304
+ if (path2[i] === '"') {
305
+ i++;
306
+ let segment = "";
307
+ let closed = false;
308
+ while (i < path2.length) {
309
+ const ch = path2[i];
310
+ if (ch === "\\" && i + 1 < path2.length) {
311
+ const next = path2[i + 1];
312
+ segment += next;
313
+ i += 2;
314
+ continue;
315
+ }
316
+ if (ch === '"') {
317
+ closed = true;
318
+ i++;
319
+ break;
320
+ }
321
+ segment += ch;
322
+ i++;
323
+ }
324
+ if (!closed) throw new ConfigError(`unterminated quoted path segment: ${path2}`, path2);
325
+ segments.push(segment);
326
+ if (i < path2.length && path2[i] === ".") i++;
327
+ } else {
328
+ const dot = path2.indexOf(".", i);
329
+ if (dot === -1) {
330
+ segments.push(path2.slice(i));
331
+ break;
332
+ }
333
+ segments.push(path2.slice(i, dot));
334
+ i = dot + 1;
335
+ }
336
+ }
337
+ return segments;
338
+ }
162
339
  function hoconToJs(v) {
163
340
  switch (v.kind) {
164
341
  case "scalar":
@@ -272,6 +449,7 @@ function tokenize(input) {
272
449
  advance();
273
450
  advance();
274
451
  let value = "";
452
+ let closed = false;
275
453
  while (pos < input.length) {
276
454
  if (peek() === '"') {
277
455
  let quoteCount = 0;
@@ -281,6 +459,7 @@ function tokenize(input) {
281
459
  }
282
460
  if (quoteCount >= 3) {
283
461
  for (let i = 0; i < quoteCount - 3; i++) value += '"';
462
+ closed = true;
284
463
  break;
285
464
  }
286
465
  for (let i = 0; i < quoteCount; i++) value += '"';
@@ -288,6 +467,9 @@ function tokenize(input) {
288
467
  }
289
468
  value += advance();
290
469
  }
470
+ if (!closed) {
471
+ throw new ParseError("unterminated triple-quoted string", sl, sc);
472
+ }
291
473
  if (value.startsWith("\n")) value = value.slice(1);
292
474
  push("triple_string", value, sl, sc, true);
293
475
  continue;
@@ -326,13 +508,19 @@ function tokenize(input) {
326
508
  value += "\f";
327
509
  break;
328
510
  case "u": {
511
+ if (pos + 4 > input.length) {
512
+ throw new ParseError("Invalid unicode escape: not enough characters", line, col);
513
+ }
329
514
  const hex = input.slice(pos, pos + 4);
515
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
516
+ throw new ParseError(`Invalid unicode escape: \\u${hex}`, line, col);
517
+ }
330
518
  value += String.fromCharCode(parseInt(hex, 16));
331
519
  for (let i = 0; i < 4; i++) advance();
332
520
  break;
333
521
  }
334
522
  default:
335
- value += esc;
523
+ throw new ParseError(`Unknown escape sequence: \\${esc}`, line, col);
336
524
  }
337
525
  } else {
338
526
  value += advance();
@@ -357,10 +545,10 @@ function tokenize(input) {
357
545
  return tokens;
358
546
  }
359
547
  function isUnquotedStart(ch) {
360
- return ch !== "" && !'{}[],:=+#\n\r "$'.includes(ch);
548
+ return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
361
549
  }
362
550
  function isUnquotedContinue(ch, nextFn) {
363
- if (ch === "" || '{}[],:=\n\r #"$'.includes(ch) || ch === " ") return false;
551
+ if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
364
552
  if (ch === "+" && nextFn() === "=") return false;
365
553
  if (ch === "/" && nextFn() === "/") return false;
366
554
  return true;
@@ -462,20 +650,46 @@ function parseTokens(tokens) {
462
650
  skip("newline");
463
651
  const t2 = peek();
464
652
  let path2;
465
- if (t2.kind === "string") {
653
+ let required = false;
654
+ if (t2.kind === "unquoted" && (t2.value === "required(" || t2.value === "required" || t2.value.startsWith("required("))) {
655
+ if (t2.value === "required") {
656
+ const next2 = tokens[pos + 1] ?? { kind: "eof", value: "" };
657
+ if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
658
+ throw new ParseError("include required must be followed by (", t2.line, t2.col);
659
+ }
660
+ }
661
+ required = true;
662
+ advance();
663
+ const innerPrefix = t2.value.startsWith("required(") ? t2.value.slice("required(".length) : "";
664
+ if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
665
+ throw new ParseError("include url(...) and classpath(...) are not supported", t2.line, t2.col);
666
+ }
667
+ const next = peek();
668
+ if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
669
+ throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
670
+ }
671
+ while (peek().kind !== "string" && peek().kind !== "eof") advance();
672
+ if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
673
+ path2 = advance().value;
674
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
675
+ } else if (t2.kind === "string") {
466
676
  path2 = advance().value;
467
677
  } else if (t2.kind === "unquoted" && (t2.value === "file(" || t2.value === "file")) {
468
678
  advance();
469
679
  while (peek().kind !== "string" && peek().kind !== "eof") advance();
470
680
  if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
471
681
  path2 = advance().value;
472
- while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof") advance();
682
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
683
+ } else if (t2.kind === "unquoted" && (t2.value === "url" || t2.value.startsWith("url("))) {
684
+ throw new ParseError("include url(...) is not supported", t2.line, t2.col);
685
+ } else if (t2.kind === "unquoted" && (t2.value === "classpath" || t2.value.startsWith("classpath("))) {
686
+ throw new ParseError("include classpath(...) is not supported", t2.line, t2.col);
473
687
  } else {
474
688
  throw new ParseError(`expected include path, got ${t2.kind}`, t2.line, t2.col);
475
689
  }
476
690
  return {
477
691
  key: [],
478
- value: { kind: "include", path: path2, pos: p },
692
+ value: { kind: "include", path: path2, required, pos: p },
479
693
  append: false,
480
694
  pos: p
481
695
  };
@@ -510,7 +724,7 @@ function parseTokens(tokens) {
510
724
  break;
511
725
  }
512
726
  if (hadSpace) {
513
- parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col } });
727
+ parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col }, _separator: true });
514
728
  }
515
729
  parts.push(node);
516
730
  }
@@ -546,13 +760,88 @@ function parseTokens(tokens) {
546
760
  const t = peek();
547
761
  if (t.kind === "lbrace") {
548
762
  advance();
549
- return parseObject(true);
763
+ const first = parseObject(true);
764
+ const allFields = first.kind === "object" ? [...first.fields] : [];
765
+ while (true) {
766
+ skip("newline");
767
+ if (peek().kind === "eof") break;
768
+ if (peek().kind === "lbrace") {
769
+ advance();
770
+ const extra = parseObject(true);
771
+ if (extra.kind === "object") allFields.push(...extra.fields);
772
+ } else {
773
+ const rest = parseObject(false);
774
+ if (rest.kind === "object") allFields.push(...rest.fields);
775
+ break;
776
+ }
777
+ }
778
+ skip("newline");
779
+ const remaining = peek();
780
+ if (remaining.kind !== "eof") {
781
+ throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
782
+ }
783
+ return { kind: "object", fields: allFields, pos: first.pos };
550
784
  }
551
785
  return parseObject(false);
552
786
  }
553
787
 
554
788
  // src/internal/resolver/resolver.ts
555
789
  var nodePath = __toESM(require("path"), 1);
790
+
791
+ // src/internal/properties/properties.ts
792
+ function parseProperties(input) {
793
+ const root = /* @__PURE__ */ Object.create(null);
794
+ for (const line of input.split("\n")) {
795
+ const trimmed = line.trim();
796
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
797
+ const sepIdx = findSeparator(trimmed);
798
+ if (sepIdx === -1) continue;
799
+ const key = trimmed.slice(0, sepIdx).trim();
800
+ const value = trimmed.slice(sepIdx + 1).trim();
801
+ if (key === "") continue;
802
+ setNested(root, key.split("."), value);
803
+ }
804
+ return root;
805
+ }
806
+ function findSeparator(line) {
807
+ for (let i = 0; i < line.length; i++) {
808
+ if (line[i] === "=" || line[i] === ":") return i;
809
+ }
810
+ return -1;
811
+ }
812
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
813
+ function setNested(obj, segments, value) {
814
+ let current = obj;
815
+ for (let i = 0; i < segments.length - 1; i++) {
816
+ const seg = segments[i];
817
+ if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
818
+ if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
819
+ current[seg] = /* @__PURE__ */ Object.create(null);
820
+ }
821
+ current = current[seg];
822
+ }
823
+ const last = segments[segments.length - 1];
824
+ if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
825
+ current[last] = value;
826
+ }
827
+ function propertiesToHoconValue(input) {
828
+ const parsed = parseProperties(input);
829
+ return recordToHoconValue(parsed);
830
+ }
831
+ function recordToHoconValue(obj) {
832
+ const fields = /* @__PURE__ */ new Map();
833
+ for (const [key, val] of Object.entries(obj)) {
834
+ if (typeof val === "string") {
835
+ fields.set(key, { kind: "scalar", value: val });
836
+ } else if (val !== null && typeof val === "object") {
837
+ fields.set(key, recordToHoconValue(val));
838
+ }
839
+ }
840
+ return { kind: "object", fields };
841
+ }
842
+
843
+ // src/internal/resolver/resolver.ts
844
+ var separatorValues = /* @__PURE__ */ new WeakSet();
556
845
  function isSubst(v) {
557
846
  return v._kind === "subst-placeholder";
558
847
  }
@@ -574,6 +863,12 @@ function resolve2(ast, opts) {
574
863
  const resolvedCache = /* @__PURE__ */ new Map();
575
864
  return resolveResObj(root, root, resolving, resolvedCache, opts);
576
865
  }
866
+ async function resolveAsync(ast, opts) {
867
+ const root = await buildResObjAsync(ast, opts);
868
+ const resolving = /* @__PURE__ */ new Set();
869
+ const resolvedCache = /* @__PURE__ */ new Map();
870
+ return resolveResObj(root, root, resolving, resolvedCache, opts);
871
+ }
577
872
  function buildResObj(ast, opts) {
578
873
  if (ast.kind !== "object") {
579
874
  throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
@@ -586,7 +881,7 @@ function buildResObj(ast, opts) {
586
881
  }
587
882
  function applyField(obj, field, opts) {
588
883
  if (field.key.length === 0 && field.value.kind === "include") {
589
- const included = loadInclude(field.value.path, opts);
884
+ const included = loadInclude(field.value.path, field.value.required, opts);
590
885
  deepMergeResObjInto(obj, included);
591
886
  return;
592
887
  }
@@ -624,8 +919,11 @@ function applyField(obj, field, opts) {
624
919
  }
625
920
  function astToResolverValue(ast, opts) {
626
921
  switch (ast.kind) {
627
- case "scalar":
628
- return { kind: "scalar", value: ast.value };
922
+ case "scalar": {
923
+ const sv = { kind: "scalar", value: ast.value };
924
+ if (ast._separator) separatorValues.add(sv);
925
+ return sv;
926
+ }
629
927
  case "array":
630
928
  return { kind: "array", items: ast.items.map((i) => astToResolverValue(i, opts)) };
631
929
  case "object": {
@@ -640,6 +938,18 @@ function astToResolverValue(ast, opts) {
640
938
  return { kind: "scalar", value: null };
641
939
  }
642
940
  }
941
+ function hoconValueToResObj(hv) {
942
+ const obj = makeResObj();
943
+ if (hv.kind !== "object") return obj;
944
+ for (const [key, val] of hv.fields) {
945
+ if (val.kind === "object") {
946
+ obj.fields.set(key, hoconValueToResObj(val));
947
+ } else {
948
+ obj.fields.set(key, val);
949
+ }
950
+ }
951
+ return obj;
952
+ }
643
953
  function deepMergeResObjInto(dst, src) {
644
954
  for (const [k, srcVal] of src.fields) {
645
955
  const dstVal = dst.fields.get(k);
@@ -727,12 +1037,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
727
1037
  const resolved = nodes.map((n) => resolveVal(n, scope, root, resolving, resolvedCache, opts)).filter((v) => v !== void 0);
728
1038
  if (resolved.length === 0) return { kind: "scalar", value: null };
729
1039
  if (resolved.length === 1) return resolved[0];
730
- if (resolved.every((v) => v.kind === "object")) {
1040
+ const nonSep = resolved.filter((v) => !separatorValues.has(v));
1041
+ if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
731
1042
  const merged = /* @__PURE__ */ new Map();
732
- for (const v of resolved) {
1043
+ for (const v of nonSep) {
733
1044
  if (v.kind === "object") {
734
1045
  for (const [k, val] of v.fields) {
735
- merged.set(k, val);
1046
+ const existing = merged.get(k);
1047
+ if (existing?.kind === "object" && val.kind === "object") {
1048
+ merged.set(k, deepMergeHoconValues(existing, val));
1049
+ } else {
1050
+ merged.set(k, val);
1051
+ }
736
1052
  }
737
1053
  }
738
1054
  }
@@ -749,6 +1065,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
749
1065
  const str = resolved.map((v) => v.kind === "scalar" ? String(v.value) : JSON.stringify(v)).join("");
750
1066
  return { kind: "scalar", value: str };
751
1067
  }
1068
+ function deepMergeHoconValues(base, overlay) {
1069
+ const merged = new Map(base.fields);
1070
+ for (const [k, v] of overlay.fields) {
1071
+ const existing = merged.get(k);
1072
+ if (existing?.kind === "object" && v.kind === "object") {
1073
+ merged.set(k, deepMergeHoconValues(existing, v));
1074
+ } else {
1075
+ merged.set(k, v);
1076
+ }
1077
+ }
1078
+ return { kind: "object", fields: merged };
1079
+ }
752
1080
  function resolveAppend(a, scope, root, resolving, resolvedCache, opts) {
753
1081
  const existing = resolveVal(a.existing, scope, root, resolving, resolvedCache, opts) ?? { kind: "array", items: [] };
754
1082
  const elem = resolveVal(a.elem, scope, root, resolving, resolvedCache, opts);
@@ -806,35 +1134,203 @@ function parseSubstPath(raw) {
806
1134
  }
807
1135
  return segments;
808
1136
  }
809
- function loadInclude(includePath, opts) {
810
- const { baseDir, readFileSync: readFileSync2, includeStack = [], env } = opts;
1137
+ function isFileNotFoundError(e) {
1138
+ if (!(e instanceof Error)) return false;
1139
+ const code = e.code;
1140
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1141
+ const msg = e.message.toLowerCase();
1142
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
1143
+ }
1144
+ function loadSingleInclude(candidate, opts) {
1145
+ const { readFileSync: readFileSync2, includeStack = [], env } = opts;
1146
+ if (includeStack.includes(candidate)) {
1147
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1148
+ }
1149
+ let content;
1150
+ try {
1151
+ content = readFileSync2(candidate);
1152
+ } catch (e) {
1153
+ if (isFileNotFoundError(e)) return void 0;
1154
+ throw e;
1155
+ }
1156
+ if (candidate.endsWith(".properties")) {
1157
+ return hoconValueToResObj(propertiesToHoconValue(content));
1158
+ }
1159
+ const ast = parseTokens(tokenize(content));
1160
+ return buildResObj(ast, {
1161
+ env,
1162
+ baseDir: nodePath.dirname(candidate),
1163
+ readFileSync: readFileSync2,
1164
+ includeStack: [...includeStack, candidate]
1165
+ });
1166
+ }
1167
+ function loadInclude(includePath, required, opts) {
1168
+ const { baseDir, includeStack = [] } = opts;
811
1169
  const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
812
1170
  if (includeStack.includes(absPath)) {
813
1171
  throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
814
1172
  }
815
- const candidates = [absPath];
816
- if (!absPath.endsWith(".conf") && !absPath.endsWith(".json") && !absPath.endsWith(".properties")) {
817
- candidates.push(absPath + ".conf", absPath + ".json");
1173
+ if (includeStack.length >= 50) {
1174
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
818
1175
  }
819
- for (const candidate of candidates) {
820
- let content;
821
- try {
822
- content = readFileSync2(candidate);
823
- } catch {
824
- continue;
1176
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1177
+ if (hasExplicitExt) {
1178
+ const result = loadSingleInclude(absPath, opts);
1179
+ if (result !== void 0) return result;
1180
+ if (required) {
1181
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
825
1182
  }
826
- if (includeStack.includes(candidate)) {
827
- throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1183
+ return makeResObj();
1184
+ }
1185
+ const merged = makeResObj();
1186
+ let foundAny = false;
1187
+ const probeExts = [".properties", ".json", ".conf"];
1188
+ for (const ext of probeExts) {
1189
+ const obj = loadSingleInclude(`${absPath}${ext}`, opts);
1190
+ if (obj !== void 0) {
1191
+ deepMergeResObjInto(merged, obj);
1192
+ foundAny = true;
828
1193
  }
829
- const ast = parseTokens(tokenize(content));
830
- return buildResObj(ast, {
831
- env,
832
- baseDir: nodePath.dirname(candidate),
833
- readFileSync: readFileSync2,
834
- includeStack: [...includeStack, candidate]
1194
+ }
1195
+ if (!foundAny && required) {
1196
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1197
+ }
1198
+ return merged;
1199
+ }
1200
+ async function buildResObjAsync(ast, opts) {
1201
+ if (ast.kind !== "object") {
1202
+ throw new ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col);
1203
+ }
1204
+ const obj = makeResObj();
1205
+ for (const field of ast.fields) {
1206
+ await applyFieldAsync(obj, field, opts);
1207
+ }
1208
+ return obj;
1209
+ }
1210
+ async function applyFieldAsync(obj, field, opts) {
1211
+ if (field.key.length === 0 && field.value.kind === "include") {
1212
+ const included = await loadIncludeAsync(field.value.path, field.value.required, opts);
1213
+ deepMergeResObjInto(obj, included);
1214
+ return;
1215
+ }
1216
+ const [head, ...tail] = field.key;
1217
+ if (!head) return;
1218
+ if (tail.length > 0) {
1219
+ const syntheticAst = {
1220
+ kind: "object",
1221
+ fields: [{ key: tail, value: field.value, append: field.append, pos: field.pos }],
1222
+ pos: field.pos
1223
+ };
1224
+ await applyFieldAsync(obj, { key: [head], value: syntheticAst, append: false, pos: field.pos }, opts);
1225
+ return;
1226
+ }
1227
+ if (field.append) {
1228
+ const existing2 = obj.fields.get(head) ?? { kind: "array", items: [] };
1229
+ obj.priorValues.set(head, existing2);
1230
+ obj.fields.set(head, {
1231
+ _kind: "append-placeholder",
1232
+ existing: existing2,
1233
+ elem: await astToResolverValueAsync(field.value, opts)
835
1234
  });
1235
+ return;
1236
+ }
1237
+ const existing = obj.fields.get(head);
1238
+ const newVal = await astToResolverValueAsync(field.value, opts);
1239
+ if (existing !== void 0) {
1240
+ obj.priorValues.set(head, existing);
1241
+ }
1242
+ if (existing !== void 0 && isResObj(existing) && isResObj(newVal)) {
1243
+ deepMergeResObjInto(existing, newVal);
1244
+ return;
1245
+ }
1246
+ obj.fields.set(head, newVal);
1247
+ }
1248
+ async function astToResolverValueAsync(ast, opts) {
1249
+ switch (ast.kind) {
1250
+ case "scalar": {
1251
+ const sv = { kind: "scalar", value: ast.value };
1252
+ if (ast._separator) separatorValues.add(sv);
1253
+ return sv;
1254
+ }
1255
+ case "array": {
1256
+ const items = [];
1257
+ for (const i of ast.items) {
1258
+ items.push(await astToResolverValueAsync(i, opts));
1259
+ }
1260
+ return { kind: "array", items };
1261
+ }
1262
+ case "object":
1263
+ return await buildResObjAsync(ast, opts);
1264
+ case "subst":
1265
+ return { _kind: "subst-placeholder", path: ast.path, optional: ast.optional, line: ast.pos.line, col: ast.pos.col };
1266
+ case "concat": {
1267
+ const nodes = [];
1268
+ for (const n of ast.nodes) {
1269
+ nodes.push(await astToResolverValueAsync(n, opts));
1270
+ }
1271
+ return { _kind: "concat-placeholder", nodes };
1272
+ }
1273
+ case "include":
1274
+ return { kind: "scalar", value: null };
1275
+ }
1276
+ }
1277
+ async function loadSingleIncludeAsync(candidate, opts) {
1278
+ const { readFile, readFileSync: readFileSync2, includeStack = [], env } = opts;
1279
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1280
+ if (includeStack.includes(candidate)) {
1281
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1282
+ }
1283
+ let content;
1284
+ try {
1285
+ content = await read(candidate);
1286
+ } catch (e) {
1287
+ if (isFileNotFoundError(e)) return void 0;
1288
+ throw e;
1289
+ }
1290
+ if (candidate.endsWith(".properties")) {
1291
+ return hoconValueToResObj(propertiesToHoconValue(content));
836
1292
  }
837
- return makeResObj();
1293
+ const ast = parseTokens(tokenize(content));
1294
+ return buildResObjAsync(ast, {
1295
+ env,
1296
+ baseDir: nodePath.dirname(candidate),
1297
+ readFileSync: readFileSync2,
1298
+ readFile,
1299
+ includeStack: [...includeStack, candidate]
1300
+ });
1301
+ }
1302
+ async function loadIncludeAsync(includePath, required, opts) {
1303
+ const { baseDir, includeStack = [] } = opts;
1304
+ const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
1305
+ if (includeStack.includes(absPath)) {
1306
+ throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
1307
+ }
1308
+ if (includeStack.length >= 50) {
1309
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
1310
+ }
1311
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1312
+ if (hasExplicitExt) {
1313
+ const result = await loadSingleIncludeAsync(absPath, opts);
1314
+ if (result !== void 0) return result;
1315
+ if (required) {
1316
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1317
+ }
1318
+ return makeResObj();
1319
+ }
1320
+ const merged = makeResObj();
1321
+ let foundAny = false;
1322
+ const probeExts = [".properties", ".json", ".conf"];
1323
+ for (const ext of probeExts) {
1324
+ const obj = await loadSingleIncludeAsync(`${absPath}${ext}`, opts);
1325
+ if (obj !== void 0) {
1326
+ deepMergeResObjInto(merged, obj);
1327
+ foundAny = true;
1328
+ }
1329
+ }
1330
+ if (!foundAny && required) {
1331
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1332
+ }
1333
+ return merged;
838
1334
  }
839
1335
 
840
1336
  // src/parse.ts
@@ -851,20 +1347,28 @@ function defaultReadFileSync(filePath) {
851
1347
  async function defaultReadFile(filePath) {
852
1348
  return fs.promises.readFile(filePath, "utf-8");
853
1349
  }
854
- function parse(input, opts = {}) {
1350
+ function buildResolveContext(input, opts) {
855
1351
  const tokens = tokenize(input);
856
1352
  const ast = parseTokens(tokens);
857
1353
  const resolveOpts = {
858
1354
  env: getEnv(opts),
859
1355
  baseDir: opts.baseDir,
860
- readFileSync: opts.readFileSync ?? defaultReadFileSync
1356
+ readFileSync: opts.readFileSync ?? defaultReadFileSync,
1357
+ readFile: opts.readFile
861
1358
  };
1359
+ return { ast, resolveOpts };
1360
+ }
1361
+ function parse(input, opts = {}) {
1362
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
862
1363
  const value = resolve2(ast, resolveOpts);
863
1364
  if (value.kind !== "object") throw new Error("resolved value is not an object");
864
1365
  return new Config(value);
865
1366
  }
866
1367
  async function parseAsync(input, opts = {}) {
867
- return Promise.resolve(parse(input, opts));
1368
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
1369
+ const value = await resolveAsync(ast, resolveOpts);
1370
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
1371
+ return new Config(value);
868
1372
  }
869
1373
  function parseFile(filePath, opts = {}) {
870
1374
  if (typeof process === "undefined" && !opts.readFileSync) {
@@ -886,9 +1390,10 @@ async function parseFileAsync(filePath, opts = {}) {
886
1390
  const resolvedPath = path.resolve(filePath);
887
1391
  const readFile = opts.readFile ?? defaultReadFile;
888
1392
  const input = await readFile(resolvedPath);
889
- return parse(input, {
1393
+ return parseAsync(input, {
890
1394
  ...opts,
891
1395
  baseDir: opts.baseDir ?? path.dirname(resolvedPath),
1396
+ readFile,
892
1397
  readFileSync: opts.readFileSync ?? defaultReadFileSync
893
1398
  });
894
1399
  }