@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.cjs
CHANGED
|
@@ -39,6 +39,27 @@ module.exports = __toCommonJS(zod_exports);
|
|
|
39
39
|
|
|
40
40
|
// src/coerce.ts
|
|
41
41
|
var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
42
|
+
function isHoconWs(cp) {
|
|
43
|
+
if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
|
|
44
|
+
if (cp >= 28 && cp <= 31) return true;
|
|
45
|
+
if (cp === 32 || cp === 160 || cp === 65279) return true;
|
|
46
|
+
if (cp === 5760) return true;
|
|
47
|
+
if (cp >= 8192 && cp <= 8202) return true;
|
|
48
|
+
if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
|
|
49
|
+
if (cp === 12288) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function trimHoconWs(s) {
|
|
53
|
+
let start = 0;
|
|
54
|
+
let end = s.length;
|
|
55
|
+
while (start < end && isHoconWs(s.charCodeAt(start))) {
|
|
56
|
+
start++;
|
|
57
|
+
}
|
|
58
|
+
while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
|
|
59
|
+
end--;
|
|
60
|
+
}
|
|
61
|
+
return s.slice(start, end);
|
|
62
|
+
}
|
|
42
63
|
var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
|
|
43
64
|
var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
|
|
44
65
|
function coerceBoolean(value) {
|
|
@@ -84,22 +105,29 @@ var OUTPUT_DURATION_UNITS = {
|
|
|
84
105
|
d: 864e5
|
|
85
106
|
};
|
|
86
107
|
function parseDuration(value, outputUnit = "ms") {
|
|
87
|
-
const trimmed = value
|
|
108
|
+
const trimmed = trimHoconWs(value);
|
|
88
109
|
let i = 0;
|
|
89
110
|
while (i < trimmed.length) {
|
|
90
111
|
const ch = trimmed[i];
|
|
91
|
-
if (
|
|
112
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
92
117
|
i++;
|
|
93
118
|
}
|
|
94
119
|
if (i === 0) return NaN;
|
|
95
120
|
const num = Number(trimmed.slice(0, i));
|
|
96
121
|
if (Number.isNaN(num)) return NaN;
|
|
97
|
-
const unit = trimmed.slice(i)
|
|
122
|
+
const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
|
|
123
|
+
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
124
|
+
if (divisor === void 0) return NaN;
|
|
125
|
+
if (unit === "") {
|
|
126
|
+
return num / divisor;
|
|
127
|
+
}
|
|
98
128
|
const mult = DURATION_UNITS[unit];
|
|
99
129
|
if (mult === void 0) return NaN;
|
|
100
130
|
const ms = num * mult;
|
|
101
|
-
const divisor = OUTPUT_DURATION_UNITS[outputUnit];
|
|
102
|
-
if (divisor === void 0) return NaN;
|
|
103
131
|
return ms / divisor;
|
|
104
132
|
}
|
|
105
133
|
var BYTE_UNITS = {
|
|
@@ -130,6 +158,22 @@ var BYTE_UNITS = {
|
|
|
130
158
|
TiB: 1099511627776,
|
|
131
159
|
tebibyte: 1099511627776,
|
|
132
160
|
tebibytes: 1099511627776,
|
|
161
|
+
// S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
|
|
162
|
+
// Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
|
|
163
|
+
// Both upper- and lowercase accepted per Lightbend behaviour.
|
|
164
|
+
// Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
|
|
165
|
+
K: 1024,
|
|
166
|
+
k: 1024,
|
|
167
|
+
M: 1024 ** 2,
|
|
168
|
+
m: 1024 ** 2,
|
|
169
|
+
G: 1024 ** 3,
|
|
170
|
+
g: 1024 ** 3,
|
|
171
|
+
T: 1024 ** 4,
|
|
172
|
+
t: 1024 ** 4,
|
|
173
|
+
P: 1024 ** 5,
|
|
174
|
+
p: 1024 ** 5,
|
|
175
|
+
E: 1024 ** 6,
|
|
176
|
+
e: 1024 ** 6,
|
|
133
177
|
// lowercase short-form aliases
|
|
134
178
|
b: 1,
|
|
135
179
|
kb: 1e3,
|
|
@@ -153,29 +197,47 @@ var OUTPUT_BYTE_UNITS = {
|
|
|
153
197
|
TiB: 1099511627776
|
|
154
198
|
};
|
|
155
199
|
function parseBytes(value, outputUnit = "B") {
|
|
156
|
-
const trimmed = value
|
|
200
|
+
const trimmed = trimHoconWs(value);
|
|
157
201
|
let i = 0;
|
|
158
202
|
while (i < trimmed.length) {
|
|
159
203
|
const ch = trimmed[i];
|
|
160
|
-
if (
|
|
204
|
+
if (i === 0 && (ch === "-" || ch === "+")) {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (ch !== "." && (ch < "0" || ch > "9")) break;
|
|
161
209
|
i++;
|
|
162
210
|
}
|
|
163
211
|
if (i === 0) return NaN;
|
|
164
212
|
const num = Number(trimmed.slice(0, i));
|
|
165
213
|
if (Number.isNaN(num)) return NaN;
|
|
166
|
-
const unit = trimmed.slice(i)
|
|
214
|
+
const unit = trimHoconWs(trimmed.slice(i));
|
|
215
|
+
const divisor = OUTPUT_BYTE_UNITS[outputUnit];
|
|
216
|
+
if (divisor === void 0) return NaN;
|
|
217
|
+
if (unit === "") {
|
|
218
|
+
const bytes2 = Math.trunc(num);
|
|
219
|
+
if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
|
|
220
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
221
|
+
}
|
|
222
|
+
return bytes2 / divisor;
|
|
223
|
+
}
|
|
167
224
|
let mult = BYTE_UNITS[unit];
|
|
168
225
|
if (mult === void 0) {
|
|
169
226
|
mult = BYTE_UNITS[unit.toLowerCase()];
|
|
170
227
|
}
|
|
171
228
|
if (mult === void 0) return NaN;
|
|
172
229
|
const bytes = num * mult;
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
|
|
231
|
+
throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
|
|
232
|
+
}
|
|
175
233
|
const result = bytes / divisor;
|
|
176
234
|
return outputUnit === "B" ? Math.round(result) : result;
|
|
177
235
|
}
|
|
178
236
|
|
|
237
|
+
// src/parse.ts
|
|
238
|
+
var fs = __toESM(require("fs"), 1);
|
|
239
|
+
var path = __toESM(require("path"), 1);
|
|
240
|
+
|
|
179
241
|
// src/errors.ts
|
|
180
242
|
var ParseError = class extends Error {
|
|
181
243
|
constructor(message, line, col, file) {
|
|
@@ -196,6 +258,14 @@ var ResolveError = class extends Error {
|
|
|
196
258
|
this.name = "ResolveError";
|
|
197
259
|
}
|
|
198
260
|
};
|
|
261
|
+
var PackageLookupError = class extends ResolveError {
|
|
262
|
+
constructor(message, identifier, packageFile, line, col) {
|
|
263
|
+
super(message, `${identifier}/${packageFile}`, line, col);
|
|
264
|
+
this.identifier = identifier;
|
|
265
|
+
this.packageFile = packageFile;
|
|
266
|
+
this.name = "PackageLookupError";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
199
269
|
var ConfigError = class extends Error {
|
|
200
270
|
constructor(message, path2) {
|
|
201
271
|
super(message);
|
|
@@ -203,6 +273,156 @@ var ConfigError = class extends Error {
|
|
|
203
273
|
this.name = "ConfigError";
|
|
204
274
|
}
|
|
205
275
|
};
|
|
276
|
+
var NotResolvedError = class extends ConfigError {
|
|
277
|
+
constructor(path2) {
|
|
278
|
+
super(`value at path "${path2}" is not resolved (call resolve() first)`, path2);
|
|
279
|
+
this.name = "NotResolvedError";
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/internal/resolver/types.ts
|
|
284
|
+
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
285
|
+
function isSubst(v) {
|
|
286
|
+
return v._kind === "subst-placeholder";
|
|
287
|
+
}
|
|
288
|
+
function isConcat(v) {
|
|
289
|
+
return v._kind === "concat-placeholder";
|
|
290
|
+
}
|
|
291
|
+
function isAppend(v) {
|
|
292
|
+
return v._kind === "append-placeholder";
|
|
293
|
+
}
|
|
294
|
+
function isResObj(v) {
|
|
295
|
+
return v._kind === "res-obj";
|
|
296
|
+
}
|
|
297
|
+
function makeResObj() {
|
|
298
|
+
return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
|
|
299
|
+
}
|
|
300
|
+
function mergeUnresolved(receiver, fallback) {
|
|
301
|
+
const result = makeResObj();
|
|
302
|
+
for (const [k, v] of fallback.fields) {
|
|
303
|
+
result.fields.set(k, v);
|
|
304
|
+
}
|
|
305
|
+
for (const [k, v] of fallback.priorValues) {
|
|
306
|
+
result.priorValues.set(k, v);
|
|
307
|
+
}
|
|
308
|
+
for (const [k, rv] of receiver.fields) {
|
|
309
|
+
const existing = result.fields.get(k);
|
|
310
|
+
if (existing !== void 0) {
|
|
311
|
+
const recPrior = receiver.priorValues.get(k);
|
|
312
|
+
if (recPrior !== void 0 && !isResObj(recPrior)) {
|
|
313
|
+
result.fields.set(k, rv);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (isResObj(rv) && isResObj(existing)) {
|
|
317
|
+
result.fields.set(k, mergeUnresolved(rv, existing));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
result.priorValues.set(k, existing);
|
|
321
|
+
}
|
|
322
|
+
result.fields.set(k, rv);
|
|
323
|
+
}
|
|
324
|
+
for (const [k, v] of receiver.priorValues) {
|
|
325
|
+
if (receiver.fields.has(k)) {
|
|
326
|
+
result.priorValues.set(k, v);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/internal/resolver/utils.ts
|
|
333
|
+
function segmentsToKey(segments) {
|
|
334
|
+
return segments.map((s) => {
|
|
335
|
+
const t = s.text;
|
|
336
|
+
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
337
|
+
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
338
|
+
}
|
|
339
|
+
return t;
|
|
340
|
+
}).join(".");
|
|
341
|
+
}
|
|
342
|
+
function lookupPath(root, segments) {
|
|
343
|
+
const [head, ...tail] = segments;
|
|
344
|
+
if (head === void 0) return void 0;
|
|
345
|
+
const key = head.text;
|
|
346
|
+
const val = root.fields.get(key);
|
|
347
|
+
if (val === void 0) return void 0;
|
|
348
|
+
if (tail.length === 0) return val;
|
|
349
|
+
if (isResObj(val)) return lookupPath(val, tail);
|
|
350
|
+
return void 0;
|
|
351
|
+
}
|
|
352
|
+
function lookupResObj(root, segments) {
|
|
353
|
+
let cur = root;
|
|
354
|
+
for (const seg of segments) {
|
|
355
|
+
const val = cur.fields.get(seg.text);
|
|
356
|
+
if (val === void 0 || !isResObj(val)) return void 0;
|
|
357
|
+
cur = val;
|
|
358
|
+
}
|
|
359
|
+
return cur;
|
|
360
|
+
}
|
|
361
|
+
function deepMergeHoconValues(base, overlay) {
|
|
362
|
+
const merged = new Map(base.fields);
|
|
363
|
+
for (const [k, v] of overlay.fields) {
|
|
364
|
+
const existing = merged.get(k);
|
|
365
|
+
if (existing?.kind === "object" && v.kind === "object") {
|
|
366
|
+
merged.set(k, deepMergeHoconValues(existing, v));
|
|
367
|
+
} else {
|
|
368
|
+
merged.set(k, v);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { kind: "object", fields: merged };
|
|
372
|
+
}
|
|
373
|
+
function deepMergeResObjInto(dst, src) {
|
|
374
|
+
for (const [k, srcVal] of src.fields) {
|
|
375
|
+
const dstVal = dst.fields.get(k);
|
|
376
|
+
if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
|
|
377
|
+
deepMergeResObjInto(dstVal, srcVal);
|
|
378
|
+
} else {
|
|
379
|
+
if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
|
|
380
|
+
dst.fields.set(k, srcVal);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
for (const [k, srcPrior] of src.priorValues) {
|
|
384
|
+
if (!dst.priorValues.has(k)) {
|
|
385
|
+
dst.priorValues.set(k, srcPrior);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function hoconValueToResObj(hv) {
|
|
390
|
+
const obj = makeResObj();
|
|
391
|
+
if (hv.kind !== "object") return obj;
|
|
392
|
+
for (const [key, val] of hv.fields) {
|
|
393
|
+
if (val.kind === "object") {
|
|
394
|
+
obj.fields.set(key, hoconValueToResObj(val));
|
|
395
|
+
} else {
|
|
396
|
+
obj.fields.set(key, val);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return obj;
|
|
400
|
+
}
|
|
401
|
+
function isFileNotFoundError(e) {
|
|
402
|
+
if (!(e instanceof Error)) return false;
|
|
403
|
+
const code = e.code;
|
|
404
|
+
if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
|
|
405
|
+
const msg = e.message.toLowerCase();
|
|
406
|
+
return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/internal/resolver/include-loader.ts
|
|
410
|
+
var import_node_module = require("module");
|
|
411
|
+
var nodePath = __toESM(require("path"), 1);
|
|
412
|
+
|
|
413
|
+
// src/internal/parser/empty-check.ts
|
|
414
|
+
function hasContentTokens(tokens) {
|
|
415
|
+
return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
|
|
416
|
+
}
|
|
417
|
+
function assertNonEmptyDocument(tokens, sourceDescriptor) {
|
|
418
|
+
if (!hasContentTokens(tokens)) {
|
|
419
|
+
throw new ParseError(
|
|
420
|
+
`empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
|
|
421
|
+
1,
|
|
422
|
+
1
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
206
426
|
|
|
207
427
|
// src/internal/lexer/lexer.ts
|
|
208
428
|
function isHoconWhitespace(ch) {
|
|
@@ -331,14 +551,6 @@ var Lexer = class {
|
|
|
331
551
|
continue;
|
|
332
552
|
}
|
|
333
553
|
if (isUnquotedStart(ch)) {
|
|
334
|
-
if (ch === "-" && !isDecimalDigit(this.peek(1))) {
|
|
335
|
-
const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
|
|
336
|
-
throw new ParseError(
|
|
337
|
-
`unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
|
|
338
|
-
sl,
|
|
339
|
-
sc
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
554
|
let value = "";
|
|
343
555
|
while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
|
|
344
556
|
value += this.advance();
|
|
@@ -442,6 +654,10 @@ var Lexer = class {
|
|
|
442
654
|
/**
|
|
443
655
|
* Parse the body of a `${...}` substitution (called after `${` has been consumed).
|
|
444
656
|
* Implements Appendix A state machine from the cross-language spec.
|
|
657
|
+
*
|
|
658
|
+
* S13c extension: if the segment-collection loop encounters `[`, it delegates to
|
|
659
|
+
* `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
|
|
660
|
+
* ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
|
|
445
661
|
*/
|
|
446
662
|
parseSubstBody(startLine, startCol) {
|
|
447
663
|
let optional = false;
|
|
@@ -456,6 +672,7 @@ var Lexer = class {
|
|
|
456
672
|
let pendingWs = "";
|
|
457
673
|
const segments = [];
|
|
458
674
|
let lastDot = null;
|
|
675
|
+
let listSuffix = false;
|
|
459
676
|
while (true) {
|
|
460
677
|
if (this.pos >= this.input.length) {
|
|
461
678
|
throw new ParseError("unterminated substitution", startLine, startCol);
|
|
@@ -465,6 +682,23 @@ var Lexer = class {
|
|
|
465
682
|
this.advance();
|
|
466
683
|
pendingWs = "";
|
|
467
684
|
break;
|
|
685
|
+
} else if (ch === "[") {
|
|
686
|
+
if (!curStarted) {
|
|
687
|
+
throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
|
|
688
|
+
}
|
|
689
|
+
for (const w of pendingWs) {
|
|
690
|
+
if (w !== " " && w !== " ") {
|
|
691
|
+
throw new ParseError(
|
|
692
|
+
`only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
|
|
693
|
+
startLine,
|
|
694
|
+
this.col
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
699
|
+
pendingWs = "";
|
|
700
|
+
listSuffix = this.parseListSuffix(startLine);
|
|
701
|
+
break;
|
|
468
702
|
} else if (ch === '"') {
|
|
469
703
|
const qLine = startLine;
|
|
470
704
|
const qCol = this.col;
|
|
@@ -524,15 +758,47 @@ var Lexer = class {
|
|
|
524
758
|
throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
|
|
525
759
|
}
|
|
526
760
|
}
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
761
|
+
if (!listSuffix) {
|
|
762
|
+
if (curStarted) {
|
|
763
|
+
segments.push({ text: curText, line: curLine, col: curCol });
|
|
764
|
+
} else if (segments.length === 0) {
|
|
765
|
+
throw new ParseError("empty substitution path", startLine, startCol);
|
|
766
|
+
} else {
|
|
767
|
+
const [errLine, errCol] = lastDot ?? [startLine, startCol];
|
|
768
|
+
throw new ParseError("empty segment in path", errLine, errCol);
|
|
769
|
+
}
|
|
534
770
|
}
|
|
535
|
-
return { segments, optional };
|
|
771
|
+
return { segments, optional, listSuffix };
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Consume the literal `[]` suffix inside a substitution body.
|
|
775
|
+
* Called after the `[` has been detected (but not yet consumed).
|
|
776
|
+
* Requires the next character to be `[`, then exactly `]`, then `}`.
|
|
777
|
+
* Whitespace inside `[]` is not permitted (per spec Decision §1).
|
|
778
|
+
* Returns true (always — only called when listSuffix path is taken).
|
|
779
|
+
*/
|
|
780
|
+
parseListSuffix(startLine) {
|
|
781
|
+
this.advance();
|
|
782
|
+
const afterBracket = this.peek();
|
|
783
|
+
if (afterBracket !== "]") {
|
|
784
|
+
const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
|
|
785
|
+
throw new ParseError(
|
|
786
|
+
`expected ']' after '[' in substitution list suffix (got ${desc})`,
|
|
787
|
+
startLine,
|
|
788
|
+
this.col
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
this.advance();
|
|
792
|
+
if (this.peek() !== "}") {
|
|
793
|
+
const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
|
|
794
|
+
throw new ParseError(
|
|
795
|
+
`expected '}' after '[]' in substitution list suffix (got ${desc})`,
|
|
796
|
+
startLine,
|
|
797
|
+
this.col
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
this.advance();
|
|
801
|
+
return true;
|
|
536
802
|
}
|
|
537
803
|
};
|
|
538
804
|
function tokenize(input) {
|
|
@@ -620,6 +886,14 @@ var Parser = class {
|
|
|
620
886
|
const t = this.peek();
|
|
621
887
|
if (t.kind === "eof" || t.kind === "rbrace") break;
|
|
622
888
|
if (t.kind === "unquoted" && t.value === "include") {
|
|
889
|
+
const next = this.peek(1);
|
|
890
|
+
if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
|
|
891
|
+
throw new ParseError(
|
|
892
|
+
`'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
|
|
893
|
+
t.line,
|
|
894
|
+
t.col
|
|
895
|
+
);
|
|
896
|
+
}
|
|
623
897
|
this.advance();
|
|
624
898
|
fields.push(this.parseInclude());
|
|
625
899
|
this.skip("newline");
|
|
@@ -628,7 +902,14 @@ var Parser = class {
|
|
|
628
902
|
continue;
|
|
629
903
|
}
|
|
630
904
|
const keyPos = this.currentPos();
|
|
631
|
-
const key = this.parseKey();
|
|
905
|
+
const { segments: key, firstWasQuoted } = this.parseKey();
|
|
906
|
+
if (key[0] === "include" && !firstWasQuoted) {
|
|
907
|
+
throw new ParseError(
|
|
908
|
+
`'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
|
|
909
|
+
keyPos.line,
|
|
910
|
+
keyPos.col
|
|
911
|
+
);
|
|
912
|
+
}
|
|
632
913
|
this.skip("newline");
|
|
633
914
|
let append = false;
|
|
634
915
|
const sep = this.peek();
|
|
@@ -658,10 +939,12 @@ var Parser = class {
|
|
|
658
939
|
parseKey() {
|
|
659
940
|
const segments = [];
|
|
660
941
|
let trailingDot = false;
|
|
942
|
+
let firstWasQuoted = false;
|
|
661
943
|
while (true) {
|
|
662
944
|
const t = this.peek();
|
|
663
945
|
if (t.kind === "string") {
|
|
664
946
|
this.advance();
|
|
947
|
+
if (segments.length === 0) firstWasQuoted = true;
|
|
665
948
|
segments.push(t.value);
|
|
666
949
|
trailingDot = false;
|
|
667
950
|
} else if (t.kind === "unquoted") {
|
|
@@ -698,64 +981,143 @@ var Parser = class {
|
|
|
698
981
|
}
|
|
699
982
|
break;
|
|
700
983
|
}
|
|
701
|
-
return segments;
|
|
984
|
+
return { segments, firstWasQuoted };
|
|
702
985
|
}
|
|
703
986
|
parseInclude() {
|
|
704
987
|
const p = this.currentPos();
|
|
705
988
|
this.skip("newline");
|
|
706
989
|
const t = this.peek();
|
|
707
|
-
let path2;
|
|
708
|
-
let required = false;
|
|
709
|
-
let isFile = false;
|
|
710
990
|
if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
|
|
711
991
|
if (t.value === "required") {
|
|
712
|
-
const
|
|
713
|
-
if (
|
|
992
|
+
const next = this.peek(1);
|
|
993
|
+
if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
|
|
714
994
|
throw new ParseError("include required must be followed by (", t.line, t.col);
|
|
715
995
|
}
|
|
716
996
|
}
|
|
717
|
-
required = true;
|
|
718
997
|
this.advance();
|
|
719
998
|
const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
|
|
720
|
-
if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
|
|
999
|
+
if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
|
|
721
1000
|
throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
|
|
722
1001
|
}
|
|
723
|
-
|
|
724
|
-
|
|
1002
|
+
const nextTok = this.peek();
|
|
1003
|
+
if (innerPrefix === "package" || innerPrefix.startsWith("package(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "package(" || nextTok.value.startsWith("package("))) {
|
|
1004
|
+
const consumePackageToken = innerPrefix === "";
|
|
1005
|
+
const { qualifier, path: path3 } = this.parsePackageArgs(consumePackageToken);
|
|
1006
|
+
return this.makeIncludeField(qualifier, path3, true, p);
|
|
725
1007
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1008
|
+
if (innerPrefix === "file" || innerPrefix.startsWith("file(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "file(" || nextTok.value === "file")) {
|
|
1009
|
+
if (innerPrefix === "") {
|
|
1010
|
+
this.advance();
|
|
1011
|
+
}
|
|
1012
|
+
const path3 = this.parseQuotedPathSkipWrapper(t);
|
|
1013
|
+
return this.makeIncludeField({ kind: "file" }, path3, true, p);
|
|
1014
|
+
}
|
|
1015
|
+
if (innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "url" || nextTok.value.startsWith("url(") || nextTok.value === "classpath" || nextTok.value.startsWith("classpath("))) {
|
|
1016
|
+
throw new ParseError("include url(...) and classpath(...) are not supported", nextTok.line, nextTok.col);
|
|
1017
|
+
}
|
|
1018
|
+
if (innerPrefix !== "") {
|
|
1019
|
+
if (innerPrefix === ")") {
|
|
1020
|
+
throw new ParseError("include required(...) must contain a path", t.line, t.col);
|
|
1021
|
+
}
|
|
1022
|
+
const name = innerPrefix.replace(/\(.*$/, "").replace(/\).*$/, "");
|
|
1023
|
+
throw new ParseError(`unknown include qualifier inside required(): "${name}"`, t.line, t.col);
|
|
1024
|
+
}
|
|
1025
|
+
const path2 = this.parseQuotedPathSkipWrapper(t);
|
|
1026
|
+
return this.makeIncludeField({ kind: "bare" }, path2, true, p);
|
|
1027
|
+
}
|
|
1028
|
+
if (t.kind === "string") {
|
|
1029
|
+
const path2 = this.advance().value;
|
|
1030
|
+
return this.makeIncludeField({ kind: "bare" }, path2, false, p);
|
|
1031
|
+
}
|
|
1032
|
+
if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
|
|
741
1033
|
this.advance();
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1034
|
+
const path2 = this.parseQuotedPathSkipWrapper(t);
|
|
1035
|
+
return this.makeIncludeField({ kind: "file" }, path2, false, p);
|
|
1036
|
+
}
|
|
1037
|
+
if (t.kind === "unquoted" && (t.value === "package(" || t.value.startsWith("package("))) {
|
|
1038
|
+
const { qualifier, path: path2 } = this.parsePackageArgs();
|
|
1039
|
+
return this.makeIncludeField(qualifier, path2, false, p);
|
|
1040
|
+
}
|
|
1041
|
+
if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
|
|
747
1042
|
throw new ParseError("include url(...) is not supported", t.line, t.col);
|
|
748
|
-
}
|
|
1043
|
+
}
|
|
1044
|
+
if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
|
|
749
1045
|
throw new ParseError("include classpath(...) is not supported", t.line, t.col);
|
|
750
|
-
} else {
|
|
751
|
-
throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
|
|
752
1046
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1047
|
+
throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
|
|
1048
|
+
}
|
|
1049
|
+
makeIncludeField(qualifier, path2, required, pos) {
|
|
1050
|
+
return { key: [], value: { kind: "include", qualifier, path: path2, required, pos }, append: false, pos };
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Skip past include-syntax wrapper tokens to find the quoted path string.
|
|
1054
|
+
* Strict: only allows `(`, `)`, and bare qualifier keywords (file / file( /
|
|
1055
|
+
* package / package( / url / url( / classpath / classpath() between the
|
|
1056
|
+
* directive and the quoted path. Anything else is a parse error rather than
|
|
1057
|
+
* silently swallowed (see ts#113).
|
|
1058
|
+
*/
|
|
1059
|
+
parseQuotedPathSkipWrapper(errTok) {
|
|
1060
|
+
while (this.isIncludeWrapperToken(this.peek())) this.advance();
|
|
1061
|
+
if (this.peek().kind !== "string") {
|
|
1062
|
+
const tok = this.peek();
|
|
1063
|
+
if (tok.kind === "eof") throw new ParseError("expected include path", errTok.line, errTok.col);
|
|
1064
|
+
const desc = tok.kind === "unquoted" ? `unquoted "${tok.value}"` : tok.kind;
|
|
1065
|
+
throw new ParseError(`expected quoted include path, got ${desc}`, tok.line, tok.col);
|
|
1066
|
+
}
|
|
1067
|
+
const path2 = this.advance().value;
|
|
1068
|
+
while (this.peek().kind === "unquoted" && (this.peek().value === ")" || this.peek().value === "))")) {
|
|
1069
|
+
this.advance();
|
|
1070
|
+
}
|
|
1071
|
+
const after = this.peek();
|
|
1072
|
+
if (after.kind !== "newline" && after.kind !== "rbrace" && after.kind !== "eof" && after.kind !== "comma") {
|
|
1073
|
+
const desc = after.kind === "unquoted" ? `unquoted "${after.value}"` : after.kind;
|
|
1074
|
+
throw new ParseError(`unexpected token after include path: ${desc}`, after.line, after.col);
|
|
1075
|
+
}
|
|
1076
|
+
return path2;
|
|
1077
|
+
}
|
|
1078
|
+
isIncludeWrapperToken(tok) {
|
|
1079
|
+
return tok.kind === "unquoted" && tok.value === "(";
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Parse the body of `package("identifier", "file")`.
|
|
1083
|
+
*
|
|
1084
|
+
* @param consumePackageToken - When true (default), the current token is `package(` and
|
|
1085
|
+
* must be consumed first. When false, the lexer already bundled `package(` into the
|
|
1086
|
+
* preceding token (e.g. `required(package("`), so we are already positioned at the
|
|
1087
|
+
* first string argument.
|
|
1088
|
+
*/
|
|
1089
|
+
parsePackageArgs(consumePackageToken = true) {
|
|
1090
|
+
const pkgTok = consumePackageToken ? this.advance() : this.peek();
|
|
1091
|
+
this.skip("newline");
|
|
1092
|
+
const idTok = this.peek();
|
|
1093
|
+
if (idTok.kind !== "string") {
|
|
1094
|
+
throw new ParseError("include package: expected quoted identifier as first argument", pkgTok.line, pkgTok.col);
|
|
1095
|
+
}
|
|
1096
|
+
const identifier = this.advance().value;
|
|
1097
|
+
this.skip("newline");
|
|
1098
|
+
if (this.peek().kind !== "comma") {
|
|
1099
|
+
const after = this.peek();
|
|
1100
|
+
throw new ParseError(
|
|
1101
|
+
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",
|
|
1102
|
+
pkgTok.line,
|
|
1103
|
+
pkgTok.col
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
this.advance();
|
|
1107
|
+
this.skip("newline");
|
|
1108
|
+
const fileTok = this.peek();
|
|
1109
|
+
if (fileTok.kind !== "string") {
|
|
1110
|
+
throw new ParseError(
|
|
1111
|
+
"include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
|
|
1112
|
+
pkgTok.line,
|
|
1113
|
+
pkgTok.col
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const filePath = this.advance().value;
|
|
1117
|
+
while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") {
|
|
1118
|
+
this.advance();
|
|
1119
|
+
}
|
|
1120
|
+
return { qualifier: { kind: "package", identifier }, path: filePath };
|
|
759
1121
|
}
|
|
760
1122
|
parseValue() {
|
|
761
1123
|
const p = this.currentPos();
|
|
@@ -777,7 +1139,7 @@ var Parser = class {
|
|
|
777
1139
|
throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
|
|
778
1140
|
}
|
|
779
1141
|
const payload = t.subst;
|
|
780
|
-
node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
|
|
1142
|
+
node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
|
|
781
1143
|
} else if (t.kind === "string" || t.kind === "triple_string") {
|
|
782
1144
|
this.advance();
|
|
783
1145
|
node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
|
|
@@ -796,7 +1158,9 @@ var Parser = class {
|
|
|
796
1158
|
parts.push(node);
|
|
797
1159
|
}
|
|
798
1160
|
if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
|
|
799
|
-
if (parts.length === 1)
|
|
1161
|
+
if (parts.length === 1) {
|
|
1162
|
+
return parts[0];
|
|
1163
|
+
}
|
|
800
1164
|
return { kind: "concat", nodes: parts, pos: p };
|
|
801
1165
|
}
|
|
802
1166
|
parseArray() {
|
|
@@ -828,107 +1192,10 @@ function parseTokens(tokens) {
|
|
|
828
1192
|
return new Parser(tokens).parse();
|
|
829
1193
|
}
|
|
830
1194
|
|
|
831
|
-
// src/internal/resolver/types.ts
|
|
832
|
-
var separatorValues = /* @__PURE__ */ new WeakSet();
|
|
833
|
-
function isSubst(v) {
|
|
834
|
-
return v._kind === "subst-placeholder";
|
|
835
|
-
}
|
|
836
|
-
function isConcat(v) {
|
|
837
|
-
return v._kind === "concat-placeholder";
|
|
838
|
-
}
|
|
839
|
-
function isAppend(v) {
|
|
840
|
-
return v._kind === "append-placeholder";
|
|
841
|
-
}
|
|
842
|
-
function isResObj(v) {
|
|
843
|
-
return v._kind === "res-obj";
|
|
844
|
-
}
|
|
845
|
-
function makeResObj() {
|
|
846
|
-
return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// src/internal/resolver/utils.ts
|
|
850
|
-
function segmentsToKey(segments) {
|
|
851
|
-
return segments.map((s) => {
|
|
852
|
-
const t = s.text;
|
|
853
|
-
if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
|
|
854
|
-
return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
855
|
-
}
|
|
856
|
-
return t;
|
|
857
|
-
}).join(".");
|
|
858
|
-
}
|
|
859
|
-
function lookupPath(root, segments) {
|
|
860
|
-
const [head, ...tail] = segments;
|
|
861
|
-
if (head === void 0) return void 0;
|
|
862
|
-
const key = head.text;
|
|
863
|
-
const val = root.fields.get(key);
|
|
864
|
-
if (val === void 0) return void 0;
|
|
865
|
-
if (tail.length === 0) return val;
|
|
866
|
-
if (isResObj(val)) return lookupPath(val, tail);
|
|
867
|
-
return void 0;
|
|
868
|
-
}
|
|
869
|
-
function lookupResObj(root, segments) {
|
|
870
|
-
let cur = root;
|
|
871
|
-
for (const seg of segments) {
|
|
872
|
-
const val = cur.fields.get(seg.text);
|
|
873
|
-
if (val === void 0 || !isResObj(val)) return void 0;
|
|
874
|
-
cur = val;
|
|
875
|
-
}
|
|
876
|
-
return cur;
|
|
877
|
-
}
|
|
878
|
-
function deepMergeHoconValues(base, overlay) {
|
|
879
|
-
const merged = new Map(base.fields);
|
|
880
|
-
for (const [k, v] of overlay.fields) {
|
|
881
|
-
const existing = merged.get(k);
|
|
882
|
-
if (existing?.kind === "object" && v.kind === "object") {
|
|
883
|
-
merged.set(k, deepMergeHoconValues(existing, v));
|
|
884
|
-
} else {
|
|
885
|
-
merged.set(k, v);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
return { kind: "object", fields: merged };
|
|
889
|
-
}
|
|
890
|
-
function deepMergeResObjInto(dst, src) {
|
|
891
|
-
for (const [k, srcVal] of src.fields) {
|
|
892
|
-
const dstVal = dst.fields.get(k);
|
|
893
|
-
if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
|
|
894
|
-
deepMergeResObjInto(dstVal, srcVal);
|
|
895
|
-
} else {
|
|
896
|
-
if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
|
|
897
|
-
dst.fields.set(k, srcVal);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
for (const [k, srcPrior] of src.priorValues) {
|
|
901
|
-
if (!dst.priorValues.has(k)) {
|
|
902
|
-
dst.priorValues.set(k, srcPrior);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
function hoconValueToResObj(hv) {
|
|
907
|
-
const obj = makeResObj();
|
|
908
|
-
if (hv.kind !== "object") return obj;
|
|
909
|
-
for (const [key, val] of hv.fields) {
|
|
910
|
-
if (val.kind === "object") {
|
|
911
|
-
obj.fields.set(key, hoconValueToResObj(val));
|
|
912
|
-
} else {
|
|
913
|
-
obj.fields.set(key, val);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
return obj;
|
|
917
|
-
}
|
|
918
|
-
function isFileNotFoundError(e) {
|
|
919
|
-
if (!(e instanceof Error)) return false;
|
|
920
|
-
const code = e.code;
|
|
921
|
-
if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
|
|
922
|
-
const msg = e.message.toLowerCase();
|
|
923
|
-
return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// src/internal/resolver/include-loader.ts
|
|
927
|
-
var nodePath = __toESM(require("path"), 1);
|
|
928
|
-
|
|
929
1195
|
// src/internal/properties/properties.ts
|
|
930
1196
|
function parseProperties(input) {
|
|
931
1197
|
const root = /* @__PURE__ */ Object.create(null);
|
|
1198
|
+
const pairs = [];
|
|
932
1199
|
for (const line of input.split("\n")) {
|
|
933
1200
|
const trimmed = line.trim();
|
|
934
1201
|
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
@@ -937,6 +1204,10 @@ function parseProperties(input) {
|
|
|
937
1204
|
const key = trimmed.slice(0, sepIdx).trim();
|
|
938
1205
|
const value = trimmed.slice(sepIdx + 1).trim();
|
|
939
1206
|
if (key === "") continue;
|
|
1207
|
+
pairs.push([key, value]);
|
|
1208
|
+
}
|
|
1209
|
+
pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1210
|
+
for (const [key, value] of pairs) {
|
|
940
1211
|
setNested(root, key.split("."), value);
|
|
941
1212
|
}
|
|
942
1213
|
return root;
|
|
@@ -960,6 +1231,7 @@ function setNested(obj, segments, value) {
|
|
|
960
1231
|
}
|
|
961
1232
|
const last = segments[segments.length - 1];
|
|
962
1233
|
if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
|
|
1234
|
+
if (typeof current[last] === "object" && current[last] !== null) return;
|
|
963
1235
|
current[last] = value;
|
|
964
1236
|
}
|
|
965
1237
|
function propertiesToHoconValue(input) {
|
|
@@ -979,6 +1251,85 @@ function recordToHoconValue(obj) {
|
|
|
979
1251
|
}
|
|
980
1252
|
|
|
981
1253
|
// src/internal/resolver/include-loader.ts
|
|
1254
|
+
var import_meta = {};
|
|
1255
|
+
var _require = (0, import_node_module.createRequire)(import_meta.url);
|
|
1256
|
+
function isPnpPath(p) {
|
|
1257
|
+
return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
|
|
1258
|
+
}
|
|
1259
|
+
function validatePackageFile(file, identifier) {
|
|
1260
|
+
if (file.length === 0) {
|
|
1261
|
+
throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
|
|
1262
|
+
}
|
|
1263
|
+
if (file.includes("\\")) {
|
|
1264
|
+
throw new ParseError(
|
|
1265
|
+
`include package("${identifier}", "${file}"): backslash not allowed in file argument (use forward slash)`,
|
|
1266
|
+
0,
|
|
1267
|
+
0
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
if (file.startsWith("/")) {
|
|
1271
|
+
throw new ParseError(
|
|
1272
|
+
`include package("${identifier}", "${file}"): absolute path not allowed in file argument`,
|
|
1273
|
+
0,
|
|
1274
|
+
0
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (/(?:^|\/)\.\.?(?:\/|$)/u.test(file)) {
|
|
1278
|
+
throw new ParseError(
|
|
1279
|
+
`include package("${identifier}", "${file}"): path traversal (. or ..) not allowed in file argument`,
|
|
1280
|
+
0,
|
|
1281
|
+
0
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
if (file.includes("//")) {
|
|
1285
|
+
throw new ParseError(
|
|
1286
|
+
`include package("${identifier}", "${file}"): consecutive slashes not allowed in file argument`,
|
|
1287
|
+
0,
|
|
1288
|
+
0
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function validatePackageIdentifier(identifier) {
|
|
1293
|
+
if (identifier.length === 0) {
|
|
1294
|
+
throw new ParseError("include package: identifier argument must be non-empty", 0, 0);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function defaultPackageResolver(identifier, file, includingFile, baseDir, resolveFrom) {
|
|
1298
|
+
const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
|
|
1299
|
+
let resolved;
|
|
1300
|
+
try {
|
|
1301
|
+
resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
|
|
1302
|
+
} catch {
|
|
1303
|
+
throw new PackageLookupError(
|
|
1304
|
+
`include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
|
|
1305
|
+
identifier,
|
|
1306
|
+
file,
|
|
1307
|
+
0,
|
|
1308
|
+
0
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
if (isPnpPath(resolved)) {
|
|
1312
|
+
throw new ResolveError(
|
|
1313
|
+
`include package("${identifier}", "${file}"): Yarn Berry PnP store paths are not supported. Provide a custom packageResolver option to handle PnP resolution.`,
|
|
1314
|
+
`${identifier}/${file}`,
|
|
1315
|
+
0,
|
|
1316
|
+
0
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const fileParts = file.split("/");
|
|
1320
|
+
const expectedBasename = fileParts[fileParts.length - 1] ?? file;
|
|
1321
|
+
const resolvedBasename = nodePath.basename(resolved);
|
|
1322
|
+
if (resolvedBasename !== expectedBasename) {
|
|
1323
|
+
throw new PackageLookupError(
|
|
1324
|
+
`include package("${identifier}", "${file}"): case mismatch \u2014 resolved "${resolvedBasename}" but expected "${expectedBasename}" (E11 decision 5: case-sensitive)`,
|
|
1325
|
+
identifier,
|
|
1326
|
+
file,
|
|
1327
|
+
0,
|
|
1328
|
+
0
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
return resolved;
|
|
1332
|
+
}
|
|
982
1333
|
var IncludeLoader = class {
|
|
983
1334
|
constructor(opts) {
|
|
984
1335
|
this.opts = opts;
|
|
@@ -1058,8 +1409,77 @@ var IncludeLoader = class {
|
|
|
1058
1409
|
}
|
|
1059
1410
|
return merged;
|
|
1060
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Resolve and load `include package("identifier", "file")`.
|
|
1414
|
+
* Validates the identifier and file arguments, resolves via the package resolver,
|
|
1415
|
+
* performs cycle detection using a JSON-serialised cycle key, and parses the content.
|
|
1416
|
+
*/
|
|
1417
|
+
// `required` is reserved for future optional-package semantics (E11 decision 7);
|
|
1418
|
+
// currently all package lookup failures throw regardless of this flag.
|
|
1419
|
+
loadPackage(identifier, file, _required) {
|
|
1420
|
+
validatePackageIdentifier(identifier);
|
|
1421
|
+
validatePackageFile(file, identifier);
|
|
1422
|
+
const { includeStack = [], baseDir, resolveFrom, packageResolver, readFileSync: readFileSync2 } = this.opts;
|
|
1423
|
+
const cycleKey = JSON.stringify(["package", identifier, file]);
|
|
1424
|
+
if (includeStack.includes(cycleKey)) {
|
|
1425
|
+
throw new ResolveError(
|
|
1426
|
+
`circular include: package("${identifier}", "${file}")`,
|
|
1427
|
+
`${identifier}/${file}`,
|
|
1428
|
+
0,
|
|
1429
|
+
0
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
if (includeStack.length >= 50) {
|
|
1433
|
+
throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
|
|
1434
|
+
}
|
|
1435
|
+
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1436
|
+
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1437
|
+
const content = readFileSync2(resolvedPath);
|
|
1438
|
+
if (content.length === 0) return makeResObj();
|
|
1439
|
+
const tokens = tokenize(content);
|
|
1440
|
+
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1441
|
+
const ast = parseTokens(tokens);
|
|
1442
|
+
return this.onBuildResObj(ast, {
|
|
1443
|
+
...this.opts,
|
|
1444
|
+
baseDir: nodePath.dirname(resolvedPath),
|
|
1445
|
+
includeStack: [...includeStack, cycleKey]
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Async variant of `loadPackage`.
|
|
1450
|
+
*/
|
|
1451
|
+
async loadPackageAsync(identifier, file, _required) {
|
|
1452
|
+
validatePackageIdentifier(identifier);
|
|
1453
|
+
validatePackageFile(file, identifier);
|
|
1454
|
+
const { includeStack = [], baseDir, resolveFrom, packageResolver, readFile, readFileSync: readFileSync2 } = this.opts;
|
|
1455
|
+
const cycleKey = JSON.stringify(["package", identifier, file]);
|
|
1456
|
+
if (includeStack.includes(cycleKey)) {
|
|
1457
|
+
throw new ResolveError(
|
|
1458
|
+
`circular include: package("${identifier}", "${file}")`,
|
|
1459
|
+
`${identifier}/${file}`,
|
|
1460
|
+
0,
|
|
1461
|
+
0
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
if (includeStack.length >= 50) {
|
|
1465
|
+
throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
|
|
1466
|
+
}
|
|
1467
|
+
const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
|
|
1468
|
+
const resolvedPath = resolver(identifier, file, void 0, baseDir);
|
|
1469
|
+
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1470
|
+
const content = await read(resolvedPath);
|
|
1471
|
+
if (content.length === 0) return makeResObj();
|
|
1472
|
+
const tokens = tokenize(content);
|
|
1473
|
+
assertNonEmptyDocument(tokens, resolvedPath);
|
|
1474
|
+
const ast = parseTokens(tokens);
|
|
1475
|
+
return this.onBuildResObjAsync(ast, {
|
|
1476
|
+
...this.opts,
|
|
1477
|
+
baseDir: nodePath.dirname(resolvedPath),
|
|
1478
|
+
includeStack: [...includeStack, cycleKey]
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1061
1481
|
loadSingle(candidate) {
|
|
1062
|
-
const { readFileSync: readFileSync2, includeStack = []
|
|
1482
|
+
const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
|
|
1063
1483
|
if (includeStack.includes(candidate)) {
|
|
1064
1484
|
throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
|
|
1065
1485
|
}
|
|
@@ -1073,16 +1493,17 @@ var IncludeLoader = class {
|
|
|
1073
1493
|
if (candidate.endsWith(".properties")) {
|
|
1074
1494
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1075
1495
|
}
|
|
1076
|
-
const
|
|
1496
|
+
const tokens = tokenize(content);
|
|
1497
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1498
|
+
const ast = parseTokens(tokens);
|
|
1077
1499
|
return this.onBuildResObj(ast, {
|
|
1078
|
-
|
|
1500
|
+
...this.opts,
|
|
1079
1501
|
baseDir: nodePath.dirname(candidate),
|
|
1080
|
-
readFileSync: readFileSync2,
|
|
1081
1502
|
includeStack: [...includeStack, candidate]
|
|
1082
1503
|
});
|
|
1083
1504
|
}
|
|
1084
1505
|
async loadSingleAsync(candidate) {
|
|
1085
|
-
const { readFile, readFileSync: readFileSync2, includeStack = []
|
|
1506
|
+
const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
|
|
1086
1507
|
const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
|
|
1087
1508
|
if (includeStack.includes(candidate)) {
|
|
1088
1509
|
throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
|
|
@@ -1097,12 +1518,12 @@ var IncludeLoader = class {
|
|
|
1097
1518
|
if (candidate.endsWith(".properties")) {
|
|
1098
1519
|
return hoconValueToResObj(propertiesToHoconValue(content));
|
|
1099
1520
|
}
|
|
1100
|
-
const
|
|
1521
|
+
const tokens = tokenize(content);
|
|
1522
|
+
assertNonEmptyDocument(tokens, candidate);
|
|
1523
|
+
const ast = parseTokens(tokens);
|
|
1101
1524
|
return this.onBuildResObjAsync(ast, {
|
|
1102
|
-
|
|
1525
|
+
...this.opts,
|
|
1103
1526
|
baseDir: nodePath.dirname(candidate),
|
|
1104
|
-
readFileSync: readFileSync2,
|
|
1105
|
-
readFile,
|
|
1106
1527
|
includeStack: [...includeStack, candidate]
|
|
1107
1528
|
});
|
|
1108
1529
|
}
|
|
@@ -1111,7 +1532,6 @@ var IncludeLoader = class {
|
|
|
1111
1532
|
// src/internal/resolver/structure-builder.ts
|
|
1112
1533
|
var StructureBuilder = class _StructureBuilder {
|
|
1113
1534
|
constructor(opts) {
|
|
1114
|
-
this.opts = opts;
|
|
1115
1535
|
this.loader = new IncludeLoader(opts);
|
|
1116
1536
|
this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
|
|
1117
1537
|
this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
|
|
@@ -1138,7 +1558,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1138
1558
|
}
|
|
1139
1559
|
applyField(obj, field, pathPrefix) {
|
|
1140
1560
|
if (field.key.length === 0 && field.value.kind === "include") {
|
|
1141
|
-
const
|
|
1561
|
+
const { qualifier, path: path2, required } = field.value;
|
|
1562
|
+
let included;
|
|
1563
|
+
if (qualifier.kind === "package") {
|
|
1564
|
+
included = this.loader.loadPackage(qualifier.identifier, path2, required);
|
|
1565
|
+
} else {
|
|
1566
|
+
included = this.loader.load(path2, required, qualifier.kind === "file");
|
|
1567
|
+
}
|
|
1142
1568
|
if (pathPrefix.length > 0) {
|
|
1143
1569
|
this.relativizeResObj(included, pathPrefix);
|
|
1144
1570
|
}
|
|
@@ -1180,7 +1606,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1180
1606
|
}
|
|
1181
1607
|
async applyFieldAsync(obj, field, pathPrefix) {
|
|
1182
1608
|
if (field.key.length === 0 && field.value.kind === "include") {
|
|
1183
|
-
const
|
|
1609
|
+
const { qualifier, path: path2, required } = field.value;
|
|
1610
|
+
let included;
|
|
1611
|
+
if (qualifier.kind === "package") {
|
|
1612
|
+
included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
|
|
1613
|
+
} else {
|
|
1614
|
+
included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
|
|
1615
|
+
}
|
|
1184
1616
|
if (pathPrefix.length > 0) {
|
|
1185
1617
|
this.relativizeResObj(included, pathPrefix);
|
|
1186
1618
|
}
|
|
@@ -1234,9 +1666,9 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1234
1666
|
return inner;
|
|
1235
1667
|
}
|
|
1236
1668
|
case "subst":
|
|
1237
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1669
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1238
1670
|
case "concat":
|
|
1239
|
-
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
|
|
1671
|
+
return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
|
|
1240
1672
|
case "include":
|
|
1241
1673
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
1242
1674
|
}
|
|
@@ -1258,13 +1690,13 @@ var StructureBuilder = class _StructureBuilder {
|
|
|
1258
1690
|
case "object":
|
|
1259
1691
|
return await this.buildAsync(ast, pathPrefix);
|
|
1260
1692
|
case "subst":
|
|
1261
|
-
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1693
|
+
return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
|
|
1262
1694
|
case "concat": {
|
|
1263
1695
|
const nodes = [];
|
|
1264
1696
|
for (const n of ast.nodes) {
|
|
1265
1697
|
nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
|
|
1266
1698
|
}
|
|
1267
|
-
return { _kind: "concat-placeholder", nodes };
|
|
1699
|
+
return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
|
|
1268
1700
|
}
|
|
1269
1701
|
case "include":
|
|
1270
1702
|
return { kind: "scalar", raw: "null", valueType: "null" };
|
|
@@ -1334,6 +1766,23 @@ var SubstitutionResolver = class {
|
|
|
1334
1766
|
this.opts = opts;
|
|
1335
1767
|
this.resolving = /* @__PURE__ */ new Set();
|
|
1336
1768
|
this.cache = /* @__PURE__ */ new Map();
|
|
1769
|
+
// Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
|
|
1770
|
+
// Used by isSelfRef detection: fires only when found === a concat currently
|
|
1771
|
+
// mid-iteration (i.e. we are inside found's own resolution), preventing the
|
|
1772
|
+
// "foofoo" double-count without misfiring when an external field looks up the
|
|
1773
|
+
// same concat via a substitution (multi-reviewer convergence fix).
|
|
1774
|
+
//
|
|
1775
|
+
// Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
|
|
1776
|
+
// preservation for self-ref detection. Round-2 multi-agent-review surfaced a
|
|
1777
|
+
// false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
|
|
1778
|
+
// criterion was tightened to node-membership-in-iterating-set — strictly
|
|
1779
|
+
// narrower than path-equality. Spec amendment deferred to a follow-up
|
|
1780
|
+
// xx.hocon PR (see Phase 6 #3f close-out notes).
|
|
1781
|
+
this.resolvingConcats = /* @__PURE__ */ new WeakSet();
|
|
1782
|
+
}
|
|
1783
|
+
/** Returns a prefix for error messages, including origin info when set. */
|
|
1784
|
+
originPrefix() {
|
|
1785
|
+
return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
|
|
1337
1786
|
}
|
|
1338
1787
|
resolve() {
|
|
1339
1788
|
return this.resolveResObj(this.root);
|
|
@@ -1372,7 +1821,15 @@ var SubstitutionResolver = class {
|
|
|
1372
1821
|
}
|
|
1373
1822
|
resolveVal(v, scope) {
|
|
1374
1823
|
if (isSubst(v)) return this.resolveSubst(v, scope);
|
|
1375
|
-
if (isConcat(v))
|
|
1824
|
+
if (isConcat(v)) {
|
|
1825
|
+
this.resolvingConcats.add(v);
|
|
1826
|
+
try {
|
|
1827
|
+
const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
|
|
1828
|
+
return concatResult;
|
|
1829
|
+
} finally {
|
|
1830
|
+
this.resolvingConcats.delete(v);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1376
1833
|
if (isAppend(v)) return this.resolveAppend(v, scope);
|
|
1377
1834
|
if (isResObj(v)) return this.resolveResObj(v);
|
|
1378
1835
|
const hv = v;
|
|
@@ -1386,15 +1843,12 @@ var SubstitutionResolver = class {
|
|
|
1386
1843
|
}
|
|
1387
1844
|
return hv;
|
|
1388
1845
|
}
|
|
1389
|
-
segmentsEqual(a, b) {
|
|
1390
|
-
return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
|
|
1391
|
-
}
|
|
1392
1846
|
resolveSubst(s, scope) {
|
|
1393
|
-
const key = segmentsToKey(s.segments);
|
|
1847
|
+
const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
|
|
1394
1848
|
if (this.cache.has(key)) return this.cache.get(key);
|
|
1395
1849
|
if (this.resolving.has(key)) {
|
|
1396
1850
|
let prior;
|
|
1397
|
-
if (s.
|
|
1851
|
+
if (s.segments.length > 1) {
|
|
1398
1852
|
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1399
1853
|
const parentScope = lookupResObj(
|
|
1400
1854
|
this.root,
|
|
@@ -1416,7 +1870,7 @@ var SubstitutionResolver = class {
|
|
|
1416
1870
|
}
|
|
1417
1871
|
if (s.optional) return void 0;
|
|
1418
1872
|
throw new ResolveError(
|
|
1419
|
-
`circular substitution: ${key}`,
|
|
1873
|
+
this.originPrefix() + `circular substitution: ${key}`,
|
|
1420
1874
|
key,
|
|
1421
1875
|
s.line,
|
|
1422
1876
|
s.col
|
|
@@ -1426,29 +1880,31 @@ var SubstitutionResolver = class {
|
|
|
1426
1880
|
try {
|
|
1427
1881
|
const found = lookupPath(this.root, s.segments);
|
|
1428
1882
|
if (found !== void 0) {
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (prior !== void 0) {
|
|
1447
|
-
const result2 = this.resolveVal(prior, scope);
|
|
1448
|
-
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1449
|
-
return result2;
|
|
1450
|
-
}
|
|
1883
|
+
if (isConcat(found) && this.resolvingConcats.has(found)) {
|
|
1884
|
+
let prior;
|
|
1885
|
+
if (s.segments.length > 1) {
|
|
1886
|
+
const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
|
|
1887
|
+
const parentScope = lookupResObj(
|
|
1888
|
+
this.root,
|
|
1889
|
+
s.segments.slice(0, s.segments.length - 1)
|
|
1890
|
+
);
|
|
1891
|
+
prior = parentScope?.priorValues.get(leafSeg);
|
|
1892
|
+
} else {
|
|
1893
|
+
const rootSeg = s.segments[0]?.text ?? "";
|
|
1894
|
+
prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
|
|
1895
|
+
}
|
|
1896
|
+
if (prior !== void 0) {
|
|
1897
|
+
const result2 = this.resolveVal(prior, scope);
|
|
1898
|
+
if (result2 !== void 0) this.cache.set(key, result2);
|
|
1899
|
+
return result2;
|
|
1451
1900
|
}
|
|
1901
|
+
if (s.optional) return void 0;
|
|
1902
|
+
throw new ResolveError(
|
|
1903
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}}`,
|
|
1904
|
+
key,
|
|
1905
|
+
s.line,
|
|
1906
|
+
s.col
|
|
1907
|
+
);
|
|
1452
1908
|
}
|
|
1453
1909
|
let result = this.resolveVal(found, scope);
|
|
1454
1910
|
const effectiveLen = s.segments.length - s.prefixLen;
|
|
@@ -1477,16 +1933,45 @@ var SubstitutionResolver = class {
|
|
|
1477
1933
|
if (result !== void 0) this.cache.set(key, result);
|
|
1478
1934
|
return result;
|
|
1479
1935
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1936
|
+
if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
|
|
1937
|
+
const result = this.resolveEnvList(s);
|
|
1938
|
+
if (result !== void 0) {
|
|
1939
|
+
this.cache.set(key, result);
|
|
1940
|
+
return result;
|
|
1941
|
+
}
|
|
1942
|
+
if (s.optional) return void 0;
|
|
1943
|
+
const envBase = s.segments.map((seg) => seg.text).join(".");
|
|
1944
|
+
throw new ResolveError(
|
|
1945
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
|
|
1946
|
+
key,
|
|
1947
|
+
s.line,
|
|
1948
|
+
s.col
|
|
1949
|
+
);
|
|
1950
|
+
} else if (s.listSuffix) {
|
|
1951
|
+
if (s.optional) return void 0;
|
|
1952
|
+
if (this.opts.allowUnresolved) return s;
|
|
1953
|
+
throw new ResolveError(
|
|
1954
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}}`,
|
|
1955
|
+
key,
|
|
1956
|
+
s.line,
|
|
1957
|
+
s.col
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
if (this.opts.useSystemEnvironment !== false) {
|
|
1961
|
+
const envKey = s.segments.map((seg) => seg.text).join(".");
|
|
1962
|
+
const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
|
|
1963
|
+
if (envVal !== void 0) {
|
|
1964
|
+
const result = { kind: "scalar", raw: envVal, valueType: "string" };
|
|
1965
|
+
this.cache.set(key, result);
|
|
1966
|
+
return result;
|
|
1967
|
+
}
|
|
1486
1968
|
}
|
|
1487
1969
|
if (s.optional) return void 0;
|
|
1970
|
+
if (this.opts.allowUnresolved) {
|
|
1971
|
+
return s;
|
|
1972
|
+
}
|
|
1488
1973
|
throw new ResolveError(
|
|
1489
|
-
|
|
1974
|
+
`${this.originPrefix()}could not resolve substitution: \${${key}}`,
|
|
1490
1975
|
key,
|
|
1491
1976
|
s.line,
|
|
1492
1977
|
s.col
|
|
@@ -1495,9 +1980,52 @@ var SubstitutionResolver = class {
|
|
|
1495
1980
|
this.resolving.delete(key);
|
|
1496
1981
|
}
|
|
1497
1982
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1983
|
+
/**
|
|
1984
|
+
* S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
|
|
1985
|
+
*
|
|
1986
|
+
* Candidate bases follow the same fully-qualified→bare order used by the scalar
|
|
1987
|
+
* env fallback. The first base whose _0 key is present wins entirely — no
|
|
1988
|
+
* cross-base merging (spec §Relativized fallback precedence rules).
|
|
1989
|
+
*
|
|
1990
|
+
* Returns the HoconValue Array if any candidate base has _0, or undefined if
|
|
1991
|
+
* none do (caller applies optional/required handling).
|
|
1992
|
+
*/
|
|
1993
|
+
resolveEnvList(s) {
|
|
1994
|
+
const bases = this.candidateBases(s);
|
|
1995
|
+
for (const base of bases) {
|
|
1996
|
+
const key0 = base + "_0";
|
|
1997
|
+
if (!(key0 in this.opts.env)) continue;
|
|
1998
|
+
const items = [];
|
|
1999
|
+
for (let i = 0; ; i++) {
|
|
2000
|
+
const k = base + "_" + i;
|
|
2001
|
+
if (!(k in this.opts.env)) break;
|
|
2002
|
+
items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
|
|
2003
|
+
}
|
|
2004
|
+
return { kind: "array", items };
|
|
2005
|
+
}
|
|
2006
|
+
return void 0;
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Return the candidate base names for env-var-list lookup, in precedence order.
|
|
2010
|
+
* Mirrors the fully-qualified→bare fallback used by the scalar env path.
|
|
2011
|
+
*/
|
|
2012
|
+
candidateBases(s) {
|
|
2013
|
+
const full = s.segments.map((seg) => seg.text).join(".");
|
|
2014
|
+
if (s.prefixLen === 0) return [full];
|
|
2015
|
+
const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
|
|
2016
|
+
return [full, bare];
|
|
2017
|
+
}
|
|
2018
|
+
resolveConcat(nodes, scope, line = 0, col = 0) {
|
|
2019
|
+
const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
|
|
2020
|
+
if (this.opts.allowUnresolved) {
|
|
2021
|
+
for (const v of rawResolved) {
|
|
2022
|
+
if (v !== void 0 && isSubst(v)) {
|
|
2023
|
+
return v;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
const resolved = rawResolved.filter((v) => v !== void 0);
|
|
2028
|
+
if (resolved.length === 0) return void 0;
|
|
1501
2029
|
if (resolved.length === 1) return resolved[0];
|
|
1502
2030
|
const nonSep = resolved.filter((v) => !separatorValues.has(v));
|
|
1503
2031
|
if (nonSep.length === 0) {
|
|
@@ -1513,27 +2041,45 @@ var SubstitutionResolver = class {
|
|
|
1513
2041
|
if (converted !== null) {
|
|
1514
2042
|
return { kind: "array", items: [...left.items, ...converted] };
|
|
1515
2043
|
}
|
|
1516
|
-
|
|
2044
|
+
throw new ResolveError(
|
|
2045
|
+
"cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
|
|
2046
|
+
"",
|
|
2047
|
+
line,
|
|
2048
|
+
col
|
|
2049
|
+
);
|
|
1517
2050
|
}
|
|
1518
2051
|
if (left.kind === "object" && right.kind === "array") {
|
|
1519
2052
|
const converted = numericObjectToArray(left);
|
|
1520
2053
|
if (converted !== null) {
|
|
1521
2054
|
return { kind: "array", items: [...converted, ...right.items] };
|
|
1522
2055
|
}
|
|
1523
|
-
|
|
2056
|
+
throw new ResolveError(
|
|
2057
|
+
"cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
|
|
2058
|
+
"",
|
|
2059
|
+
line,
|
|
2060
|
+
col
|
|
2061
|
+
);
|
|
1524
2062
|
}
|
|
1525
2063
|
if (left.kind === "array" && right.kind === "array") {
|
|
1526
2064
|
return { kind: "array", items: [...left.items, ...right.items] };
|
|
1527
2065
|
}
|
|
1528
|
-
if (left.kind === "array") {
|
|
1529
|
-
|
|
2066
|
+
if (left.kind === "array" || right.kind === "array") {
|
|
2067
|
+
throw new ResolveError(
|
|
2068
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
2069
|
+
"",
|
|
2070
|
+
line,
|
|
2071
|
+
col
|
|
2072
|
+
);
|
|
1530
2073
|
}
|
|
1531
|
-
if (right.kind === "
|
|
1532
|
-
|
|
2074
|
+
if (left.kind === "object" || right.kind === "object") {
|
|
2075
|
+
throw new ResolveError(
|
|
2076
|
+
`cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
|
|
2077
|
+
"",
|
|
2078
|
+
line,
|
|
2079
|
+
col
|
|
2080
|
+
);
|
|
1533
2081
|
}
|
|
1534
|
-
|
|
1535
|
-
const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
|
|
1536
|
-
return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
|
|
2082
|
+
return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
|
|
1537
2083
|
};
|
|
1538
2084
|
const [head, ...tail] = nonSep;
|
|
1539
2085
|
const folded = tail.reduce(joinPair, head);
|
|
@@ -1553,15 +2099,89 @@ var SubstitutionResolver = class {
|
|
|
1553
2099
|
};
|
|
1554
2100
|
|
|
1555
2101
|
// src/internal/resolver/resolver.ts
|
|
2102
|
+
function buildTree(ast, opts) {
|
|
2103
|
+
return new StructureBuilder(opts).build(ast);
|
|
2104
|
+
}
|
|
2105
|
+
function resolveTree(tree, opts) {
|
|
2106
|
+
return new SubstitutionResolver(tree, opts).resolve();
|
|
2107
|
+
}
|
|
2108
|
+
function containsPlaceholders(tree) {
|
|
2109
|
+
for (const val of tree.fields.values()) {
|
|
2110
|
+
if (valContainsPlaceholders(val)) return true;
|
|
2111
|
+
}
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
function valContainsPlaceholders(v) {
|
|
2115
|
+
const rv = v;
|
|
2116
|
+
if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
|
|
2117
|
+
if (isResObj(rv)) return containsPlaceholders(rv);
|
|
2118
|
+
const hv = v;
|
|
2119
|
+
if (hv?.kind === "array") {
|
|
2120
|
+
return hv.items.some((item) => valContainsPlaceholders(item));
|
|
2121
|
+
}
|
|
2122
|
+
return false;
|
|
2123
|
+
}
|
|
2124
|
+
function buildPartialHoconFromResObj(tree) {
|
|
2125
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2126
|
+
for (const [k, v] of tree.fields) {
|
|
2127
|
+
if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
|
|
2128
|
+
if (isResObj(v)) {
|
|
2129
|
+
fields.set(k, buildPartialHoconFromResObj(v));
|
|
2130
|
+
} else {
|
|
2131
|
+
if (!valContainsPlaceholders(v)) {
|
|
2132
|
+
fields.set(k, v);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return { kind: "object", fields };
|
|
2138
|
+
}
|
|
2139
|
+
function hoconValueToResObj2(value) {
|
|
2140
|
+
const obj = makeResObj();
|
|
2141
|
+
for (const [k, v] of value.fields) {
|
|
2142
|
+
if (v.kind === "object") {
|
|
2143
|
+
obj.fields.set(k, hoconValueToResObj2(v));
|
|
2144
|
+
} else {
|
|
2145
|
+
obj.fields.set(k, v);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return obj;
|
|
2149
|
+
}
|
|
1556
2150
|
function resolve2(ast, opts) {
|
|
1557
|
-
const
|
|
1558
|
-
return
|
|
2151
|
+
const tree = buildTree(ast, opts);
|
|
2152
|
+
return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
|
|
1559
2153
|
}
|
|
1560
2154
|
|
|
1561
2155
|
// src/config.ts
|
|
1562
2156
|
var Config = class _Config {
|
|
1563
|
-
constructor(root) {
|
|
2157
|
+
constructor(root, opts) {
|
|
1564
2158
|
this.root = root;
|
|
2159
|
+
this._resolved = opts?.resolved ?? true;
|
|
2160
|
+
this._parseBaseDir = opts?.parseBaseDir;
|
|
2161
|
+
this._originDescription = opts?.originDescription;
|
|
2162
|
+
this._resObjRoot = opts?.resObjRoot;
|
|
2163
|
+
this._resolveOpts = opts?.resolveOpts;
|
|
2164
|
+
}
|
|
2165
|
+
/** Returns true when the Config's value tree contains no unresolved
|
|
2166
|
+
* substitution placeholders. Whole-config granularity (E12 decision 11). */
|
|
2167
|
+
isResolved() {
|
|
2168
|
+
return this._resolved;
|
|
2169
|
+
}
|
|
2170
|
+
/** @internal Test-only: construct a resolved Config from a HoconValue. */
|
|
2171
|
+
static _fromResolvedValue(root, opts) {
|
|
2172
|
+
return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
|
|
2173
|
+
}
|
|
2174
|
+
/** @internal Construct an unresolved Config carrying a ResObj tree.
|
|
2175
|
+
* Used by parseStringWithOptions (deferred path) in T6. */
|
|
2176
|
+
static _fromUnresolvedResObj(tree, opts) {
|
|
2177
|
+
const partialRoot = buildPartialHoconFromResObj(tree);
|
|
2178
|
+
return new _Config(partialRoot, {
|
|
2179
|
+
resolved: opts.resolved,
|
|
2180
|
+
parseBaseDir: opts.parseBaseDir,
|
|
2181
|
+
originDescription: opts.originDescription,
|
|
2182
|
+
resObjRoot: tree,
|
|
2183
|
+
resolveOpts: opts.resolveOpts
|
|
2184
|
+
});
|
|
1565
2185
|
}
|
|
1566
2186
|
get(path2) {
|
|
1567
2187
|
const v = this.lookupNode(path2);
|
|
@@ -1600,17 +2220,34 @@ var Config = class _Config {
|
|
|
1600
2220
|
}
|
|
1601
2221
|
const result = parseBytes(v.raw, unit);
|
|
1602
2222
|
if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
2223
|
+
if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
|
|
1603
2224
|
return result;
|
|
1604
2225
|
}
|
|
1605
2226
|
getConfig(path2) {
|
|
1606
2227
|
const v = this.lookupNode(path2);
|
|
1607
|
-
if (v === void 0)
|
|
2228
|
+
if (v === void 0) {
|
|
2229
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2230
|
+
throw new NotResolvedError(path2);
|
|
2231
|
+
}
|
|
2232
|
+
throw new ConfigError(`path not found: ${path2}`, path2);
|
|
2233
|
+
}
|
|
1608
2234
|
if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
|
|
1609
|
-
|
|
2235
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2236
|
+
throw new NotResolvedError(path2);
|
|
2237
|
+
}
|
|
2238
|
+
return new _Config(v, { resolved: this._resolved });
|
|
1610
2239
|
}
|
|
1611
2240
|
getList(path2) {
|
|
1612
2241
|
const v = this.lookupNode(path2);
|
|
1613
|
-
if (v === void 0)
|
|
2242
|
+
if (v === void 0) {
|
|
2243
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2244
|
+
throw new NotResolvedError(path2);
|
|
2245
|
+
}
|
|
2246
|
+
throw new ConfigError(`path not found: ${path2}`, path2);
|
|
2247
|
+
}
|
|
2248
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2249
|
+
throw new NotResolvedError(path2);
|
|
2250
|
+
}
|
|
1614
2251
|
if (v.kind === "object") {
|
|
1615
2252
|
const converted = numericObjectToArray(v);
|
|
1616
2253
|
if (converted !== null) return converted.map(hoconToJs);
|
|
@@ -1625,12 +2262,138 @@ var Config = class _Config {
|
|
|
1625
2262
|
return [...this.root.fields.keys()];
|
|
1626
2263
|
}
|
|
1627
2264
|
withFallback(fallback) {
|
|
1628
|
-
|
|
1629
|
-
|
|
2265
|
+
if (fallback == null) return this;
|
|
2266
|
+
const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
|
|
2267
|
+
const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
|
|
2268
|
+
const merged = mergeUnresolved(selfTree, fbTree);
|
|
2269
|
+
const hasPlaceholders = containsPlaceholders(merged);
|
|
2270
|
+
const partialRoot = buildPartialHoconFromResObj(merged);
|
|
2271
|
+
return new _Config(partialRoot, {
|
|
2272
|
+
resolved: !hasPlaceholders,
|
|
2273
|
+
parseBaseDir: this._parseBaseDir,
|
|
2274
|
+
originDescription: this._originDescription,
|
|
2275
|
+
resObjRoot: merged,
|
|
2276
|
+
resolveOpts: this._resolveOpts ?? fallback._resolveOpts
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Runs substitution resolution (phase 2) on the stored unresolved tree.
|
|
2281
|
+
* Idempotent: calling resolve() on an already-resolved Config returns an
|
|
2282
|
+
* equivalent resolved Config.
|
|
2283
|
+
*
|
|
2284
|
+
* opts.allowUnresolved (default false) — when true, leaves unresolved
|
|
2285
|
+
* non-optional placeholders in place rather than throwing.
|
|
2286
|
+
* opts.useSystemEnvironment (default true) — when false, env var lookups
|
|
2287
|
+
* are suppressed (hermetic resolution).
|
|
2288
|
+
*
|
|
2289
|
+
* E12 decision 3.
|
|
2290
|
+
*/
|
|
2291
|
+
resolve(opts = {}) {
|
|
2292
|
+
if (this._resolved) {
|
|
2293
|
+
return new _Config(this.root, {
|
|
2294
|
+
resolved: true,
|
|
2295
|
+
parseBaseDir: this._parseBaseDir,
|
|
2296
|
+
originDescription: this._originDescription
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
const tree = this._resObjRoot;
|
|
2300
|
+
if (!tree) {
|
|
2301
|
+
return new _Config(this.root, { resolved: true });
|
|
2302
|
+
}
|
|
2303
|
+
const resolveOpts = {
|
|
2304
|
+
...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
|
|
2305
|
+
throw new Error("no files");
|
|
2306
|
+
} },
|
|
2307
|
+
allowUnresolved: opts.allowUnresolved ?? false,
|
|
2308
|
+
useSystemEnvironment: opts.useSystemEnvironment ?? true,
|
|
2309
|
+
originDescription: this._originDescription
|
|
2310
|
+
};
|
|
2311
|
+
const resolved = resolveTree(tree, resolveOpts);
|
|
2312
|
+
if (resolved.kind !== "object") throw new Error("resolve: expected object root");
|
|
2313
|
+
if (opts.allowUnresolved) {
|
|
2314
|
+
const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
|
|
2315
|
+
return new _Config(stripped, {
|
|
2316
|
+
resolved: !hadPlaceholders,
|
|
2317
|
+
parseBaseDir: this._parseBaseDir,
|
|
2318
|
+
originDescription: this._originDescription,
|
|
2319
|
+
resObjRoot: hadPlaceholders ? tree : void 0,
|
|
2320
|
+
resolveOpts: hadPlaceholders ? resolveOpts : void 0
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
return new _Config(resolved, {
|
|
2324
|
+
resolved: true,
|
|
2325
|
+
parseBaseDir: this._parseBaseDir,
|
|
2326
|
+
originDescription: this._originDescription
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Resolves receiver substitutions using source as lookup context.
|
|
2331
|
+
* Source's keys are NOT merged into the result — differs from
|
|
2332
|
+
* `this.withFallback(source).resolve()` which includes source keys.
|
|
2333
|
+
*
|
|
2334
|
+
* Precondition (E12 decision 10): source must be resolved. If
|
|
2335
|
+
* source.isResolved() is false, throws NotResolvedError.
|
|
2336
|
+
*
|
|
2337
|
+
* On an already-resolved receiver, resolveWith is a no-op (returns an
|
|
2338
|
+
* equivalent resolved Config, source keys not included).
|
|
2339
|
+
*
|
|
2340
|
+
* E12 decisions 9, 10.
|
|
2341
|
+
*/
|
|
2342
|
+
resolveWith(source, opts = {}) {
|
|
2343
|
+
if (!source.isResolved()) {
|
|
2344
|
+
throw new NotResolvedError("source");
|
|
2345
|
+
}
|
|
2346
|
+
if (this._resolved) {
|
|
2347
|
+
return new _Config(this.root, {
|
|
2348
|
+
resolved: true,
|
|
2349
|
+
parseBaseDir: this._parseBaseDir,
|
|
2350
|
+
originDescription: this._originDescription
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
|
|
2354
|
+
const srcTree = hoconValueToResObj2(source.root);
|
|
2355
|
+
const merged = mergeUnresolved(receiverTree, srcTree);
|
|
2356
|
+
const resolveOpts = {
|
|
2357
|
+
...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
|
|
2358
|
+
throw new Error("no files");
|
|
2359
|
+
} },
|
|
2360
|
+
allowUnresolved: opts.allowUnresolved ?? false,
|
|
2361
|
+
useSystemEnvironment: opts.useSystemEnvironment ?? true,
|
|
2362
|
+
originDescription: this._originDescription
|
|
2363
|
+
};
|
|
2364
|
+
const resolved = resolveTree(merged, resolveOpts);
|
|
2365
|
+
if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
|
|
2366
|
+
const receiverShapeForFilter = resObjToKeyShape(receiverTree);
|
|
2367
|
+
const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
|
|
2368
|
+
if (opts.allowUnresolved) {
|
|
2369
|
+
const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
|
|
2370
|
+
return new _Config(stripped, {
|
|
2371
|
+
resolved: !hadPlaceholders,
|
|
2372
|
+
parseBaseDir: this._parseBaseDir,
|
|
2373
|
+
originDescription: this._originDescription,
|
|
2374
|
+
resObjRoot: hadPlaceholders ? receiverTree : void 0,
|
|
2375
|
+
resolveOpts: hadPlaceholders ? resolveOpts : void 0
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
return new _Config(filtered, {
|
|
2379
|
+
resolved: true,
|
|
2380
|
+
parseBaseDir: this._parseBaseDir,
|
|
2381
|
+
originDescription: this._originDescription
|
|
2382
|
+
});
|
|
1630
2383
|
}
|
|
1631
2384
|
toObject() {
|
|
1632
2385
|
return hoconToJs(this.root);
|
|
1633
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* @internal Test-only: renders this resolved Config's value tree as canonical
|
|
2389
|
+
* JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
|
|
2390
|
+
* against Lightbend ground truth. NOT part of the public API.
|
|
2391
|
+
*
|
|
2392
|
+
* Throws if the Config is unresolved (contains placeholders).
|
|
2393
|
+
*/
|
|
2394
|
+
_renderJSONForTest() {
|
|
2395
|
+
return renderHoconAsJSON(this.root);
|
|
2396
|
+
}
|
|
1634
2397
|
lookupNode(path2) {
|
|
1635
2398
|
const segments = splitConfigPath(path2);
|
|
1636
2399
|
let current = this.root;
|
|
@@ -1642,9 +2405,40 @@ var Config = class _Config {
|
|
|
1642
2405
|
}
|
|
1643
2406
|
return current;
|
|
1644
2407
|
}
|
|
2408
|
+
/**
|
|
2409
|
+
* _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
|
|
2410
|
+
* return true if the subtree there contains any unresolved placeholder.
|
|
2411
|
+
* Used by getConfig / getList (T2) to detect transitive unresolved placeholders
|
|
2412
|
+
* in object / array subtrees that would otherwise appear to be "resolved" in the
|
|
2413
|
+
* partial HoconValue root (because individual leaf fields are omitted rather than
|
|
2414
|
+
* the whole subtree when their value is a direct placeholder scalar, but object
|
|
2415
|
+
* sub-containers are retained in the partial root with the placeholder-keyed
|
|
2416
|
+
* fields omitted from them — so the object appears "present but empty-ish").
|
|
2417
|
+
*/
|
|
2418
|
+
_resObjRootSubtreeHasPlaceholders(path2) {
|
|
2419
|
+
const tree = this._resObjRoot;
|
|
2420
|
+
if (!tree) return false;
|
|
2421
|
+
const segments = splitConfigPath(path2);
|
|
2422
|
+
let cur = tree;
|
|
2423
|
+
for (const seg of segments) {
|
|
2424
|
+
const val = cur.fields.get(seg);
|
|
2425
|
+
if (val === void 0) return false;
|
|
2426
|
+
if (isResObj(val)) {
|
|
2427
|
+
cur = val;
|
|
2428
|
+
} else {
|
|
2429
|
+
return valContainsPlaceholders(val);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return containsPlaceholders(cur);
|
|
2433
|
+
}
|
|
1645
2434
|
requireScalar(path2) {
|
|
1646
2435
|
const v = this.lookupNode(path2);
|
|
1647
|
-
if (v === void 0)
|
|
2436
|
+
if (v === void 0) {
|
|
2437
|
+
if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
|
|
2438
|
+
throw new NotResolvedError(path2);
|
|
2439
|
+
}
|
|
2440
|
+
throw new ConfigError(`path not found: ${path2}`, path2);
|
|
2441
|
+
}
|
|
1648
2442
|
if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
|
|
1649
2443
|
return v;
|
|
1650
2444
|
}
|
|
@@ -1707,28 +2501,89 @@ function hoconToJs(v) {
|
|
|
1707
2501
|
case "array":
|
|
1708
2502
|
return v.items.map(hoconToJs);
|
|
1709
2503
|
case "object": {
|
|
1710
|
-
const obj =
|
|
2504
|
+
const obj = /* @__PURE__ */ Object.create(null);
|
|
1711
2505
|
for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
|
|
1712
|
-
return obj;
|
|
2506
|
+
return Object.assign({}, obj);
|
|
1713
2507
|
}
|
|
1714
2508
|
}
|
|
1715
2509
|
}
|
|
1716
|
-
function
|
|
1717
|
-
const
|
|
1718
|
-
for (const [k, v] of
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2510
|
+
function resObjToKeyShape(tree) {
|
|
2511
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2512
|
+
for (const [k, v] of tree.fields) {
|
|
2513
|
+
if (isSubst(v) || isConcat(v) || isAppend(v)) {
|
|
2514
|
+
fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
|
|
2515
|
+
} else if (isResObj(v)) {
|
|
2516
|
+
fields.set(k, resObjToKeyShape(v));
|
|
1722
2517
|
} else {
|
|
1723
|
-
|
|
2518
|
+
fields.set(k, v);
|
|
1724
2519
|
}
|
|
1725
2520
|
}
|
|
1726
|
-
return { kind: "object", fields
|
|
2521
|
+
return { kind: "object", fields };
|
|
2522
|
+
}
|
|
2523
|
+
function stripPlaceholderFields(v) {
|
|
2524
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2525
|
+
let hadPlaceholders = false;
|
|
2526
|
+
for (const [k, val] of v.fields) {
|
|
2527
|
+
if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
|
|
2528
|
+
hadPlaceholders = true;
|
|
2529
|
+
} else if (val.kind === "object") {
|
|
2530
|
+
const inner = stripPlaceholderFields(val);
|
|
2531
|
+
if (inner.hadPlaceholders) hadPlaceholders = true;
|
|
2532
|
+
fields.set(k, inner.stripped);
|
|
2533
|
+
} else {
|
|
2534
|
+
fields.set(k, val);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return { stripped: { kind: "object", fields }, hadPlaceholders };
|
|
2538
|
+
}
|
|
2539
|
+
function filterByReceiverShape(resolved, receiverShape) {
|
|
2540
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2541
|
+
for (const [k, rv] of resolved.fields) {
|
|
2542
|
+
if (!receiverShape.fields.has(k)) continue;
|
|
2543
|
+
const receiverVal = receiverShape.fields.get(k);
|
|
2544
|
+
if (rv.kind === "object" && receiverVal.kind === "object") {
|
|
2545
|
+
fields.set(k, filterByReceiverShape(rv, receiverVal));
|
|
2546
|
+
} else {
|
|
2547
|
+
fields.set(k, rv);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return { kind: "object", fields };
|
|
2551
|
+
}
|
|
2552
|
+
function renderHoconAsJSON(v) {
|
|
2553
|
+
switch (v.kind) {
|
|
2554
|
+
case "scalar": {
|
|
2555
|
+
switch (v.valueType) {
|
|
2556
|
+
case "null":
|
|
2557
|
+
return "null";
|
|
2558
|
+
case "boolean":
|
|
2559
|
+
return v.raw;
|
|
2560
|
+
case "number": {
|
|
2561
|
+
const n = Number(v.raw);
|
|
2562
|
+
if (Number.isFinite(n)) return v.raw;
|
|
2563
|
+
return JSON.stringify(v.raw);
|
|
2564
|
+
}
|
|
2565
|
+
case "string":
|
|
2566
|
+
return JSON.stringify(v.raw);
|
|
2567
|
+
}
|
|
2568
|
+
break;
|
|
2569
|
+
}
|
|
2570
|
+
case "array": {
|
|
2571
|
+
const items = v.items.map(renderHoconAsJSON);
|
|
2572
|
+
return `[${items.join(",")}]`;
|
|
2573
|
+
}
|
|
2574
|
+
case "object": {
|
|
2575
|
+
const keys = [...v.fields.keys()].sort();
|
|
2576
|
+
const pairs = keys.map((k) => {
|
|
2577
|
+
const val = v.fields.get(k);
|
|
2578
|
+
return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
|
|
2579
|
+
});
|
|
2580
|
+
return `{${pairs.join(",")}}`;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
|
|
1727
2584
|
}
|
|
1728
2585
|
|
|
1729
2586
|
// src/parse.ts
|
|
1730
|
-
var fs = __toESM(require("fs"), 1);
|
|
1731
|
-
var path = __toESM(require("path"), 1);
|
|
1732
2587
|
function getEnv(opts) {
|
|
1733
2588
|
if (opts.env !== void 0) return opts.env;
|
|
1734
2589
|
if (typeof process !== "undefined" && process.env) return process.env;
|
|
@@ -1739,12 +2594,16 @@ function defaultReadFileSync(filePath) {
|
|
|
1739
2594
|
}
|
|
1740
2595
|
function buildResolveContext(input, opts) {
|
|
1741
2596
|
const tokens = tokenize(input);
|
|
2597
|
+
assertNonEmptyDocument(tokens, "input");
|
|
1742
2598
|
const ast = parseTokens(tokens);
|
|
1743
2599
|
const resolveOpts = {
|
|
1744
2600
|
env: getEnv(opts),
|
|
1745
2601
|
baseDir: opts.baseDir,
|
|
1746
2602
|
readFileSync: opts.readFileSync ?? defaultReadFileSync,
|
|
1747
|
-
readFile: opts.readFile
|
|
2603
|
+
readFile: opts.readFile,
|
|
2604
|
+
originDescription: opts.originDescription,
|
|
2605
|
+
resolveFrom: opts.resolveFrom,
|
|
2606
|
+
packageResolver: opts.packageResolver
|
|
1748
2607
|
};
|
|
1749
2608
|
return { ast, resolveOpts };
|
|
1750
2609
|
}
|
|
@@ -1752,7 +2611,7 @@ function parse(input, opts = {}) {
|
|
|
1752
2611
|
const { ast, resolveOpts } = buildResolveContext(input, opts);
|
|
1753
2612
|
const value = resolve2(ast, resolveOpts);
|
|
1754
2613
|
if (value.kind !== "object") throw new Error("resolved value is not an object");
|
|
1755
|
-
return new Config(value);
|
|
2614
|
+
return new Config(value, { resolved: true, originDescription: opts.originDescription });
|
|
1756
2615
|
}
|
|
1757
2616
|
function parseFile(filePath, opts = {}) {
|
|
1758
2617
|
if (typeof process === "undefined" && !opts.readFileSync) {
|