@o3co/ts.hocon 1.2.0 → 1.4.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.ja.md +4 -2
- package/README.md +59 -8
- package/dist/config-BwjvKeZ5.d.cts +271 -0
- package/dist/config-BwjvKeZ5.d.ts +271 -0
- package/dist/index.cjs +1379 -400
- package/dist/index.d.cts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +1368 -399
- package/dist/zod.cjs +1112 -253
- package/dist/zod.d.cts +1 -1
- package/dist/zod.d.ts +1 -1
- package/dist/zod.js +1111 -253
- package/package.json +3 -1
- package/dist/parse-nNbyqnOh.d.cts +0 -53
- package/dist/parse-nNbyqnOh.d.ts +0 -53
package/dist/zod.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
// src/coerce.ts
|
|
2
2
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
3
|
+
function isHoconWs(cp) {
|
|
4
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
5
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
6
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
7
|
+
if (cp === 5760) return true;
|
|
8
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
9
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
10
|
+
if (cp === 12288) return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
function trimHoconWs(s) {
|
|
14
|
+
let start = 0;
|
|
15
|
+
let end = s.length;
|
|
16
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
17
|
+
start++;
|
|
18
|
+
}
|
|
19
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
20
|
+
end--;
|
|
21
|
+
}
|
|
22
|
+
return s.slice(start, end);
|
|
23
|
+
}
|
|
3
24
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
4
25
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
5
26
|
function coerceBoolean(value) {
|
|
@@ -45,22 +66,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
45
66
|
d: 864e5
|
|
46
67
|
};
|
|
47
68
|
function parseDuration(value, outputUnit = "ms") {
|
|
48
|
-
const trimmed = value
|
|
69
|
+
const trimmed = trimHoconWs(value);
|
|
49
70
|
let i = 0;
|
|
50
71
|
while (i < trimmed.length) {
|
|
51
72
|
const ch = trimmed[i];
|
|
52
|
-
if (
|
|
73
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
53
78
|
i++;
|
|
54
79
|
}
|
|
55
80
|
if (i === 0) return NaN;
|
|
56
81
|
const num = Number(trimmed.slice(0, i));
|
|
57
82
|
if (Number.isNaN(num)) return NaN;
|
|
58
|
-
const unit = trimmed.slice(i)
|
|
83
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
84
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
85
|
+
if (divisor === void 0) return NaN;
|
|
86
|
+
if (unit === "") {
|
|
87
|
+
return num / divisor;
|
|
88
|
+
}
|
|
59
89
|
const mult = DURATION_UNITS[unit];
|
|
60
90
|
if (mult === void 0) return NaN;
|
|
61
91
|
const ms = num * mult;
|
|
62
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
63
|
-
if (divisor === void 0) return NaN;
|
|
64
92
|
return ms / divisor;
|
|
65
93
|
}
|
|
66
94
|
var BYTE_UNITS = {
|
|
@@ -91,6 +119,22 @@ var BYTE_UNITS = {
|
|
|
91
119
|
TiB: 1099511627776,
|
|
92
120
|
tebibyte: 1099511627776,
|
|
93
121
|
tebibytes: 1099511627776,
|
|
122
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
123
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
124
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
125
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
126
|
+
K: 1024,
|
|
127
|
+
k: 1024,
|
|
128
|
+
M: 1024 ** 2,
|
|
129
|
+
m: 1024 ** 2,
|
|
130
|
+
G: 1024 ** 3,
|
|
131
|
+
g: 1024 ** 3,
|
|
132
|
+
T: 1024 ** 4,
|
|
133
|
+
t: 1024 ** 4,
|
|
134
|
+
P: 1024 ** 5,
|
|
135
|
+
p: 1024 ** 5,
|
|
136
|
+
E: 1024 ** 6,
|
|
137
|
+
e: 1024 ** 6,
|
|
94
138
|
// lowercase short-form aliases
|
|
95
139
|
b: 1,
|
|
96
140
|
kb: 1e3,
|
|
@@ -114,29 +158,47 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
114
158
|
TiB: 1099511627776
|
|
115
159
|
};
|
|
116
160
|
function parseBytes(value, outputUnit = "B") {
|
|
117
|
-
const trimmed = value
|
|
161
|
+
const trimmed = trimHoconWs(value);
|
|
118
162
|
let i = 0;
|
|
119
163
|
while (i < trimmed.length) {
|
|
120
164
|
const ch = trimmed[i];
|
|
121
|
-
if (
|
|
165
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
122
170
|
i++;
|
|
123
171
|
}
|
|
124
172
|
if (i === 0) return NaN;
|
|
125
173
|
const num = Number(trimmed.slice(0, i));
|
|
126
174
|
if (Number.isNaN(num)) return NaN;
|
|
127
|
-
const unit = trimmed.slice(i)
|
|
175
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
176
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
177
|
+
if (divisor === void 0) return NaN;
|
|
178
|
+
if (unit === "") {
|
|
179
|
+
const bytes2 = Math.trunc(num);
|
|
180
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
181
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
182
|
+
}
|
|
183
|
+
return bytes2 / divisor;
|
|
184
|
+
}
|
|
128
185
|
let mult = BYTE_UNITS[unit];
|
|
129
186
|
if (mult === void 0) {
|
|
130
187
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
131
188
|
}
|
|
132
189
|
if (mult === void 0) return NaN;
|
|
133
190
|
const bytes = num * mult;
|
|
134
|
-
|
|
135
|
-
|
|
191
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
192
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
193
|
+
}
|
|
136
194
|
const result = bytes / divisor;
|
|
137
195
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
138
196
|
}
|
|
139
197
|
|
|
198
|
+
// src/parse.ts
|
|
199
|
+
import * as fs from "fs";
|
|
200
|
+
import * as path from "path";
|
|
201
|
+
|
|
140
202
|
// src/errors.ts
|
|
141
203
|
var ParseError = class extends Error {
|
|
142
204
|
constructor(message, line, col, file) {
|
|
@@ -157,6 +219,14 @@ var ResolveError = class extends Error {
|
|
|
157
219
|
this.name = "ResolveError";
|
|
158
220
|
}
|
|
159
221
|
};
|
|
222
|
+
var PackageLookupError = class extends ResolveError {
|
|
223
|
+
constructor(message, identifier, packageFile, line, col) {
|
|
224
|
+
super(message, `${identifier}/${packageFile}`, line, col);
|
|
225
|
+
this.identifier = identifier;
|
|
226
|
+
this.packageFile = packageFile;
|
|
227
|
+
this.name = "PackageLookupError";
|
|
228
|
+
}
|
|
229
|
+
};
|
|
160
230
|
var ConfigError = class extends Error {
|
|
161
231
|
constructor(message, path2) {
|
|
162
232
|
super(message);
|
|
@@ -164,6 +234,156 @@ var ConfigError = class extends Error {
|
|
|
164
234
|
this.name = "ConfigError";
|
|
165
235
|
}
|
|
166
236
|
};
|
|
237
|
+
var NotResolvedError = class extends ConfigError {
|
|
238
|
+
constructor(path2) {
|
|
239
|
+
super(`value at path "${path2}" is not resolved (call resolve() first)`, path2);
|
|
240
|
+
this.name = "NotResolvedError";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/internal/resolver/types.ts
|
|
245
|
+
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
246
|
+
function isSubst(v) {
|
|
247
|
+
return v._kind === "subst-placeholder";
|
|
248
|
+
}
|
|
249
|
+
function isConcat(v) {
|
|
250
|
+
return v._kind === "concat-placeholder";
|
|
251
|
+
}
|
|
252
|
+
function isAppend(v) {
|
|
253
|
+
return v._kind === "append-placeholder";
|
|
254
|
+
}
|
|
255
|
+
function isResObj(v) {
|
|
256
|
+
return v._kind === "res-obj";
|
|
257
|
+
}
|
|
258
|
+
function makeResObj() {
|
|
259
|
+
return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
|
|
260
|
+
}
|
|
261
|
+
function mergeUnresolved(receiver, fallback) {
|
|
262
|
+
const result = makeResObj();
|
|
263
|
+
for (const [k, v] of fallback.fields) {
|
|
264
|
+
result.fields.set(k, v);
|
|
265
|
+
}
|
|
266
|
+
for (const [k, v] of fallback.priorValues) {
|
|
267
|
+
result.priorValues.set(k, v);
|
|
268
|
+
}
|
|
269
|
+
for (const [k, rv] of receiver.fields) {
|
|
270
|
+
const existing = result.fields.get(k);
|
|
271
|
+
if (existing !== void 0) {
|
|
272
|
+
const recPrior = receiver.priorValues.get(k);
|
|
273
|
+
if (recPrior !== void 0 && !isResObj(recPrior)) {
|
|
274
|
+
result.fields.set(k, rv);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (isResObj(rv) && isResObj(existing)) {
|
|
278
|
+
result.fields.set(k, mergeUnresolved(rv, existing));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
result.priorValues.set(k, existing);
|
|
282
|
+
}
|
|
283
|
+
result.fields.set(k, rv);
|
|
284
|
+
}
|
|
285
|
+
for (const [k, v] of receiver.priorValues) {
|
|
286
|
+
if (receiver.fields.has(k)) {
|
|
287
|
+
result.priorValues.set(k, v);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/internal/resolver/utils.ts
|
|
294
|
+
function segmentsToKey(segments) {
|
|
295
|
+
return segments.map((s) => {
|
|
296
|
+
const t = s.text;
|
|
297
|
+
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
298
|
+
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
299
|
+
}
|
|
300
|
+
return t;
|
|
301
|
+
}).join(".");
|
|
302
|
+
}
|
|
303
|
+
function lookupPath(root, segments) {
|
|
304
|
+
const [head, ...tail] = segments;
|
|
305
|
+
if (head === void 0) return void 0;
|
|
306
|
+
const key = head.text;
|
|
307
|
+
const val = root.fields.get(key);
|
|
308
|
+
if (val === void 0) return void 0;
|
|
309
|
+
if (tail.length === 0) return val;
|
|
310
|
+
if (isResObj(val)) return lookupPath(val, tail);
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
function lookupResObj(root, segments) {
|
|
314
|
+
let cur = root;
|
|
315
|
+
for (const seg of segments) {
|
|
316
|
+
const val = cur.fields.get(seg.text);
|
|
317
|
+
if (val === void 0 || !isResObj(val)) return void 0;
|
|
318
|
+
cur = val;
|
|
319
|
+
}
|
|
320
|
+
return cur;
|
|
321
|
+
}
|
|
322
|
+
function deepMergeHoconValues(base, overlay) {
|
|
323
|
+
const merged = new Map(base.fields);
|
|
324
|
+
for (const [k, v] of overlay.fields) {
|
|
325
|
+
const existing = merged.get(k);
|
|
326
|
+
if (existing?.kind === "object" && v.kind === "object") {
|
|
327
|
+
merged.set(k, deepMergeHoconValues(existing, v));
|
|
328
|
+
} else {
|
|
329
|
+
merged.set(k, v);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { kind: "object", fields: merged };
|
|
333
|
+
}
|
|
334
|
+
function deepMergeResObjInto(dst, src) {
|
|
335
|
+
for (const [k, srcVal] of src.fields) {
|
|
336
|
+
const dstVal = dst.fields.get(k);
|
|
337
|
+
if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
|
|
338
|
+
deepMergeResObjInto(dstVal, srcVal);
|
|
339
|
+
} else {
|
|
340
|
+
if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
|
|
341
|
+
dst.fields.set(k, srcVal);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const [k, srcPrior] of src.priorValues) {
|
|
345
|
+
if (!dst.priorValues.has(k)) {
|
|
346
|
+
dst.priorValues.set(k, srcPrior);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function hoconValueToResObj(hv) {
|
|
351
|
+
const obj = makeResObj();
|
|
352
|
+
if (hv.kind !== "object") return obj;
|
|
353
|
+
for (const [key, val] of hv.fields) {
|
|
354
|
+
if (val.kind === "object") {
|
|
355
|
+
obj.fields.set(key, hoconValueToResObj(val));
|
|
356
|
+
} else {
|
|
357
|
+
obj.fields.set(key, val);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return obj;
|
|
361
|
+
}
|
|
362
|
+
function isFileNotFoundError(e) {
|
|
363
|
+
if (!(e instanceof Error)) return false;
|
|
364
|
+
const code = e.code;
|
|
365
|
+
if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
|
|
366
|
+
const msg = e.message.toLowerCase();
|
|
367
|
+
return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/internal/resolver/include-loader.ts
|
|
371
|
+
import { createRequire } from "module";
|
|
372
|
+
import * as nodePath from "path";
|
|
373
|
+
|
|
374
|
+
// src/internal/parser/empty-check.ts
|
|
375
|
+
function hasContentTokens(tokens) {
|
|
376
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
377
|
+
}
|
|
378
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
379
|
+
if (!hasContentTokens(tokens)) {
|
|
380
|
+
throw new ParseError(
|
|
381
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
382
|
+
1,
|
|
383
|
+
1
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
167
387
|
|
|
168
388
|
// src/internal/lexer/lexer.ts
|
|
169
389
|
function isHoconWhitespace(ch) {
|
|
@@ -292,14 +512,6 @@ var Lexer = class {
|
|
|
292
512
|
continue;
|
|
293
513
|
}
|
|
294
514
|
if (isUnquotedStart(ch)) {
|
|
295
|
-
if (ch === "-" && !isDecimalDigit(this.peek(1))) {
|
|
296
|
-
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
297
|
-
throw new ParseError(
|
|
298
|
-
`unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
299
|
-
sl,
|
|
300
|
-
sc
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
515
|
let value = "";
|
|
304
516
|
while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
|
|
305
517
|
value += this.advance();
|
|
@@ -403,6 +615,10 @@ var Lexer = class {
|
|
|
403
615
|
/**
|
|
404
616
|
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
405
617
|
* Implements Appendix A state machine from the cross-language spec.
|
|
618
|
+
*
|
|
619
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
620
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
621
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
406
622
|
*/
|
|
407
623
|
parseSubstBody(startLine, startCol) {
|
|
408
624
|
let optional = false;
|
|
@@ -417,6 +633,7 @@ var Lexer = class {
|
|
|
417
633
|
let pendingWs = "";
|
|
418
634
|
const segments = [];
|
|
419
635
|
let lastDot = null;
|
|
636
|
+
let listSuffix = false;
|
|
420
637
|
while (true) {
|
|
421
638
|
if (this.pos >= this.input.length) {
|
|
422
639
|
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
@@ -426,6 +643,23 @@ var Lexer = class {
|
|
|
426
643
|
this.advance();
|
|
427
644
|
pendingWs = "";
|
|
428
645
|
break;
|
|
646
|
+
} else if (ch === "[") {
|
|
647
|
+
if (!curStarted) {
|
|
648
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
649
|
+
}
|
|
650
|
+
for (const w of pendingWs) {
|
|
651
|
+
if (w !== " " && w !== " ") {
|
|
652
|
+
throw new ParseError(
|
|
653
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
654
|
+
startLine,
|
|
655
|
+
this.col
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
660
|
+
pendingWs = "";
|
|
661
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
662
|
+
break;
|
|
429
663
|
} else if (ch === '"') {
|
|
430
664
|
const qLine = startLine;
|
|
431
665
|
const qCol = this.col;
|
|
@@ -485,15 +719,47 @@ var Lexer = class {
|
|
|
485
719
|
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
486
720
|
}
|
|
487
721
|
}
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
722
|
+
if (!listSuffix) {
|
|
723
|
+
if (curStarted) {
|
|
724
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
725
|
+
} else if (segments.length === 0) {
|
|
726
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
727
|
+
} else {
|
|
728
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
729
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
730
|
+
}
|
|
495
731
|
}
|
|
496
|
-
return { segments, optional };
|
|
732
|
+
return { segments, optional, listSuffix };
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
736
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
737
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
738
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
739
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
740
|
+
*/
|
|
741
|
+
parseListSuffix(startLine) {
|
|
742
|
+
this.advance();
|
|
743
|
+
const afterBracket = this.peek();
|
|
744
|
+
if (afterBracket !== "]") {
|
|
745
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
746
|
+
throw new ParseError(
|
|
747
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
748
|
+
startLine,
|
|
749
|
+
this.col
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
this.advance();
|
|
753
|
+
if (this.peek() !== "}") {
|
|
754
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
755
|
+
throw new ParseError(
|
|
756
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
757
|
+
startLine,
|
|
758
|
+
this.col
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
this.advance();
|
|
762
|
+
return true;
|
|
497
763
|
}
|
|
498
764
|
};
|
|
499
765
|
function tokenize(input) {
|
|
@@ -581,6 +847,14 @@ var Parser = class {
|
|
|
581
847
|
const t = this.peek();
|
|
582
848
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
583
849
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
850
|
+
const next = this.peek(1);
|
|
851
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
852
|
+
throw new ParseError(
|
|
853
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
854
|
+
t.line,
|
|
855
|
+
t.col
|
|
856
|
+
);
|
|
857
|
+
}
|
|
584
858
|
this.advance();
|
|
585
859
|
fields.push(this.parseInclude());
|
|
586
860
|
this.skip("newline");
|
|
@@ -589,7 +863,14 @@ var Parser = class {
|
|
|
589
863
|
continue;
|
|
590
864
|
}
|
|
591
865
|
const keyPos = this.currentPos();
|
|
592
|
-
const key = this.parseKey();
|
|
866
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
867
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
868
|
+
throw new ParseError(
|
|
869
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
870
|
+
keyPos.line,
|
|
871
|
+
keyPos.col
|
|
872
|
+
);
|
|
873
|
+
}
|
|
593
874
|
this.skip("newline");
|
|
594
875
|
let append = false;
|
|
595
876
|
const sep = this.peek();
|
|
@@ -619,10 +900,12 @@ var Parser = class {
|
|
|
619
900
|
parseKey() {
|
|
620
901
|
const segments = [];
|
|
621
902
|
let trailingDot = false;
|
|
903
|
+
let firstWasQuoted = false;
|
|
622
904
|
while (true) {
|
|
623
905
|
const t = this.peek();
|
|
624
906
|
if (t.kind === "string") {
|
|
625
907
|
this.advance();
|
|
908
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
626
909
|
segments.push(t.value);
|
|
627
910
|
trailingDot = false;
|
|
628
911
|
} else if (t.kind === "unquoted") {
|
|
@@ -659,64 +942,143 @@ var Parser = class {
|
|
|
659
942
|
}
|
|
660
943
|
break;
|
|
661
944
|
}
|
|
662
|
-
return segments;
|
|
945
|
+
return { segments, firstWasQuoted };
|
|
663
946
|
}
|
|
664
947
|
parseInclude() {
|
|
665
948
|
const p = this.currentPos();
|
|
666
949
|
this.skip("newline");
|
|
667
950
|
const t = this.peek();
|
|
668
|
-
let path2;
|
|
669
|
-
let required = false;
|
|
670
|
-
let isFile = false;
|
|
671
951
|
if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
|
|
672
952
|
if (t.value === "required") {
|
|
673
|
-
const
|
|
674
|
-
if (
|
|
953
|
+
const next = this.peek(1);
|
|
954
|
+
if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
|
|
675
955
|
throw new ParseError("include required must be followed by (", t.line, t.col);
|
|
676
956
|
}
|
|
677
957
|
}
|
|
678
|
-
required = true;
|
|
679
958
|
this.advance();
|
|
680
959
|
const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
|
|
681
|
-
if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
|
|
960
|
+
if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
|
|
682
961
|
throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
|
|
683
962
|
}
|
|
684
|
-
|
|
685
|
-
|
|
963
|
+
const nextTok = this.peek();
|
|
964
|
+
if (innerPrefix === "package" || innerPrefix.startsWith("package(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "package(" || nextTok.value.startsWith("package("))) {
|
|
965
|
+
const consumePackageToken = innerPrefix === "";
|
|
966
|
+
const { qualifier, path: path3 } = this.parsePackageArgs(consumePackageToken);
|
|
967
|
+
return this.makeIncludeField(qualifier, path3, true, p);
|
|
686
968
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
969
|
+
if (innerPrefix === "file" || innerPrefix.startsWith("file(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "file(" || nextTok.value === "file")) {
|
|
970
|
+
if (innerPrefix === "") {
|
|
971
|
+
this.advance();
|
|
972
|
+
}
|
|
973
|
+
const path3 = this.parseQuotedPathSkipWrapper(t);
|
|
974
|
+
return this.makeIncludeField({ kind: "file" }, path3, true, p);
|
|
975
|
+
}
|
|
976
|
+
if (innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "url" || nextTok.value.startsWith("url(") || nextTok.value === "classpath" || nextTok.value.startsWith("classpath("))) {
|
|
977
|
+
throw new ParseError("include url(...) and classpath(...) are not supported", nextTok.line, nextTok.col);
|
|
978
|
+
}
|
|
979
|
+
if (innerPrefix !== "") {
|
|
980
|
+
if (innerPrefix === ")") {
|
|
981
|
+
throw new ParseError("include required(...) must contain a path", t.line, t.col);
|
|
982
|
+
}
|
|
983
|
+
const name = innerPrefix.replace(/\(.*$/, "").replace(/\).*$/, "");
|
|
984
|
+
throw new ParseError(`unknown include qualifier inside required(): "${name}"`, t.line, t.col);
|
|
985
|
+
}
|
|
986
|
+
const path2 = this.parseQuotedPathSkipWrapper(t);
|
|
987
|
+
return this.makeIncludeField({ kind: "bare" }, path2, true, p);
|
|
988
|
+
}
|
|
989
|
+
if (t.kind === "string") {
|
|
990
|
+
const path2 = this.advance().value;
|
|
991
|
+
return this.makeIncludeField({ kind: "bare" }, path2, false, p);
|
|
992
|
+
}
|
|
993
|
+
if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
|
|
702
994
|
this.advance();
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
995
|
+
const path2 = this.parseQuotedPathSkipWrapper(t);
|
|
996
|
+
return this.makeIncludeField({ kind: "file" }, path2, false, p);
|
|
997
|
+
}
|
|
998
|
+
if (t.kind === "unquoted" && (t.value === "package(" || t.value.startsWith("package("))) {
|
|
999
|
+
const { qualifier, path: path2 } = this.parsePackageArgs();
|
|
1000
|
+
return this.makeIncludeField(qualifier, path2, false, p);
|
|
1001
|
+
}
|
|
1002
|
+
if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
|
|
708
1003
|
throw new ParseError("include url(...) is not supported", t.line, t.col);
|
|
709
|
-
}
|
|
1004
|
+
}
|
|
1005
|
+
if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
|
|
710
1006
|
throw new ParseError("include classpath(...) is not supported", t.line, t.col);
|
|
711
|
-
} else {
|
|
712
|
-
throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
|
|
713
1007
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1008
|
+
throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
|
|
1009
|
+
}
|
|
1010
|
+
makeIncludeField(qualifier, path2, required, pos) {
|
|
1011
|
+
return { key: [], value: { kind: "include", qualifier, path: path2, required, pos }, append: false, pos };
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Skip past include-syntax wrapper tokens to find the quoted path string.
|
|
1015
|
+
* Strict: only allows `(`, `)`, and bare qualifier keywords (file / file( /
|
|
1016
|
+
* package / package( / url / url( / classpath / classpath() between the
|
|
1017
|
+
* directive and the quoted path. Anything else is a parse error rather than
|
|
1018
|
+
* silently swallowed (see ts#113).
|
|
1019
|
+
*/
|
|
1020
|
+
parseQuotedPathSkipWrapper(errTok) {
|
|
1021
|
+
while (this.isIncludeWrapperToken(this.peek())) this.advance();
|
|
1022
|
+
if (this.peek().kind !== "string") {
|
|
1023
|
+
const tok = this.peek();
|
|
1024
|
+
if (tok.kind === "eof") throw new ParseError("expected include path", errTok.line, errTok.col);
|
|
1025
|
+
const desc = tok.kind === "unquoted" ? `unquoted "${tok.value}"` : tok.kind;
|
|
1026
|
+
throw new ParseError(`expected quoted include path, got ${desc}`, tok.line, tok.col);
|
|
1027
|
+
}
|
|
1028
|
+
const path2 = this.advance().value;
|
|
1029
|
+
while (this.peek().kind === "unquoted" && (this.peek().value === ")" || this.peek().value === "))")) {
|
|
1030
|
+
this.advance();
|
|
1031
|
+
}
|
|
1032
|
+
const after = this.peek();
|
|
1033
|
+
if (after.kind !== "newline" && after.kind !== "rbrace" && after.kind !== "eof" && after.kind !== "comma") {
|
|
1034
|
+
const desc = after.kind === "unquoted" ? `unquoted "${after.value}"` : after.kind;
|
|
1035
|
+
throw new ParseError(`unexpected token after include path: ${desc}`, after.line, after.col);
|
|
1036
|
+
}
|
|
1037
|
+
return path2;
|
|
1038
|
+
}
|
|
1039
|
+
isIncludeWrapperToken(tok) {
|
|
1040
|
+
return tok.kind === "unquoted" && tok.value === "(";
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Parse the body of `package("identifier", "file")`.
|
|
1044
|
+
*
|
|
1045
|
+
* @param consumePackageToken - When true (default), the current token is `package(` and
|
|
1046
|
+
* must be consumed first. When false, the lexer already bundled `package(` into the
|
|
1047
|
+
* preceding token (e.g. `required(package("`), so we are already positioned at the
|
|
1048
|
+
* first string argument.
|
|
1049
|
+
*/
|
|
1050
|
+
parsePackageArgs(consumePackageToken = true) {
|
|
1051
|
+
const pkgTok = consumePackageToken ? this.advance() : this.peek();
|
|
1052
|
+
this.skip("newline");
|
|
1053
|
+
const idTok = this.peek();
|
|
1054
|
+
if (idTok.kind !== "string") {
|
|
1055
|
+
throw new ParseError("include package: expected quoted identifier as first argument", pkgTok.line, pkgTok.col);
|
|
1056
|
+
}
|
|
1057
|
+
const identifier = this.advance().value;
|
|
1058
|
+
this.skip("newline");
|
|
1059
|
+
if (this.peek().kind !== "comma") {
|
|
1060
|
+
const after = this.peek();
|
|
1061
|
+
throw new ParseError(
|
|
1062
|
+
after.kind === "string" ? "include package: missing comma between identifier and file arguments" : "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
|
|
1063
|
+
pkgTok.line,
|
|
1064
|
+
pkgTok.col
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
this.advance();
|
|
1068
|
+
this.skip("newline");
|
|
1069
|
+
const fileTok = this.peek();
|
|
1070
|
+
if (fileTok.kind !== "string") {
|
|
1071
|
+
throw new ParseError(
|
|
1072
|
+
"include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
|
|
1073
|
+
pkgTok.line,
|
|
1074
|
+
pkgTok.col
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const filePath = this.advance().value;
|
|
1078
|
+
while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") {
|
|
1079
|
+
this.advance();
|
|
1080
|
+
}
|
|
1081
|
+
return { qualifier: { kind: "package", identifier }, path: filePath };
|
|
720
1082
|
}
|
|
721
1083
|
parseValue() {
|
|
722
1084
|
const p = this.currentPos();
|
|
@@ -738,7 +1100,7 @@ var Parser = class {
|
|
|
738
1100
|
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
739
1101
|
}
|
|
740
1102
|
const payload = t.subst;
|
|
741
|
-
node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
|
|
1103
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
742
1104
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
743
1105
|
this.advance();
|
|
744
1106
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -757,7 +1119,9 @@ var Parser = class {
|
|
|
757
1119
|
parts.push(node);
|
|
758
1120
|
}
|
|
759
1121
|
if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
|
|
760
|
-
if (parts.length === 1)
|
|
1122
|
+
if (parts.length === 1) {
|
|
1123
|
+
return parts[0];
|
|
1124
|
+
}
|
|
761
1125
|
return { kind: "concat", nodes: parts, pos: p };
|
|
762
1126
|
}
|
|
763
1127
|
parseArray() {
|
|
@@ -789,107 +1153,10 @@ function parseTokens(tokens) {
|
|
|
789
1153
|
return new Parser(tokens).parse();
|
|
790
1154
|
}
|
|
791
1155
|
|
|
792
|
-
// src/internal/resolver/types.ts
|
|
793
|
-
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
794
|
-
function isSubst(v) {
|
|
795
|
-
return v._kind === "subst-placeholder";
|
|
796
|
-
}
|
|
797
|
-
function isConcat(v) {
|
|
798
|
-
return v._kind === "concat-placeholder";
|
|
799
|
-
}
|
|
800
|
-
function isAppend(v) {
|
|
801
|
-
return v._kind === "append-placeholder";
|
|
802
|
-
}
|
|
803
|
-
function isResObj(v) {
|
|
804
|
-
return v._kind === "res-obj";
|
|
805
|
-
}
|
|
806
|
-
function makeResObj() {
|
|
807
|
-
return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// src/internal/resolver/utils.ts
|
|
811
|
-
function segmentsToKey(segments) {
|
|
812
|
-
return segments.map((s) => {
|
|
813
|
-
const t = s.text;
|
|
814
|
-
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
815
|
-
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
816
|
-
}
|
|
817
|
-
return t;
|
|
818
|
-
}).join(".");
|
|
819
|
-
}
|
|
820
|
-
function lookupPath(root, segments) {
|
|
821
|
-
const [head, ...tail] = segments;
|
|
822
|
-
if (head === void 0) return void 0;
|
|
823
|
-
const key = head.text;
|
|
824
|
-
const val = root.fields.get(key);
|
|
825
|
-
if (val === void 0) return void 0;
|
|
826
|
-
if (tail.length === 0) return val;
|
|
827
|
-
if (isResObj(val)) return lookupPath(val, tail);
|
|
828
|
-
return void 0;
|
|
829
|
-
}
|
|
830
|
-
function lookupResObj(root, segments) {
|
|
831
|
-
let cur = root;
|
|
832
|
-
for (const seg of segments) {
|
|
833
|
-
const val = cur.fields.get(seg.text);
|
|
834
|
-
if (val === void 0 || !isResObj(val)) return void 0;
|
|
835
|
-
cur = val;
|
|
836
|
-
}
|
|
837
|
-
return cur;
|
|
838
|
-
}
|
|
839
|
-
function deepMergeHoconValues(base, overlay) {
|
|
840
|
-
const merged = new Map(base.fields);
|
|
841
|
-
for (const [k, v] of overlay.fields) {
|
|
842
|
-
const existing = merged.get(k);
|
|
843
|
-
if (existing?.kind === "object" && v.kind === "object") {
|
|
844
|
-
merged.set(k, deepMergeHoconValues(existing, v));
|
|
845
|
-
} else {
|
|
846
|
-
merged.set(k, v);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return { kind: "object", fields: merged };
|
|
850
|
-
}
|
|
851
|
-
function deepMergeResObjInto(dst, src) {
|
|
852
|
-
for (const [k, srcVal] of src.fields) {
|
|
853
|
-
const dstVal = dst.fields.get(k);
|
|
854
|
-
if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
|
|
855
|
-
deepMergeResObjInto(dstVal, srcVal);
|
|
856
|
-
} else {
|
|
857
|
-
if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
|
|
858
|
-
dst.fields.set(k, srcVal);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
for (const [k, srcPrior] of src.priorValues) {
|
|
862
|
-
if (!dst.priorValues.has(k)) {
|
|
863
|
-
dst.priorValues.set(k, srcPrior);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
function hoconValueToResObj(hv) {
|
|
868
|
-
const obj = makeResObj();
|
|
869
|
-
if (hv.kind !== "object") return obj;
|
|
870
|
-
for (const [key, val] of hv.fields) {
|
|
871
|
-
if (val.kind === "object") {
|
|
872
|
-
obj.fields.set(key, hoconValueToResObj(val));
|
|
873
|
-
} else {
|
|
874
|
-
obj.fields.set(key, val);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
return obj;
|
|
878
|
-
}
|
|
879
|
-
function isFileNotFoundError(e) {
|
|
880
|
-
if (!(e instanceof Error)) return false;
|
|
881
|
-
const code = e.code;
|
|
882
|
-
if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
|
|
883
|
-
const msg = e.message.toLowerCase();
|
|
884
|
-
return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// src/internal/resolver/include-loader.ts
|
|
888
|
-
import * as nodePath from "path";
|
|
889
|
-
|
|
890
1156
|
// src/internal/properties/properties.ts
|
|
891
1157
|
function parseProperties(input) {
|
|
892
1158
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1159
|
+
const pairs = [];
|
|
893
1160
|
for (const line of input.split("\n")) {
|
|
894
1161
|
const trimmed = line.trim();
|
|
895
1162
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -898,6 +1165,10 @@ function parseProperties(input) {
|
|
|
898
1165
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
899
1166
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
900
1167
|
if (key === "") continue;
|
|
1168
|
+
pairs.push([key, value]);
|
|
1169
|
+
}
|
|
1170
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1171
|
+
for (const [key, value] of pairs) {
|
|
901
1172
|
setNested(root, key.split("."), value);
|
|
902
1173
|
}
|
|
903
1174
|
return root;
|
|
@@ -921,6 +1192,7 @@ function setNested(obj, segments, value) {
|
|
|
921
1192
|
}
|
|
922
1193
|
const last = segments[segments.length - 1];
|
|
923
1194
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1195
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
924
1196
|
current[last] = value;
|
|
925
1197
|
}
|
|
926
1198
|
function propertiesToHoconValue(input) {
|
|
@@ -940,6 +1212,84 @@ function recordToHoconValue(obj) {
|
|
|
940
1212
|
}
|
|
941
1213
|
|
|
942
1214
|
// src/internal/resolver/include-loader.ts
|
|
1215
|
+
var _require = createRequire(import.meta.url);
|
|
1216
|
+
function isPnpPath(p) {
|
|
1217
|
+
return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
|
|
1218
|
+
}
|
|
1219
|
+
function validatePackageFile(file, identifier) {
|
|
1220
|
+
if (file.length === 0) {
|
|
1221
|
+
throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
|
|
1222
|
+
}
|
|
1223
|
+
if (file.includes("\\")) {
|
|
1224
|
+
throw new ParseError(
|
|
1225
|
+
`include package("${identifier}", "${file}"): backslash not allowed in file argument (use forward slash)`,
|
|
1226
|
+
0,
|
|
1227
|
+
0
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (file.startsWith("/")) {
|
|
1231
|
+
throw new ParseError(
|
|
1232
|
+
`include package("${identifier}", "${file}"): absolute path not allowed in file argument`,
|
|
1233
|
+
0,
|
|
1234
|
+
0
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
if (/(?:^|\/)\.\.?(?:\/|$)/u.test(file)) {
|
|
1238
|
+
throw new ParseError(
|
|
1239
|
+
`include package("${identifier}", "${file}"): path traversal (. or ..) not allowed in file argument`,
|
|
1240
|
+
0,
|
|
1241
|
+
0
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (file.includes("//")) {
|
|
1245
|
+
throw new ParseError(
|
|
1246
|
+
`include package("${identifier}", "${file}"): consecutive slashes not allowed in file argument`,
|
|
1247
|
+
0,
|
|
1248
|
+
0
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function validatePackageIdentifier(identifier) {
|
|
1253
|
+
if (identifier.length === 0) {
|
|
1254
|
+
throw new ParseError("include package: identifier argument must be non-empty", 0, 0);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function defaultPackageResolver(identifier, file, includingFile, baseDir, resolveFrom) {
|
|
1258
|
+
const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
|
|
1259
|
+
let resolved;
|
|
1260
|
+
try {
|
|
1261
|
+
resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
|
|
1262
|
+
} catch {
|
|
1263
|
+
throw new PackageLookupError(
|
|
1264
|
+
`include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
|
|
1265
|
+
identifier,
|
|
1266
|
+
file,
|
|
1267
|
+
0,
|
|
1268
|
+
0
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (isPnpPath(resolved)) {
|
|
1272
|
+
throw new ResolveError(
|
|
1273
|
+
`include package("${identifier}", "${file}"): Yarn Berry PnP store paths are not supported. Provide a custom packageResolver option to handle PnP resolution.`,
|
|
1274
|
+
`${identifier}/${file}`,
|
|
1275
|
+
0,
|
|
1276
|
+
0
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
const fileParts = file.split("/");
|
|
1280
|
+
const expectedBasename = fileParts[fileParts.length - 1] ?? file;
|
|
1281
|
+
const resolvedBasename = nodePath.basename(resolved);
|
|
1282
|
+
if (resolvedBasename !== expectedBasename) {
|
|
1283
|
+
throw new PackageLookupError(
|
|
1284
|
+
`include package("${identifier}", "${file}"): case mismatch \u2014 resolved "${resolvedBasename}" but expected "${expectedBasename}" (E11 decision 5: case-sensitive)`,
|
|
1285
|
+
identifier,
|
|
1286
|
+
file,
|
|
1287
|
+
0,
|
|
1288
|
+
0
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
return resolved;
|
|
1292
|
+
}
|
|
943
1293
|
var IncludeLoader = class {
|
|
944
1294
|
constructor(opts) {
|
|
945
1295
|
this.opts = opts;
|
|
@@ -1019,8 +1369,77 @@ var IncludeLoader = class {
|
|
|
1019
1369
|
}
|
|
1020
1370
|
return merged;
|
|
1021
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Resolve and load `include package("identifier", "file")`.
|
|
1374
|
+
* Validates the identifier and file arguments, resolves via the package resolver,
|
|
1375
|
+
* performs cycle detection using a JSON-serialised cycle key, and parses the content.
|
|
1376
|
+
*/
|
|
1377
|
+
// `required` is reserved for future optional-package semantics (E11 decision 7);
|
|
1378
|
+
// currently all package lookup failures throw regardless of this flag.
|
|
1379
|
+
loadPackage(identifier, file, _required) {
|
|
1380
|
+
validatePackageIdentifier(identifier);
|
|
1381
|
+
validatePackageFile(file, identifier);
|
|
1382
|
+
const { includeStack = [], baseDir, resolveFrom, packageResolver, readFileSync: readFileSync2 } = this.opts;
|
|
1383
|
+
const cycleKey = JSON.stringify(["package", identifier, file]);
|
|
1384
|
+
if (includeStack.includes(cycleKey)) {
|
|
1385
|
+
throw new ResolveError(
|
|
1386
|
+
`circular include: package("${identifier}", "${file}")`,
|
|
1387
|
+
`${identifier}/${file}`,
|
|
1388
|
+
0,
|
|
1389
|
+
0
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (includeStack.length >= 50) {
|
|
1393
|
+
throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
|
|
1394
|
+
}
|
|
1395
|
+
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1396
|
+
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1397
|
+
const content = readFileSync2(resolvedPath);
|
|
1398
|
+
if (content.length === 0) return makeResObj();
|
|
1399
|
+
const tokens = tokenize(content);
|
|
1400
|
+
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1401
|
+
const ast = parseTokens(tokens);
|
|
1402
|
+
return this.onBuildResObj(ast, {
|
|
1403
|
+
...this.opts,
|
|
1404
|
+
baseDir: nodePath.dirname(resolvedPath),
|
|
1405
|
+
includeStack: [...includeStack, cycleKey]
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Async variant of `loadPackage`.
|
|
1410
|
+
*/
|
|
1411
|
+
async loadPackageAsync(identifier, file, _required) {
|
|
1412
|
+
validatePackageIdentifier(identifier);
|
|
1413
|
+
validatePackageFile(file, identifier);
|
|
1414
|
+
const { includeStack = [], baseDir, resolveFrom, packageResolver, readFile, readFileSync: readFileSync2 } = this.opts;
|
|
1415
|
+
const cycleKey = JSON.stringify(["package", identifier, file]);
|
|
1416
|
+
if (includeStack.includes(cycleKey)) {
|
|
1417
|
+
throw new ResolveError(
|
|
1418
|
+
`circular include: package("${identifier}", "${file}")`,
|
|
1419
|
+
`${identifier}/${file}`,
|
|
1420
|
+
0,
|
|
1421
|
+
0
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
if (includeStack.length >= 50) {
|
|
1425
|
+
throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
|
|
1426
|
+
}
|
|
1427
|
+
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1428
|
+
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1429
|
+
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1430
|
+
const content = await read(resolvedPath);
|
|
1431
|
+
if (content.length === 0) return makeResObj();
|
|
1432
|
+
const tokens = tokenize(content);
|
|
1433
|
+
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1434
|
+
const ast = parseTokens(tokens);
|
|
1435
|
+
return this.onBuildResObjAsync(ast, {
|
|
1436
|
+
...this.opts,
|
|
1437
|
+
baseDir: nodePath.dirname(resolvedPath),
|
|
1438
|
+
includeStack: [...includeStack, cycleKey]
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1022
1441
|
loadSingle(candidate) {
|
|
1023
|
-
const { readFileSync: readFileSync2, includeStack = []
|
|
1442
|
+
const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
|
|
1024
1443
|
if (includeStack.includes(candidate)) {
|
|
1025
1444
|
throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
|
|
1026
1445
|
}
|
|
@@ -1034,16 +1453,17 @@ var IncludeLoader = class {
|
|
|
1034
1453
|
if (candidate.endsWith(".properties")) {
|
|
1035
1454
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1036
1455
|
}
|
|
1037
|
-
const
|
|
1456
|
+
const tokens = tokenize(content);
|
|
1457
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1458
|
+
const ast = parseTokens(tokens);
|
|
1038
1459
|
return this.onBuildResObj(ast, {
|
|
1039
|
-
|
|
1460
|
+
...this.opts,
|
|
1040
1461
|
baseDir: nodePath.dirname(candidate),
|
|
1041
|
-
readFileSync: readFileSync2,
|
|
1042
1462
|
includeStack: [...includeStack, candidate]
|
|
1043
1463
|
});
|
|
1044
1464
|
}
|
|
1045
1465
|
async loadSingleAsync(candidate) {
|
|
1046
|
-
const { readFile, readFileSync: readFileSync2, includeStack = []
|
|
1466
|
+
const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
|
|
1047
1467
|
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1048
1468
|
if (includeStack.includes(candidate)) {
|
|
1049
1469
|
throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
|
|
@@ -1058,12 +1478,12 @@ var IncludeLoader = class {
|
|
|
1058
1478
|
if (candidate.endsWith(".properties")) {
|
|
1059
1479
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1060
1480
|
}
|
|
1061
|
-
const
|
|
1481
|
+
const tokens = tokenize(content);
|
|
1482
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1483
|
+
const ast = parseTokens(tokens);
|
|
1062
1484
|
return this.onBuildResObjAsync(ast, {
|
|
1063
|
-
|
|
1485
|
+
...this.opts,
|
|
1064
1486
|
baseDir: nodePath.dirname(candidate),
|
|
1065
|
-
readFileSync: readFileSync2,
|
|
1066
|
-
readFile,
|
|
1067
1487
|
includeStack: [...includeStack, candidate]
|
|
1068
1488
|
});
|
|
1069
1489
|
}
|
|
@@ -1072,7 +1492,6 @@ var IncludeLoader = class {
|
|
|
1072
1492
|
// src/internal/resolver/structure-builder.ts
|
|
1073
1493
|
var StructureBuilder = class _StructureBuilder {
|
|
1074
1494
|
constructor(opts) {
|
|
1075
|
-
this.opts = opts;
|
|
1076
1495
|
this.loader = new IncludeLoader(opts);
|
|
1077
1496
|
this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
|
|
1078
1497
|
this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
|
|
@@ -1099,7 +1518,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1099
1518
|
}
|
|
1100
1519
|
applyField(obj, field, pathPrefix) {
|
|
1101
1520
|
if (field.key.length === 0 && field.value.kind === "include") {
|
|
1102
|
-
const
|
|
1521
|
+
const { qualifier, path: path2, required } = field.value;
|
|
1522
|
+
let included;
|
|
1523
|
+
if (qualifier.kind === "package") {
|
|
1524
|
+
included = this.loader.loadPackage(qualifier.identifier, path2, required);
|
|
1525
|
+
} else {
|
|
1526
|
+
included = this.loader.load(path2, required, qualifier.kind === "file");
|
|
1527
|
+
}
|
|
1103
1528
|
if (pathPrefix.length > 0) {
|
|
1104
1529
|
this.relativizeResObj(included, pathPrefix);
|
|
1105
1530
|
}
|
|
@@ -1141,7 +1566,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1141
1566
|
}
|
|
1142
1567
|
async applyFieldAsync(obj, field, pathPrefix) {
|
|
1143
1568
|
if (field.key.length === 0 && field.value.kind === "include") {
|
|
1144
|
-
const
|
|
1569
|
+
const { qualifier, path: path2, required } = field.value;
|
|
1570
|
+
let included;
|
|
1571
|
+
if (qualifier.kind === "package") {
|
|
1572
|
+
included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
|
|
1573
|
+
} else {
|
|
1574
|
+
included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
|
|
1575
|
+
}
|
|
1145
1576
|
if (pathPrefix.length > 0) {
|
|
1146
1577
|
this.relativizeResObj(included, pathPrefix);
|
|
1147
1578
|
}
|
|
@@ -1195,9 +1626,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1195
1626
|
return inner;
|
|
1196
1627
|
}
|
|
1197
1628
|
case "subst":
|
|
1198
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1629
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1199
1630
|
case "concat":
|
|
1200
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1631
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1201
1632
|
case "include":
|
|
1202
1633
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1203
1634
|
}
|
|
@@ -1219,13 +1650,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1219
1650
|
case "object":
|
|
1220
1651
|
return await this.buildAsync(ast, pathPrefix);
|
|
1221
1652
|
case "subst":
|
|
1222
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1653
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1223
1654
|
case "concat": {
|
|
1224
1655
|
const nodes = [];
|
|
1225
1656
|
for (const n of ast.nodes) {
|
|
1226
1657
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1227
1658
|
}
|
|
1228
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1659
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1229
1660
|
}
|
|
1230
1661
|
case "include":
|
|
1231
1662
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1295,6 +1726,23 @@ var SubstitutionResolver = class {
|
|
|
1295
1726
|
this.opts = opts;
|
|
1296
1727
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1297
1728
|
this.cache = /* @__PURE__ */ new Map();
|
|
1729
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1730
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1731
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1732
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1733
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1734
|
+
//
|
|
1735
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1736
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1737
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1738
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1739
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1740
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1741
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1742
|
+
}
|
|
1743
|
+
/** Returns a prefix for error messages, including origin info when set. */
|
|
1744
|
+
originPrefix() {
|
|
1745
|
+
return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
|
|
1298
1746
|
}
|
|
1299
1747
|
resolve() {
|
|
1300
1748
|
return this.resolveResObj(this.root);
|
|
@@ -1333,7 +1781,15 @@ var SubstitutionResolver = class {
|
|
|
1333
1781
|
}
|
|
1334
1782
|
resolveVal(v, scope) {
|
|
1335
1783
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1336
|
-
if (isConcat(v))
|
|
1784
|
+
if (isConcat(v)) {
|
|
1785
|
+
this.resolvingConcats.add(v);
|
|
1786
|
+
try {
|
|
1787
|
+
const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1788
|
+
return concatResult;
|
|
1789
|
+
} finally {
|
|
1790
|
+
this.resolvingConcats.delete(v);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1337
1793
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1338
1794
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1339
1795
|
const hv = v;
|
|
@@ -1347,15 +1803,12 @@ var SubstitutionResolver = class {
|
|
|
1347
1803
|
}
|
|
1348
1804
|
return hv;
|
|
1349
1805
|
}
|
|
1350
|
-
segmentsEqual(a, b) {
|
|
1351
|
-
return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
|
|
1352
|
-
}
|
|
1353
1806
|
resolveSubst(s, scope) {
|
|
1354
|
-
const key = segmentsToKey(s.segments);
|
|
1807
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1355
1808
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1356
1809
|
if (this.resolving.has(key)) {
|
|
1357
1810
|
let prior;
|
|
1358
|
-
if (s.
|
|
1811
|
+
if (s.segments.length > 1) {
|
|
1359
1812
|
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1360
1813
|
const parentScope = lookupResObj(
|
|
1361
1814
|
this.root,
|
|
@@ -1377,7 +1830,7 @@ var SubstitutionResolver = class {
|
|
|
1377
1830
|
}
|
|
1378
1831
|
if (s.optional) return void 0;
|
|
1379
1832
|
throw new ResolveError(
|
|
1380
|
-
`circular substitution: ${key}`,
|
|
1833
|
+
this.originPrefix() + `circular substitution: ${key}`,
|
|
1381
1834
|
key,
|
|
1382
1835
|
s.line,
|
|
1383
1836
|
s.col
|
|
@@ -1387,29 +1840,31 @@ var SubstitutionResolver = class {
|
|
|
1387
1840
|
try {
|
|
1388
1841
|
const found = lookupPath(this.root, s.segments);
|
|
1389
1842
|
if (found !== void 0) {
|
|
1390
|
-
if (
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (prior !== void 0) {
|
|
1408
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1409
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1410
|
-
return result2;
|
|
1411
|
-
}
|
|
1843
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1844
|
+
let prior;
|
|
1845
|
+
if (s.segments.length > 1) {
|
|
1846
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1847
|
+
const parentScope = lookupResObj(
|
|
1848
|
+
this.root,
|
|
1849
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1850
|
+
);
|
|
1851
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1852
|
+
} else {
|
|
1853
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1854
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1855
|
+
}
|
|
1856
|
+
if (prior !== void 0) {
|
|
1857
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1858
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1859
|
+
return result2;
|
|
1412
1860
|
}
|
|
1861
|
+
if (s.optional) return void 0;
|
|
1862
|
+
throw new ResolveError(
|
|
1863
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}}`,
|
|
1864
|
+
key,
|
|
1865
|
+
s.line,
|
|
1866
|
+
s.col
|
|
1867
|
+
);
|
|
1413
1868
|
}
|
|
1414
1869
|
let result = this.resolveVal(found, scope);
|
|
1415
1870
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
@@ -1438,16 +1893,45 @@ var SubstitutionResolver = class {
|
|
|
1438
1893
|
if (result !== void 0) this.cache.set(key, result);
|
|
1439
1894
|
return result;
|
|
1440
1895
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1896
|
+
if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
|
|
1897
|
+
const result = this.resolveEnvList(s);
|
|
1898
|
+
if (result !== void 0) {
|
|
1899
|
+
this.cache.set(key, result);
|
|
1900
|
+
return result;
|
|
1901
|
+
}
|
|
1902
|
+
if (s.optional) return void 0;
|
|
1903
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1904
|
+
throw new ResolveError(
|
|
1905
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1906
|
+
key,
|
|
1907
|
+
s.line,
|
|
1908
|
+
s.col
|
|
1909
|
+
);
|
|
1910
|
+
} else if (s.listSuffix) {
|
|
1911
|
+
if (s.optional) return void 0;
|
|
1912
|
+
if (this.opts.allowUnresolved) return s;
|
|
1913
|
+
throw new ResolveError(
|
|
1914
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}}`,
|
|
1915
|
+
key,
|
|
1916
|
+
s.line,
|
|
1917
|
+
s.col
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
if (this.opts.useSystemEnvironment !== false) {
|
|
1921
|
+
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1922
|
+
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1923
|
+
if (envVal !== void 0) {
|
|
1924
|
+
const result = { kind: "scalar", raw: envVal, valueType: "string" };
|
|
1925
|
+
this.cache.set(key, result);
|
|
1926
|
+
return result;
|
|
1927
|
+
}
|
|
1447
1928
|
}
|
|
1448
1929
|
if (s.optional) return void 0;
|
|
1930
|
+
if (this.opts.allowUnresolved) {
|
|
1931
|
+
return s;
|
|
1932
|
+
}
|
|
1449
1933
|
throw new ResolveError(
|
|
1450
|
-
|
|
1934
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}}`,
|
|
1451
1935
|
key,
|
|
1452
1936
|
s.line,
|
|
1453
1937
|
s.col
|
|
@@ -1456,9 +1940,52 @@ var SubstitutionResolver = class {
|
|
|
1456
1940
|
this.resolving.delete(key);
|
|
1457
1941
|
}
|
|
1458
1942
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1943
|
+
/**
|
|
1944
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1945
|
+
*
|
|
1946
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1947
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1948
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1949
|
+
*
|
|
1950
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1951
|
+
* none do (caller applies optional/required handling).
|
|
1952
|
+
*/
|
|
1953
|
+
resolveEnvList(s) {
|
|
1954
|
+
const bases = this.candidateBases(s);
|
|
1955
|
+
for (const base of bases) {
|
|
1956
|
+
const key0 = base + "_0";
|
|
1957
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1958
|
+
const items = [];
|
|
1959
|
+
for (let i = 0; ; i++) {
|
|
1960
|
+
const k = base + "_" + i;
|
|
1961
|
+
if (!(k in this.opts.env)) break;
|
|
1962
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
1963
|
+
}
|
|
1964
|
+
return { kind: "array", items };
|
|
1965
|
+
}
|
|
1966
|
+
return void 0;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
1970
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
1971
|
+
*/
|
|
1972
|
+
candidateBases(s) {
|
|
1973
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
1974
|
+
if (s.prefixLen === 0) return [full];
|
|
1975
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
1976
|
+
return [full, bare];
|
|
1977
|
+
}
|
|
1978
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
1979
|
+
const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
|
|
1980
|
+
if (this.opts.allowUnresolved) {
|
|
1981
|
+
for (const v of rawResolved) {
|
|
1982
|
+
if (v !== void 0 && isSubst(v)) {
|
|
1983
|
+
return v;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
const resolved = rawResolved.filter((v) => v !== void 0);
|
|
1988
|
+
if (resolved.length === 0) return void 0;
|
|
1462
1989
|
if (resolved.length === 1) return resolved[0];
|
|
1463
1990
|
const nonSep = resolved.filter((v) => !separatorValues.has(v));
|
|
1464
1991
|
if (nonSep.length === 0) {
|
|
@@ -1474,27 +2001,45 @@ var SubstitutionResolver = class {
|
|
|
1474
2001
|
if (converted !== null) {
|
|
1475
2002
|
return { kind: "array", items: [...left.items, ...converted] };
|
|
1476
2003
|
}
|
|
1477
|
-
|
|
2004
|
+
throw new ResolveError(
|
|
2005
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
2006
|
+
"",
|
|
2007
|
+
line,
|
|
2008
|
+
col
|
|
2009
|
+
);
|
|
1478
2010
|
}
|
|
1479
2011
|
if (left.kind === "object" && right.kind === "array") {
|
|
1480
2012
|
const converted = numericObjectToArray(left);
|
|
1481
2013
|
if (converted !== null) {
|
|
1482
2014
|
return { kind: "array", items: [...converted, ...right.items] };
|
|
1483
2015
|
}
|
|
1484
|
-
|
|
2016
|
+
throw new ResolveError(
|
|
2017
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
2018
|
+
"",
|
|
2019
|
+
line,
|
|
2020
|
+
col
|
|
2021
|
+
);
|
|
1485
2022
|
}
|
|
1486
2023
|
if (left.kind === "array" && right.kind === "array") {
|
|
1487
2024
|
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1488
2025
|
}
|
|
1489
|
-
if (left.kind === "array") {
|
|
1490
|
-
|
|
2026
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
2027
|
+
throw new ResolveError(
|
|
2028
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
2029
|
+
"",
|
|
2030
|
+
line,
|
|
2031
|
+
col
|
|
2032
|
+
);
|
|
1491
2033
|
}
|
|
1492
|
-
if (right.kind === "
|
|
1493
|
-
|
|
2034
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
2035
|
+
throw new ResolveError(
|
|
2036
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
2037
|
+
"",
|
|
2038
|
+
line,
|
|
2039
|
+
col
|
|
2040
|
+
);
|
|
1494
2041
|
}
|
|
1495
|
-
|
|
1496
|
-
const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
|
|
1497
|
-
return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
|
|
2042
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1498
2043
|
};
|
|
1499
2044
|
const [head, ...tail] = nonSep;
|
|
1500
2045
|
const folded = tail.reduce(joinPair, head);
|
|
@@ -1514,15 +2059,89 @@ var SubstitutionResolver = class {
|
|
|
1514
2059
|
};
|
|
1515
2060
|
|
|
1516
2061
|
// src/internal/resolver/resolver.ts
|
|
2062
|
+
function buildTree(ast, opts) {
|
|
2063
|
+
return new StructureBuilder(opts).build(ast);
|
|
2064
|
+
}
|
|
2065
|
+
function resolveTree(tree, opts) {
|
|
2066
|
+
return new SubstitutionResolver(tree, opts).resolve();
|
|
2067
|
+
}
|
|
2068
|
+
function containsPlaceholders(tree) {
|
|
2069
|
+
for (const val of tree.fields.values()) {
|
|
2070
|
+
if (valContainsPlaceholders(val)) return true;
|
|
2071
|
+
}
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
function valContainsPlaceholders(v) {
|
|
2075
|
+
const rv = v;
|
|
2076
|
+
if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
|
|
2077
|
+
if (isResObj(rv)) return containsPlaceholders(rv);
|
|
2078
|
+
const hv = v;
|
|
2079
|
+
if (hv?.kind === "array") {
|
|
2080
|
+
return hv.items.some((item) => valContainsPlaceholders(item));
|
|
2081
|
+
}
|
|
2082
|
+
return false;
|
|
2083
|
+
}
|
|
2084
|
+
function buildPartialHoconFromResObj(tree) {
|
|
2085
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2086
|
+
for (const [k, v] of tree.fields) {
|
|
2087
|
+
if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
|
|
2088
|
+
if (isResObj(v)) {
|
|
2089
|
+
fields.set(k, buildPartialHoconFromResObj(v));
|
|
2090
|
+
} else {
|
|
2091
|
+
if (!valContainsPlaceholders(v)) {
|
|
2092
|
+
fields.set(k, v);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return { kind: "object", fields };
|
|
2098
|
+
}
|
|
2099
|
+
function hoconValueToResObj2(value) {
|
|
2100
|
+
const obj = makeResObj();
|
|
2101
|
+
for (const [k, v] of value.fields) {
|
|
2102
|
+
if (v.kind === "object") {
|
|
2103
|
+
obj.fields.set(k, hoconValueToResObj2(v));
|
|
2104
|
+
} else {
|
|
2105
|
+
obj.fields.set(k, v);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return obj;
|
|
2109
|
+
}
|
|
1517
2110
|
function resolve2(ast, opts) {
|
|
1518
|
-
const
|
|
1519
|
-
return
|
|
2111
|
+
const tree = buildTree(ast, opts);
|
|
2112
|
+
return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
|
|
1520
2113
|
}
|
|
1521
2114
|
|
|
1522
2115
|
// src/config.ts
|
|
1523
2116
|
var Config = class _Config {
|
|
1524
|
-
constructor(root) {
|
|
2117
|
+
constructor(root, opts) {
|
|
1525
2118
|
this.root = root;
|
|
2119
|
+
this._resolved = opts?.resolved ?? true;
|
|
2120
|
+
this._parseBaseDir = opts?.parseBaseDir;
|
|
2121
|
+
this._originDescription = opts?.originDescription;
|
|
2122
|
+
this._resObjRoot = opts?.resObjRoot;
|
|
2123
|
+
this._resolveOpts = opts?.resolveOpts;
|
|
2124
|
+
}
|
|
2125
|
+
/** Returns true when the Config's value tree contains no unresolved
|
|
2126
|
+
* substitution placeholders. Whole-config granularity (E12 decision 11). */
|
|
2127
|
+
isResolved() {
|
|
2128
|
+
return this._resolved;
|
|
2129
|
+
}
|
|
2130
|
+
/** @internal Test-only: construct a resolved Config from a HoconValue. */
|
|
2131
|
+
static _fromResolvedValue(root, opts) {
|
|
2132
|
+
return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
|
|
2133
|
+
}
|
|
2134
|
+
/** @internal Construct an unresolved Config carrying a ResObj tree.
|
|
2135
|
+
* Used by parseStringWithOptions (deferred path) in T6. */
|
|
2136
|
+
static _fromUnresolvedResObj(tree, opts) {
|
|
2137
|
+
const partialRoot = buildPartialHoconFromResObj(tree);
|
|
2138
|
+
return new _Config(partialRoot, {
|
|
2139
|
+
resolved: opts.resolved,
|
|
2140
|
+
parseBaseDir: opts.parseBaseDir,
|
|
2141
|
+
originDescription: opts.originDescription,
|
|
2142
|
+
resObjRoot: tree,
|
|
2143
|
+
resolveOpts: opts.resolveOpts
|
|
2144
|
+
});
|
|
1526
2145
|
}
|
|
1527
2146
|
get(path2) {
|
|
1528
2147
|
const v = this.lookupNode(path2);
|
|
@@ -1561,17 +2180,34 @@ var Config = class _Config {
|
|
|
1561
2180
|
}
|
|
1562
2181
|
const result = parseBytes(v.raw, unit);
|
|
1563
2182
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
2183
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1564
2184
|
return result;
|
|
1565
2185
|
}
|
|
1566
2186
|
getConfig(path2) {
|
|
1567
2187
|
const v = this.lookupNode(path2);
|
|
1568
|
-
if (v === void 0)
|
|
2188
|
+
if (v === void 0) {
|
|
2189
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2190
|
+
throw new NotResolvedError(path2);
|
|
2191
|
+
}
|
|
2192
|
+
throw new ConfigError(`path not found: ${path2}`, path2);
|
|
2193
|
+
}
|
|
1569
2194
|
if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
|
|
1570
|
-
|
|
2195
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2196
|
+
throw new NotResolvedError(path2);
|
|
2197
|
+
}
|
|
2198
|
+
return new _Config(v, { resolved: this._resolved });
|
|
1571
2199
|
}
|
|
1572
2200
|
getList(path2) {
|
|
1573
2201
|
const v = this.lookupNode(path2);
|
|
1574
|
-
if (v === void 0)
|
|
2202
|
+
if (v === void 0) {
|
|
2203
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2204
|
+
throw new NotResolvedError(path2);
|
|
2205
|
+
}
|
|
2206
|
+
throw new ConfigError(`path not found: ${path2}`, path2);
|
|
2207
|
+
}
|
|
2208
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2209
|
+
throw new NotResolvedError(path2);
|
|
2210
|
+
}
|
|
1575
2211
|
if (v.kind === "object") {
|
|
1576
2212
|
const converted = numericObjectToArray(v);
|
|
1577
2213
|
if (converted !== null) return converted.map(hoconToJs);
|
|
@@ -1586,12 +2222,138 @@ var Config = class _Config {
|
|
|
1586
2222
|
return [...this.root.fields.keys()];
|
|
1587
2223
|
}
|
|
1588
2224
|
withFallback(fallback) {
|
|
1589
|
-
|
|
1590
|
-
|
|
2225
|
+
if (fallback == null) return this;
|
|
2226
|
+
const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
|
|
2227
|
+
const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
|
|
2228
|
+
const merged = mergeUnresolved(selfTree, fbTree);
|
|
2229
|
+
const hasPlaceholders = containsPlaceholders(merged);
|
|
2230
|
+
const partialRoot = buildPartialHoconFromResObj(merged);
|
|
2231
|
+
return new _Config(partialRoot, {
|
|
2232
|
+
resolved: !hasPlaceholders,
|
|
2233
|
+
parseBaseDir: this._parseBaseDir,
|
|
2234
|
+
originDescription: this._originDescription,
|
|
2235
|
+
resObjRoot: merged,
|
|
2236
|
+
resolveOpts: this._resolveOpts ?? fallback._resolveOpts
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Runs substitution resolution (phase 2) on the stored unresolved tree.
|
|
2241
|
+
* Idempotent: calling resolve() on an already-resolved Config returns an
|
|
2242
|
+
* equivalent resolved Config.
|
|
2243
|
+
*
|
|
2244
|
+
* opts.allowUnresolved (default false) — when true, leaves unresolved
|
|
2245
|
+
* non-optional placeholders in place rather than throwing.
|
|
2246
|
+
* opts.useSystemEnvironment (default true) — when false, env var lookups
|
|
2247
|
+
* are suppressed (hermetic resolution).
|
|
2248
|
+
*
|
|
2249
|
+
* E12 decision 3.
|
|
2250
|
+
*/
|
|
2251
|
+
resolve(opts = {}) {
|
|
2252
|
+
if (this._resolved) {
|
|
2253
|
+
return new _Config(this.root, {
|
|
2254
|
+
resolved: true,
|
|
2255
|
+
parseBaseDir: this._parseBaseDir,
|
|
2256
|
+
originDescription: this._originDescription
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
const tree = this._resObjRoot;
|
|
2260
|
+
if (!tree) {
|
|
2261
|
+
return new _Config(this.root, { resolved: true });
|
|
2262
|
+
}
|
|
2263
|
+
const resolveOpts = {
|
|
2264
|
+
...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
|
|
2265
|
+
throw new Error("no files");
|
|
2266
|
+
} },
|
|
2267
|
+
allowUnresolved: opts.allowUnresolved ?? false,
|
|
2268
|
+
useSystemEnvironment: opts.useSystemEnvironment ?? true,
|
|
2269
|
+
originDescription: this._originDescription
|
|
2270
|
+
};
|
|
2271
|
+
const resolved = resolveTree(tree, resolveOpts);
|
|
2272
|
+
if (resolved.kind !== "object") throw new Error("resolve: expected object root");
|
|
2273
|
+
if (opts.allowUnresolved) {
|
|
2274
|
+
const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
|
|
2275
|
+
return new _Config(stripped, {
|
|
2276
|
+
resolved: !hadPlaceholders,
|
|
2277
|
+
parseBaseDir: this._parseBaseDir,
|
|
2278
|
+
originDescription: this._originDescription,
|
|
2279
|
+
resObjRoot: hadPlaceholders ? tree : void 0,
|
|
2280
|
+
resolveOpts: hadPlaceholders ? resolveOpts : void 0
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
return new _Config(resolved, {
|
|
2284
|
+
resolved: true,
|
|
2285
|
+
parseBaseDir: this._parseBaseDir,
|
|
2286
|
+
originDescription: this._originDescription
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Resolves receiver substitutions using source as lookup context.
|
|
2291
|
+
* Source's keys are NOT merged into the result — differs from
|
|
2292
|
+
* `this.withFallback(source).resolve()` which includes source keys.
|
|
2293
|
+
*
|
|
2294
|
+
* Precondition (E12 decision 10): source must be resolved. If
|
|
2295
|
+
* source.isResolved() is false, throws NotResolvedError.
|
|
2296
|
+
*
|
|
2297
|
+
* On an already-resolved receiver, resolveWith is a no-op (returns an
|
|
2298
|
+
* equivalent resolved Config, source keys not included).
|
|
2299
|
+
*
|
|
2300
|
+
* E12 decisions 9, 10.
|
|
2301
|
+
*/
|
|
2302
|
+
resolveWith(source, opts = {}) {
|
|
2303
|
+
if (!source.isResolved()) {
|
|
2304
|
+
throw new NotResolvedError("source");
|
|
2305
|
+
}
|
|
2306
|
+
if (this._resolved) {
|
|
2307
|
+
return new _Config(this.root, {
|
|
2308
|
+
resolved: true,
|
|
2309
|
+
parseBaseDir: this._parseBaseDir,
|
|
2310
|
+
originDescription: this._originDescription
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
|
|
2314
|
+
const srcTree = hoconValueToResObj2(source.root);
|
|
2315
|
+
const merged = mergeUnresolved(receiverTree, srcTree);
|
|
2316
|
+
const resolveOpts = {
|
|
2317
|
+
...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
|
|
2318
|
+
throw new Error("no files");
|
|
2319
|
+
} },
|
|
2320
|
+
allowUnresolved: opts.allowUnresolved ?? false,
|
|
2321
|
+
useSystemEnvironment: opts.useSystemEnvironment ?? true,
|
|
2322
|
+
originDescription: this._originDescription
|
|
2323
|
+
};
|
|
2324
|
+
const resolved = resolveTree(merged, resolveOpts);
|
|
2325
|
+
if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
|
|
2326
|
+
const receiverShapeForFilter = resObjToKeyShape(receiverTree);
|
|
2327
|
+
const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
|
|
2328
|
+
if (opts.allowUnresolved) {
|
|
2329
|
+
const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
|
|
2330
|
+
return new _Config(stripped, {
|
|
2331
|
+
resolved: !hadPlaceholders,
|
|
2332
|
+
parseBaseDir: this._parseBaseDir,
|
|
2333
|
+
originDescription: this._originDescription,
|
|
2334
|
+
resObjRoot: hadPlaceholders ? receiverTree : void 0,
|
|
2335
|
+
resolveOpts: hadPlaceholders ? resolveOpts : void 0
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
return new _Config(filtered, {
|
|
2339
|
+
resolved: true,
|
|
2340
|
+
parseBaseDir: this._parseBaseDir,
|
|
2341
|
+
originDescription: this._originDescription
|
|
2342
|
+
});
|
|
1591
2343
|
}
|
|
1592
2344
|
toObject() {
|
|
1593
2345
|
return hoconToJs(this.root);
|
|
1594
2346
|
}
|
|
2347
|
+
/**
|
|
2348
|
+
* @internal Test-only: renders this resolved Config's value tree as canonical
|
|
2349
|
+
* JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
|
|
2350
|
+
* against Lightbend ground truth. NOT part of the public API.
|
|
2351
|
+
*
|
|
2352
|
+
* Throws if the Config is unresolved (contains placeholders).
|
|
2353
|
+
*/
|
|
2354
|
+
_renderJSONForTest() {
|
|
2355
|
+
return renderHoconAsJSON(this.root);
|
|
2356
|
+
}
|
|
1595
2357
|
lookupNode(path2) {
|
|
1596
2358
|
const segments = splitConfigPath(path2);
|
|
1597
2359
|
let current = this.root;
|
|
@@ -1603,9 +2365,40 @@ var Config = class _Config {
|
|
|
1603
2365
|
}
|
|
1604
2366
|
return current;
|
|
1605
2367
|
}
|
|
2368
|
+
/**
|
|
2369
|
+
* _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
|
|
2370
|
+
* return true if the subtree there contains any unresolved placeholder.
|
|
2371
|
+
* Used by getConfig / getList (T2) to detect transitive unresolved placeholders
|
|
2372
|
+
* in object / array subtrees that would otherwise appear to be "resolved" in the
|
|
2373
|
+
* partial HoconValue root (because individual leaf fields are omitted rather than
|
|
2374
|
+
* the whole subtree when their value is a direct placeholder scalar, but object
|
|
2375
|
+
* sub-containers are retained in the partial root with the placeholder-keyed
|
|
2376
|
+
* fields omitted from them — so the object appears "present but empty-ish").
|
|
2377
|
+
*/
|
|
2378
|
+
_resObjRootSubtreeHasPlaceholders(path2) {
|
|
2379
|
+
const tree = this._resObjRoot;
|
|
2380
|
+
if (!tree) return false;
|
|
2381
|
+
const segments = splitConfigPath(path2);
|
|
2382
|
+
let cur = tree;
|
|
2383
|
+
for (const seg of segments) {
|
|
2384
|
+
const val = cur.fields.get(seg);
|
|
2385
|
+
if (val === void 0) return false;
|
|
2386
|
+
if (isResObj(val)) {
|
|
2387
|
+
cur = val;
|
|
2388
|
+
} else {
|
|
2389
|
+
return valContainsPlaceholders(val);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return containsPlaceholders(cur);
|
|
2393
|
+
}
|
|
1606
2394
|
requireScalar(path2) {
|
|
1607
2395
|
const v = this.lookupNode(path2);
|
|
1608
|
-
if (v === void 0)
|
|
2396
|
+
if (v === void 0) {
|
|
2397
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2398
|
+
throw new NotResolvedError(path2);
|
|
2399
|
+
}
|
|
2400
|
+
throw new ConfigError(`path not found: ${path2}`, path2);
|
|
2401
|
+
}
|
|
1609
2402
|
if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
|
|
1610
2403
|
return v;
|
|
1611
2404
|
}
|
|
@@ -1668,28 +2461,89 @@ function hoconToJs(v) {
|
|
|
1668
2461
|
case "array":
|
|
1669
2462
|
return v.items.map(hoconToJs);
|
|
1670
2463
|
case "object": {
|
|
1671
|
-
const obj =
|
|
2464
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
1672
2465
|
for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
|
|
1673
|
-
return obj;
|
|
2466
|
+
return Object.assign({}, obj);
|
|
1674
2467
|
}
|
|
1675
2468
|
}
|
|
1676
2469
|
}
|
|
1677
|
-
function
|
|
1678
|
-
const
|
|
1679
|
-
for (const [k, v] of
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2470
|
+
function resObjToKeyShape(tree) {
|
|
2471
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2472
|
+
for (const [k, v] of tree.fields) {
|
|
2473
|
+
if (isSubst(v) || isConcat(v) || isAppend(v)) {
|
|
2474
|
+
fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
|
|
2475
|
+
} else if (isResObj(v)) {
|
|
2476
|
+
fields.set(k, resObjToKeyShape(v));
|
|
1683
2477
|
} else {
|
|
1684
|
-
|
|
2478
|
+
fields.set(k, v);
|
|
1685
2479
|
}
|
|
1686
2480
|
}
|
|
1687
|
-
return { kind: "object", fields
|
|
2481
|
+
return { kind: "object", fields };
|
|
2482
|
+
}
|
|
2483
|
+
function stripPlaceholderFields(v) {
|
|
2484
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2485
|
+
let hadPlaceholders = false;
|
|
2486
|
+
for (const [k, val] of v.fields) {
|
|
2487
|
+
if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
|
|
2488
|
+
hadPlaceholders = true;
|
|
2489
|
+
} else if (val.kind === "object") {
|
|
2490
|
+
const inner = stripPlaceholderFields(val);
|
|
2491
|
+
if (inner.hadPlaceholders) hadPlaceholders = true;
|
|
2492
|
+
fields.set(k, inner.stripped);
|
|
2493
|
+
} else {
|
|
2494
|
+
fields.set(k, val);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return { stripped: { kind: "object", fields }, hadPlaceholders };
|
|
2498
|
+
}
|
|
2499
|
+
function filterByReceiverShape(resolved, receiverShape) {
|
|
2500
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2501
|
+
for (const [k, rv] of resolved.fields) {
|
|
2502
|
+
if (!receiverShape.fields.has(k)) continue;
|
|
2503
|
+
const receiverVal = receiverShape.fields.get(k);
|
|
2504
|
+
if (rv.kind === "object" && receiverVal.kind === "object") {
|
|
2505
|
+
fields.set(k, filterByReceiverShape(rv, receiverVal));
|
|
2506
|
+
} else {
|
|
2507
|
+
fields.set(k, rv);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return { kind: "object", fields };
|
|
2511
|
+
}
|
|
2512
|
+
function renderHoconAsJSON(v) {
|
|
2513
|
+
switch (v.kind) {
|
|
2514
|
+
case "scalar": {
|
|
2515
|
+
switch (v.valueType) {
|
|
2516
|
+
case "null":
|
|
2517
|
+
return "null";
|
|
2518
|
+
case "boolean":
|
|
2519
|
+
return v.raw;
|
|
2520
|
+
case "number": {
|
|
2521
|
+
const n = Number(v.raw);
|
|
2522
|
+
if (Number.isFinite(n)) return v.raw;
|
|
2523
|
+
return JSON.stringify(v.raw);
|
|
2524
|
+
}
|
|
2525
|
+
case "string":
|
|
2526
|
+
return JSON.stringify(v.raw);
|
|
2527
|
+
}
|
|
2528
|
+
break;
|
|
2529
|
+
}
|
|
2530
|
+
case "array": {
|
|
2531
|
+
const items = v.items.map(renderHoconAsJSON);
|
|
2532
|
+
return `[${items.join(",")}]`;
|
|
2533
|
+
}
|
|
2534
|
+
case "object": {
|
|
2535
|
+
const keys = [...v.fields.keys()].sort();
|
|
2536
|
+
const pairs = keys.map((k) => {
|
|
2537
|
+
const val = v.fields.get(k);
|
|
2538
|
+
return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
|
|
2539
|
+
});
|
|
2540
|
+
return `{${pairs.join(",")}}`;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
|
|
1688
2544
|
}
|
|
1689
2545
|
|
|
1690
2546
|
// src/parse.ts
|
|
1691
|
-
import * as fs from "fs";
|
|
1692
|
-
import * as path from "path";
|
|
1693
2547
|
function getEnv(opts) {
|
|
1694
2548
|
if (opts.env !== void 0) return opts.env;
|
|
1695
2549
|
if (typeof process !== "undefined" && process.env) return process.env;
|
|
@@ -1700,12 +2554,16 @@ function defaultReadFileSync(filePath) {
|
|
|
1700
2554
|
}
|
|
1701
2555
|
function buildResolveContext(input, opts) {
|
|
1702
2556
|
const tokens = tokenize(input);
|
|
2557
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1703
2558
|
const ast = parseTokens(tokens);
|
|
1704
2559
|
const resolveOpts = {
|
|
1705
2560
|
env: getEnv(opts),
|
|
1706
2561
|
baseDir: opts.baseDir,
|
|
1707
2562
|
readFileSync: opts.readFileSync ?? defaultReadFileSync,
|
|
1708
|
-
readFile: opts.readFile
|
|
2563
|
+
readFile: opts.readFile,
|
|
2564
|
+
originDescription: opts.originDescription,
|
|
2565
|
+
resolveFrom: opts.resolveFrom,
|
|
2566
|
+
packageResolver: opts.packageResolver
|
|
1709
2567
|
};
|
|
1710
2568
|
return { ast, resolveOpts };
|
|
1711
2569
|
}
|
|
@@ -1713,7 +2571,7 @@ function parse(input, opts = {}) {
|
|
|
1713
2571
|
const { ast, resolveOpts } = buildResolveContext(input, opts);
|
|
1714
2572
|
const value = resolve2(ast, resolveOpts);
|
|
1715
2573
|
if (value.kind !== "object") throw new Error("resolved value is not an object");
|
|
1716
|
-
return new Config(value);
|
|
2574
|
+
return new Config(value, { resolved: true, originDescription: opts.originDescription });
|
|
1717
2575
|
}
|
|
1718
2576
|
function parseFile(filePath, opts = {}) {
|
|
1719
2577
|
if (typeof process === "undefined" && !opts.readFileSync) {
|