@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/README.md +45 -0
- package/dist/index.cjs +544 -39
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +544 -39
- package/dist/{parse-C1NDMzD5.d.cts → parse-C4VOYuEk.d.cts} +8 -5
- package/dist/{parse-C1NDMzD5.d.ts → parse-C4VOYuEk.d.ts} +8 -5
- package/dist/zod.cjs +398 -38
- package/dist/zod.d.cts +1 -1
- package/dist/zod.d.ts +1 -1
- package/dist/zod.js +398 -38
- package/package.json +10 -2
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
|
|
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
|
-
|
|
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 "
|
|
505
|
+
return ch !== "" && !'{}[],:=+#\n\r "$?!@*&^\\'.includes(ch);
|
|
318
506
|
}
|
|
319
507
|
function isUnquotedContinue(ch, nextFn) {
|
|
320
|
-
if (ch === "" || '{}[],:=\n\r #"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1000
|
+
for (const v of nonSep) {
|
|
690
1001
|
if (v.kind === "object") {
|
|
691
1002
|
for (const [k, val] of v.fields) {
|
|
692
|
-
merged.
|
|
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
|
|
767
|
-
|
|
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
|
-
|
|
773
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|