@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.
@@ -1,3 +1,6 @@
1
+ type DurationUnit = 'ns' | 'us' | 'ms' | 's' | 'm' | 'h' | 'd';
2
+ type ByteUnit = 'B' | 'KB' | 'KiB' | 'MB' | 'MiB' | 'GB' | 'GiB' | 'TB' | 'TiB';
3
+
1
4
  type HoconValue = {
2
5
  kind: 'object';
3
6
  fields: Map<string, HoconValue>;
@@ -18,6 +21,8 @@ declare class Config {
18
21
  getString(path: string): string;
19
22
  getNumber(path: string): number;
20
23
  getBoolean(path: string): boolean;
24
+ getDuration(path: string, unit?: DurationUnit): number;
25
+ getBytes(path: string, unit?: ByteUnit): number;
21
26
  getConfig(path: string): Config;
22
27
  getList(path: string): unknown[];
23
28
  has(path: string): boolean;
@@ -36,13 +41,11 @@ type ParseOptions = {
36
41
  };
37
42
  declare function parse(input: string, opts?: ParseOptions): Config;
38
43
  /**
39
- * Async wrapper around `parse()`. Returns a resolved Promise — the parsing
40
- * itself is synchronous. Use `parseFileAsync()` for async file I/O.
41
- * Note: `include` directives in string input are resolved synchronously
42
- * via `readFileSync`; `readFile` is not used by this function.
44
+ * Truly async version of `parse()`. Include directives are resolved
45
+ * asynchronously via `readFile` when provided.
43
46
  */
44
47
  declare function parseAsync(input: string, opts?: ParseOptions): Promise<Config>;
45
48
  declare function parseFile(filePath: string, opts?: ParseOptions): Config;
46
49
  declare function parseFileAsync(filePath: string, opts?: ParseOptions): Promise<Config>;
47
50
 
48
- export { Config as C, type HoconValue as H, type ParseOptions as P, parseAsync as a, parseFile as b, parseFileAsync as c, parse as p };
51
+ export { type ByteUnit as B, Config as C, type DurationUnit as D, type HoconValue as H, type ParseOptions as P, parseAsync as a, parseFile as b, parseFileAsync as c, parse as p };
@@ -1,3 +1,6 @@
1
+ type DurationUnit = 'ns' | 'us' | 'ms' | 's' | 'm' | 'h' | 'd';
2
+ type ByteUnit = 'B' | 'KB' | 'KiB' | 'MB' | 'MiB' | 'GB' | 'GiB' | 'TB' | 'TiB';
3
+
1
4
  type HoconValue = {
2
5
  kind: 'object';
3
6
  fields: Map<string, HoconValue>;
@@ -18,6 +21,8 @@ declare class Config {
18
21
  getString(path: string): string;
19
22
  getNumber(path: string): number;
20
23
  getBoolean(path: string): boolean;
24
+ getDuration(path: string, unit?: DurationUnit): number;
25
+ getBytes(path: string, unit?: ByteUnit): number;
21
26
  getConfig(path: string): Config;
22
27
  getList(path: string): unknown[];
23
28
  has(path: string): boolean;
@@ -36,13 +41,11 @@ type ParseOptions = {
36
41
  };
37
42
  declare function parse(input: string, opts?: ParseOptions): Config;
38
43
  /**
39
- * Async wrapper around `parse()`. Returns a resolved Promise — the parsing
40
- * itself is synchronous. Use `parseFileAsync()` for async file I/O.
41
- * Note: `include` directives in string input are resolved synchronously
42
- * via `readFileSync`; `readFile` is not used by this function.
44
+ * Truly async version of `parse()`. Include directives are resolved
45
+ * asynchronously via `readFile` when provided.
43
46
  */
44
47
  declare function parseAsync(input: string, opts?: ParseOptions): Promise<Config>;
45
48
  declare function parseFile(filePath: string, opts?: ParseOptions): Config;
46
49
  declare function parseFileAsync(filePath: string, opts?: ParseOptions): Promise<Config>;
47
50
 
48
- export { Config as C, type HoconValue as H, type ParseOptions as P, parseAsync as a, parseFile as b, parseFileAsync as c, parse as p };
51
+ export { type ByteUnit as B, Config as C, type DurationUnit as D, type HoconValue as H, type ParseOptions as P, parseAsync as a, parseFile as b, parseFileAsync as c, parse as p };
package/dist/zod.cjs CHANGED
@@ -50,6 +50,130 @@ function coerceNumber(value) {
50
50
  if (!/^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) return void 0;
51
51
  return Number(value);
52
52
  }
53
+ var DURATION_UNITS = {
54
+ ns: 1e-6,
55
+ nanosecond: 1e-6,
56
+ nanoseconds: 1e-6,
57
+ us: 1e-3,
58
+ microsecond: 1e-3,
59
+ microseconds: 1e-3,
60
+ ms: 1,
61
+ millisecond: 1,
62
+ milliseconds: 1,
63
+ s: 1e3,
64
+ second: 1e3,
65
+ seconds: 1e3,
66
+ m: 6e4,
67
+ minute: 6e4,
68
+ minutes: 6e4,
69
+ h: 36e5,
70
+ hour: 36e5,
71
+ hours: 36e5,
72
+ d: 864e5,
73
+ day: 864e5,
74
+ days: 864e5
75
+ };
76
+ var OUTPUT_DURATION_UNITS = {
77
+ ns: 1e-6,
78
+ us: 1e-3,
79
+ ms: 1,
80
+ s: 1e3,
81
+ m: 6e4,
82
+ h: 36e5,
83
+ d: 864e5
84
+ };
85
+ function parseDuration(value, outputUnit = "ms") {
86
+ const trimmed = value.trim();
87
+ let i = 0;
88
+ while (i < trimmed.length) {
89
+ const ch = trimmed[i];
90
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
91
+ i++;
92
+ }
93
+ if (i === 0) return NaN;
94
+ const num = Number(trimmed.slice(0, i));
95
+ if (Number.isNaN(num)) return NaN;
96
+ const unit = trimmed.slice(i).trim().toLowerCase();
97
+ const mult = DURATION_UNITS[unit];
98
+ if (mult === void 0) return NaN;
99
+ const ms = num * mult;
100
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
101
+ if (divisor === void 0) return NaN;
102
+ return ms / divisor;
103
+ }
104
+ var BYTE_UNITS = {
105
+ B: 1,
106
+ byte: 1,
107
+ bytes: 1,
108
+ KB: 1e3,
109
+ kilobyte: 1e3,
110
+ kilobytes: 1e3,
111
+ KiB: 1024,
112
+ kibibyte: 1024,
113
+ kibibytes: 1024,
114
+ MB: 1e6,
115
+ megabyte: 1e6,
116
+ megabytes: 1e6,
117
+ MiB: 1048576,
118
+ mebibyte: 1048576,
119
+ mebibytes: 1048576,
120
+ GB: 1e9,
121
+ gigabyte: 1e9,
122
+ gigabytes: 1e9,
123
+ GiB: 1073741824,
124
+ gibibyte: 1073741824,
125
+ gibibytes: 1073741824,
126
+ TB: 1e12,
127
+ terabyte: 1e12,
128
+ terabytes: 1e12,
129
+ TiB: 1099511627776,
130
+ tebibyte: 1099511627776,
131
+ tebibytes: 1099511627776,
132
+ // lowercase short-form aliases
133
+ b: 1,
134
+ kb: 1e3,
135
+ kib: 1024,
136
+ mb: 1e6,
137
+ mib: 1048576,
138
+ gb: 1e9,
139
+ gib: 1073741824,
140
+ tb: 1e12,
141
+ tib: 1099511627776
142
+ };
143
+ var OUTPUT_BYTE_UNITS = {
144
+ B: 1,
145
+ KB: 1e3,
146
+ KiB: 1024,
147
+ MB: 1e6,
148
+ MiB: 1048576,
149
+ GB: 1e9,
150
+ GiB: 1073741824,
151
+ TB: 1e12,
152
+ TiB: 1099511627776
153
+ };
154
+ function parseBytes(value, outputUnit = "B") {
155
+ const trimmed = value.trim();
156
+ let i = 0;
157
+ while (i < trimmed.length) {
158
+ const ch = trimmed[i];
159
+ if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
160
+ i++;
161
+ }
162
+ if (i === 0) return NaN;
163
+ const num = Number(trimmed.slice(0, i));
164
+ if (Number.isNaN(num)) return NaN;
165
+ const unit = trimmed.slice(i).trim();
166
+ let mult = BYTE_UNITS[unit];
167
+ if (mult === void 0) {
168
+ mult = BYTE_UNITS[unit.toLowerCase()];
169
+ }
170
+ if (mult === void 0) return NaN;
171
+ const bytes = num * mult;
172
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
173
+ if (divisor === void 0) return NaN;
174
+ const result = bytes / divisor;
175
+ return outputUnit === "B" ? Math.round(result) : result;
176
+ }
53
177
 
54
178
  // src/errors.ts
55
179
  var ParseError = class extends Error {
@@ -166,6 +290,7 @@ function tokenize(input) {
166
290
  advance();
167
291
  advance();
168
292
  let value = "";
293
+ let closed = false;
169
294
  while (pos < input.length) {
170
295
  if (peek() === '"') {
171
296
  let quoteCount = 0;
@@ -175,6 +300,7 @@ function tokenize(input) {
175
300
  }
176
301
  if (quoteCount >= 3) {
177
302
  for (let i = 0; i < quoteCount - 3; i++) value += '"';
303
+ closed = true;
178
304
  break;
179
305
  }
180
306
  for (let i = 0; i < quoteCount; i++) value += '"';
@@ -182,6 +308,9 @@ function tokenize(input) {
182
308
  }
183
309
  value += advance();
184
310
  }
311
+ if (!closed) {
312
+ throw new ParseError("unterminated triple-quoted string", sl, sc);
313
+ }
185
314
  if (value.startsWith("\n")) value = value.slice(1);
186
315
  push("triple_string", value, sl, sc, true);
187
316
  continue;
@@ -220,13 +349,19 @@ function tokenize(input) {
220
349
  value += "\f";
221
350
  break;
222
351
  case "u": {
352
+ if (pos + 4 > input.length) {
353
+ throw new ParseError("Invalid unicode escape: not enough characters", line, col);
354
+ }
223
355
  const hex = input.slice(pos, pos + 4);
356
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
357
+ throw new ParseError(`Invalid unicode escape: \\u${hex}`, line, col);
358
+ }
224
359
  value += String.fromCharCode(parseInt(hex, 16));
225
360
  for (let i = 0; i < 4; i++) advance();
226
361
  break;
227
362
  }
228
363
  default:
229
- value += esc;
364
+ throw new ParseError(`Unknown escape sequence: \\${esc}`, line, col);
230
365
  }
231
366
  } else {
232
367
  value += advance();
@@ -251,10 +386,10 @@ function tokenize(input) {
251
386
  return tokens;
252
387
  }
253
388
  function isUnquotedStart(ch) {
254
- return ch !== "" && !'{}[],:=+#\n\r "$'.includes(ch);
389
+ return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
255
390
  }
256
391
  function isUnquotedContinue(ch, nextFn) {
257
- if (ch === "" || '{}[],:=\n\r #"$'.includes(ch) || ch === " ") return false;
392
+ if (ch === "" || '{}[],:=\n\r #"$?!@*&^\\'.includes(ch) || ch === " ") return false;
258
393
  if (ch === "+" && nextFn() === "=") return false;
259
394
  if (ch === "/" && nextFn() === "/") return false;
260
395
  return true;
@@ -356,20 +491,46 @@ function parseTokens(tokens) {
356
491
  skip("newline");
357
492
  const t2 = peek();
358
493
  let path2;
359
- if (t2.kind === "string") {
494
+ let required = false;
495
+ if (t2.kind === "unquoted" && (t2.value === "required(" || t2.value === "required" || t2.value.startsWith("required("))) {
496
+ if (t2.value === "required") {
497
+ const next2 = tokens[pos + 1] ?? { kind: "eof", value: "" };
498
+ if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
499
+ throw new ParseError("include required must be followed by (", t2.line, t2.col);
500
+ }
501
+ }
502
+ required = true;
503
+ advance();
504
+ const innerPrefix = t2.value.startsWith("required(") ? t2.value.slice("required(".length) : "";
505
+ if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
506
+ throw new ParseError("include url(...) and classpath(...) are not supported", t2.line, t2.col);
507
+ }
508
+ const next = peek();
509
+ if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
510
+ throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
511
+ }
512
+ while (peek().kind !== "string" && peek().kind !== "eof") advance();
513
+ if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
514
+ path2 = advance().value;
515
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
516
+ } else if (t2.kind === "string") {
360
517
  path2 = advance().value;
361
518
  } else if (t2.kind === "unquoted" && (t2.value === "file(" || t2.value === "file")) {
362
519
  advance();
363
520
  while (peek().kind !== "string" && peek().kind !== "eof") advance();
364
521
  if (peek().kind === "eof") throw new ParseError("expected include path", t2.line, t2.col);
365
522
  path2 = advance().value;
366
- while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof") advance();
523
+ while (peek().kind !== "newline" && peek().kind !== "rbrace" && peek().kind !== "eof" && peek().kind !== "comma") advance();
524
+ } else if (t2.kind === "unquoted" && (t2.value === "url" || t2.value.startsWith("url("))) {
525
+ throw new ParseError("include url(...) is not supported", t2.line, t2.col);
526
+ } else if (t2.kind === "unquoted" && (t2.value === "classpath" || t2.value.startsWith("classpath("))) {
527
+ throw new ParseError("include classpath(...) is not supported", t2.line, t2.col);
367
528
  } else {
368
529
  throw new ParseError(`expected include path, got ${t2.kind}`, t2.line, t2.col);
369
530
  }
370
531
  return {
371
532
  key: [],
372
- value: { kind: "include", path: path2, pos: p },
533
+ value: { kind: "include", path: path2, required, pos: p },
373
534
  append: false,
374
535
  pos: p
375
536
  };
@@ -404,7 +565,7 @@ function parseTokens(tokens) {
404
565
  break;
405
566
  }
406
567
  if (hadSpace) {
407
- parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col } });
568
+ parts.push({ kind: "scalar", value: " ", pos: { line: t2.line, col: t2.col }, _separator: true });
408
569
  }
409
570
  parts.push(node);
410
571
  }
@@ -440,13 +601,88 @@ function parseTokens(tokens) {
440
601
  const t = peek();
441
602
  if (t.kind === "lbrace") {
442
603
  advance();
443
- return parseObject(true);
604
+ const first = parseObject(true);
605
+ const allFields = first.kind === "object" ? [...first.fields] : [];
606
+ while (true) {
607
+ skip("newline");
608
+ if (peek().kind === "eof") break;
609
+ if (peek().kind === "lbrace") {
610
+ advance();
611
+ const extra = parseObject(true);
612
+ if (extra.kind === "object") allFields.push(...extra.fields);
613
+ } else {
614
+ const rest = parseObject(false);
615
+ if (rest.kind === "object") allFields.push(...rest.fields);
616
+ break;
617
+ }
618
+ }
619
+ skip("newline");
620
+ const remaining = peek();
621
+ if (remaining.kind !== "eof") {
622
+ throw new ParseError(`Unexpected token '${remaining.value}' after closing brace`, remaining.line, remaining.col);
623
+ }
624
+ return { kind: "object", fields: allFields, pos: first.pos };
444
625
  }
445
626
  return parseObject(false);
446
627
  }
447
628
 
448
629
  // src/internal/resolver/resolver.ts
449
630
  var nodePath = __toESM(require("path"), 1);
631
+
632
+ // src/internal/properties/properties.ts
633
+ function parseProperties(input) {
634
+ const root = /* @__PURE__ */ Object.create(null);
635
+ for (const line of input.split("\n")) {
636
+ const trimmed = line.trim();
637
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
638
+ const sepIdx = findSeparator(trimmed);
639
+ if (sepIdx === -1) continue;
640
+ const key = trimmed.slice(0, sepIdx).trim();
641
+ const value = trimmed.slice(sepIdx + 1).trim();
642
+ if (key === "") continue;
643
+ setNested(root, key.split("."), value);
644
+ }
645
+ return root;
646
+ }
647
+ function findSeparator(line) {
648
+ for (let i = 0; i < line.length; i++) {
649
+ if (line[i] === "=" || line[i] === ":") return i;
650
+ }
651
+ return -1;
652
+ }
653
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
654
+ function setNested(obj, segments, value) {
655
+ let current = obj;
656
+ for (let i = 0; i < segments.length - 1; i++) {
657
+ const seg = segments[i];
658
+ if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
659
+ if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
660
+ current[seg] = /* @__PURE__ */ Object.create(null);
661
+ }
662
+ current = current[seg];
663
+ }
664
+ const last = segments[segments.length - 1];
665
+ if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
666
+ current[last] = value;
667
+ }
668
+ function propertiesToHoconValue(input) {
669
+ const parsed = parseProperties(input);
670
+ return recordToHoconValue(parsed);
671
+ }
672
+ function recordToHoconValue(obj) {
673
+ const fields = /* @__PURE__ */ new Map();
674
+ for (const [key, val] of Object.entries(obj)) {
675
+ if (typeof val === "string") {
676
+ fields.set(key, { kind: "scalar", value: val });
677
+ } else if (val !== null && typeof val === "object") {
678
+ fields.set(key, recordToHoconValue(val));
679
+ }
680
+ }
681
+ return { kind: "object", fields };
682
+ }
683
+
684
+ // src/internal/resolver/resolver.ts
685
+ var separatorValues = /* @__PURE__ */ new WeakSet();
450
686
  function isSubst(v) {
451
687
  return v._kind === "subst-placeholder";
452
688
  }
@@ -480,7 +716,7 @@ function buildResObj(ast, opts) {
480
716
  }
481
717
  function applyField(obj, field, opts) {
482
718
  if (field.key.length === 0 && field.value.kind === "include") {
483
- const included = loadInclude(field.value.path, opts);
719
+ const included = loadInclude(field.value.path, field.value.required, opts);
484
720
  deepMergeResObjInto(obj, included);
485
721
  return;
486
722
  }
@@ -518,8 +754,11 @@ function applyField(obj, field, opts) {
518
754
  }
519
755
  function astToResolverValue(ast, opts) {
520
756
  switch (ast.kind) {
521
- case "scalar":
522
- return { kind: "scalar", value: ast.value };
757
+ case "scalar": {
758
+ const sv = { kind: "scalar", value: ast.value };
759
+ if (ast._separator) separatorValues.add(sv);
760
+ return sv;
761
+ }
523
762
  case "array":
524
763
  return { kind: "array", items: ast.items.map((i) => astToResolverValue(i, opts)) };
525
764
  case "object": {
@@ -534,6 +773,18 @@ function astToResolverValue(ast, opts) {
534
773
  return { kind: "scalar", value: null };
535
774
  }
536
775
  }
776
+ function hoconValueToResObj(hv) {
777
+ const obj = makeResObj();
778
+ if (hv.kind !== "object") return obj;
779
+ for (const [key, val] of hv.fields) {
780
+ if (val.kind === "object") {
781
+ obj.fields.set(key, hoconValueToResObj(val));
782
+ } else {
783
+ obj.fields.set(key, val);
784
+ }
785
+ }
786
+ return obj;
787
+ }
537
788
  function deepMergeResObjInto(dst, src) {
538
789
  for (const [k, srcVal] of src.fields) {
539
790
  const dstVal = dst.fields.get(k);
@@ -621,12 +872,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
621
872
  const resolved = nodes.map((n) => resolveVal(n, scope, root, resolving, resolvedCache, opts)).filter((v) => v !== void 0);
622
873
  if (resolved.length === 0) return { kind: "scalar", value: null };
623
874
  if (resolved.length === 1) return resolved[0];
624
- if (resolved.every((v) => v.kind === "object")) {
875
+ const nonSep = resolved.filter((v) => !separatorValues.has(v));
876
+ if (nonSep.length > 0 && nonSep.every((v) => v.kind === "object")) {
625
877
  const merged = /* @__PURE__ */ new Map();
626
- for (const v of resolved) {
878
+ for (const v of nonSep) {
627
879
  if (v.kind === "object") {
628
880
  for (const [k, val] of v.fields) {
629
- merged.set(k, val);
881
+ const existing = merged.get(k);
882
+ if (existing?.kind === "object" && val.kind === "object") {
883
+ merged.set(k, deepMergeHoconValues(existing, val));
884
+ } else {
885
+ merged.set(k, val);
886
+ }
630
887
  }
631
888
  }
632
889
  }
@@ -643,6 +900,18 @@ function resolveConcat(nodes, scope, root, resolving, resolvedCache, opts) {
643
900
  const str = resolved.map((v) => v.kind === "scalar" ? String(v.value) : JSON.stringify(v)).join("");
644
901
  return { kind: "scalar", value: str };
645
902
  }
903
+ function deepMergeHoconValues(base, overlay) {
904
+ const merged = new Map(base.fields);
905
+ for (const [k, v] of overlay.fields) {
906
+ const existing = merged.get(k);
907
+ if (existing?.kind === "object" && v.kind === "object") {
908
+ merged.set(k, deepMergeHoconValues(existing, v));
909
+ } else {
910
+ merged.set(k, v);
911
+ }
912
+ }
913
+ return { kind: "object", fields: merged };
914
+ }
646
915
  function resolveAppend(a, scope, root, resolving, resolvedCache, opts) {
647
916
  const existing = resolveVal(a.existing, scope, root, resolving, resolvedCache, opts) ?? { kind: "array", items: [] };
648
917
  const elem = resolveVal(a.elem, scope, root, resolving, resolvedCache, opts);
@@ -700,35 +969,68 @@ function parseSubstPath(raw) {
700
969
  }
701
970
  return segments;
702
971
  }
703
- function loadInclude(includePath, opts) {
704
- const { baseDir, readFileSync: readFileSync2, includeStack = [], env } = opts;
972
+ function isFileNotFoundError(e) {
973
+ if (!(e instanceof Error)) return false;
974
+ const code = e.code;
975
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
976
+ const msg = e.message.toLowerCase();
977
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
978
+ }
979
+ function loadSingleInclude(candidate, opts) {
980
+ const { readFileSync: readFileSync2, includeStack = [], env } = opts;
981
+ if (includeStack.includes(candidate)) {
982
+ throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
983
+ }
984
+ let content;
985
+ try {
986
+ content = readFileSync2(candidate);
987
+ } catch (e) {
988
+ if (isFileNotFoundError(e)) return void 0;
989
+ throw e;
990
+ }
991
+ if (candidate.endsWith(".properties")) {
992
+ return hoconValueToResObj(propertiesToHoconValue(content));
993
+ }
994
+ const ast = parseTokens(tokenize(content));
995
+ return buildResObj(ast, {
996
+ env,
997
+ baseDir: nodePath.dirname(candidate),
998
+ readFileSync: readFileSync2,
999
+ includeStack: [...includeStack, candidate]
1000
+ });
1001
+ }
1002
+ function loadInclude(includePath, required, opts) {
1003
+ const { baseDir, includeStack = [] } = opts;
705
1004
  const absPath = baseDir ? nodePath.resolve(baseDir, includePath) : nodePath.resolve(includePath);
706
1005
  if (includeStack.includes(absPath)) {
707
1006
  throw new ResolveError(`circular include: ${absPath}`, absPath, 0, 0);
708
1007
  }
709
- const candidates = [absPath];
710
- if (!absPath.endsWith(".conf") && !absPath.endsWith(".json") && !absPath.endsWith(".properties")) {
711
- candidates.push(absPath + ".conf", absPath + ".json");
1008
+ if (includeStack.length >= 50) {
1009
+ throw new ResolveError(`include depth limit exceeded (max 50)`, includePath, 0, 0);
712
1010
  }
713
- for (const candidate of candidates) {
714
- let content;
715
- try {
716
- content = readFileSync2(candidate);
717
- } catch {
718
- continue;
1011
+ const hasExplicitExt = absPath.endsWith(".conf") || absPath.endsWith(".json") || absPath.endsWith(".properties");
1012
+ if (hasExplicitExt) {
1013
+ const result = loadSingleInclude(absPath, opts);
1014
+ if (result !== void 0) return result;
1015
+ if (required) {
1016
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
719
1017
  }
720
- if (includeStack.includes(candidate)) {
721
- throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1018
+ return makeResObj();
1019
+ }
1020
+ const merged = makeResObj();
1021
+ let foundAny = false;
1022
+ const probeExts = [".properties", ".json", ".conf"];
1023
+ for (const ext of probeExts) {
1024
+ const obj = loadSingleInclude(`${absPath}${ext}`, opts);
1025
+ if (obj !== void 0) {
1026
+ deepMergeResObjInto(merged, obj);
1027
+ foundAny = true;
722
1028
  }
723
- const ast = parseTokens(tokenize(content));
724
- return buildResObj(ast, {
725
- env,
726
- baseDir: nodePath.dirname(candidate),
727
- readFileSync: readFileSync2,
728
- includeStack: [...includeStack, candidate]
729
- });
730
1029
  }
731
- return makeResObj();
1030
+ if (!foundAny && required) {
1031
+ throw new ResolveError(`required include file not found: ${includePath}`, includePath, 0, 0);
1032
+ }
1033
+ return merged;
732
1034
  }
733
1035
 
734
1036
  // src/config.ts
@@ -764,6 +1066,20 @@ var Config = class _Config {
764
1066
  }
765
1067
  throw new ConfigError(`expected boolean at ${path2}, got ${typeof v}`, path2);
766
1068
  }
1069
+ getDuration(path2, unit) {
1070
+ const v = this.requireScalar(path2);
1071
+ if (typeof v !== "string") throw new ConfigError(`expected duration string at ${path2}, got ${typeof v}`, path2);
1072
+ const result = parseDuration(v, unit);
1073
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v)}`, path2);
1074
+ return result;
1075
+ }
1076
+ getBytes(path2, unit) {
1077
+ const v = this.requireScalar(path2);
1078
+ if (typeof v !== "string") throw new ConfigError(`expected byte size string at ${path2}, got ${typeof v}`, path2);
1079
+ const result = parseBytes(v, unit);
1080
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v)}`, path2);
1081
+ return result;
1082
+ }
767
1083
  getConfig(path2) {
768
1084
  const v = this.lookupNode(path2);
769
1085
  if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
@@ -790,7 +1106,7 @@ var Config = class _Config {
790
1106
  return hoconToJs(this.root);
791
1107
  }
792
1108
  lookupNode(path2) {
793
- const segments = path2.split(".");
1109
+ const segments = splitConfigPath(path2);
794
1110
  let current = this.root;
795
1111
  for (const seg of segments) {
796
1112
  if (current.kind !== "object") return void 0;
@@ -807,6 +1123,45 @@ var Config = class _Config {
807
1123
  return v.value;
808
1124
  }
809
1125
  };
1126
+ function splitConfigPath(path2) {
1127
+ const segments = [];
1128
+ let i = 0;
1129
+ while (i < path2.length) {
1130
+ if (path2[i] === '"') {
1131
+ i++;
1132
+ let segment = "";
1133
+ let closed = false;
1134
+ while (i < path2.length) {
1135
+ const ch = path2[i];
1136
+ if (ch === "\\" && i + 1 < path2.length) {
1137
+ const next = path2[i + 1];
1138
+ segment += next;
1139
+ i += 2;
1140
+ continue;
1141
+ }
1142
+ if (ch === '"') {
1143
+ closed = true;
1144
+ i++;
1145
+ break;
1146
+ }
1147
+ segment += ch;
1148
+ i++;
1149
+ }
1150
+ if (!closed) throw new ConfigError(`unterminated quoted path segment: ${path2}`, path2);
1151
+ segments.push(segment);
1152
+ if (i < path2.length && path2[i] === ".") i++;
1153
+ } else {
1154
+ const dot = path2.indexOf(".", i);
1155
+ if (dot === -1) {
1156
+ segments.push(path2.slice(i));
1157
+ break;
1158
+ }
1159
+ segments.push(path2.slice(i, dot));
1160
+ i = dot + 1;
1161
+ }
1162
+ }
1163
+ return segments;
1164
+ }
810
1165
  function hoconToJs(v) {
811
1166
  switch (v.kind) {
812
1167
  case "scalar":
@@ -844,14 +1199,19 @@ function getEnv(opts) {
844
1199
  function defaultReadFileSync(filePath) {
845
1200
  return fs.readFileSync(filePath, "utf-8");
846
1201
  }
847
- function parse(input, opts = {}) {
1202
+ function buildResolveContext(input, opts) {
848
1203
  const tokens = tokenize(input);
849
1204
  const ast = parseTokens(tokens);
850
1205
  const resolveOpts = {
851
1206
  env: getEnv(opts),
852
1207
  baseDir: opts.baseDir,
853
- readFileSync: opts.readFileSync ?? defaultReadFileSync
1208
+ readFileSync: opts.readFileSync ?? defaultReadFileSync,
1209
+ readFile: opts.readFile
854
1210
  };
1211
+ return { ast, resolveOpts };
1212
+ }
1213
+ function parse(input, opts = {}) {
1214
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
855
1215
  const value = resolve2(ast, resolveOpts);
856
1216
  if (value.kind !== "object") throw new Error("resolved value is not an object");
857
1217
  return new Config(value);
package/dist/zod.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ZodType } from 'zod';
2
- import { C as Config, P as ParseOptions } from './parse-C1NDMzD5.cjs';
2
+ import { C as Config, P as ParseOptions } from './parse-C4VOYuEk.cjs';
3
3
 
4
4
  declare function validate<T>(config: Config, schema: ZodType<T>): T;
5
5
  declare function getValidated<T>(config: Config, path: string, schema: ZodType<T>): T;
package/dist/zod.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ZodType } from 'zod';
2
- import { C as Config, P as ParseOptions } from './parse-C1NDMzD5.js';
2
+ import { C as Config, P as ParseOptions } from './parse-C4VOYuEk.js';
3
3
 
4
4
  declare function validate<T>(config: Config, schema: ZodType<T>): T;
5
5
  declare function getValidated<T>(config: Config, path: string, schema: ZodType<T>): T;