@o3co/ts.hocon 1.3.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/dist/zod.cjs CHANGED
@@ -234,6 +234,10 @@ function parseBytes(value, outputUnit = "B") {
234
234
  return outputUnit === "B" ? Math.round(result) : result;
235
235
  }
236
236
 
237
+ // src/parse.ts
238
+ var fs = __toESM(require("fs"), 1);
239
+ var path = __toESM(require("path"), 1);
240
+
237
241
  // src/errors.ts
238
242
  var ParseError = class extends Error {
239
243
  constructor(message, line, col, file) {
@@ -254,6 +258,14 @@ var ResolveError = class extends Error {
254
258
  this.name = "ResolveError";
255
259
  }
256
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
+ };
257
269
  var ConfigError = class extends Error {
258
270
  constructor(message, path2) {
259
271
  super(message);
@@ -261,6 +273,156 @@ var ConfigError = class extends Error {
261
273
  this.name = "ConfigError";
262
274
  }
263
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
+ }
264
426
 
265
427
  // src/internal/lexer/lexer.ts
266
428
  function isHoconWhitespace(ch) {
@@ -825,58 +987,137 @@ var Parser = class {
825
987
  const p = this.currentPos();
826
988
  this.skip("newline");
827
989
  const t = this.peek();
828
- let path2;
829
- let required = false;
830
- let isFile = false;
831
990
  if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
832
991
  if (t.value === "required") {
833
- const next2 = this.peek(1);
834
- if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
992
+ const next = this.peek(1);
993
+ if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
835
994
  throw new ParseError("include required must be followed by (", t.line, t.col);
836
995
  }
837
996
  }
838
- required = true;
839
997
  this.advance();
840
998
  const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
841
- if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
999
+ if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
842
1000
  throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
843
1001
  }
844
- if (innerPrefix.startsWith("file")) {
845
- isFile = true;
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);
846
1007
  }
847
- const next = this.peek();
848
- if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
849
- throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
850
- }
851
- if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
852
- isFile = true;
853
- }
854
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
855
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
856
- path2 = this.advance().value;
857
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
858
- } else if (t.kind === "string") {
859
- path2 = this.advance().value;
860
- } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
861
- isFile = true;
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")) {
862
1033
  this.advance();
863
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
864
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
865
- path2 = this.advance().value;
866
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
867
- } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
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("))) {
868
1042
  throw new ParseError("include url(...) is not supported", t.line, t.col);
869
- } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
1043
+ }
1044
+ if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
870
1045
  throw new ParseError("include classpath(...) is not supported", t.line, t.col);
871
- } else {
872
- throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
873
1046
  }
874
- return {
875
- key: [],
876
- value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
877
- append: false,
878
- pos: p
879
- };
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 };
880
1121
  }
881
1122
  parseValue() {
882
1123
  const p = this.currentPos();
@@ -917,7 +1158,9 @@ var Parser = class {
917
1158
  parts.push(node);
918
1159
  }
919
1160
  if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
920
- if (parts.length === 1) return parts[0];
1161
+ if (parts.length === 1) {
1162
+ return parts[0];
1163
+ }
921
1164
  return { kind: "concat", nodes: parts, pos: p };
922
1165
  }
923
1166
  parseArray() {
@@ -949,118 +1192,6 @@ function parseTokens(tokens) {
949
1192
  return new Parser(tokens).parse();
950
1193
  }
951
1194
 
952
- // src/internal/parser/empty-check.ts
953
- function hasContentTokens(tokens) {
954
- return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
955
- }
956
- function assertNonEmptyDocument(tokens, sourceDescriptor) {
957
- if (!hasContentTokens(tokens)) {
958
- throw new ParseError(
959
- `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
960
- 1,
961
- 1
962
- );
963
- }
964
- }
965
-
966
- // src/internal/resolver/types.ts
967
- var separatorValues = /* @__PURE__ */ new WeakSet();
968
- function isSubst(v) {
969
- return v._kind === "subst-placeholder";
970
- }
971
- function isConcat(v) {
972
- return v._kind === "concat-placeholder";
973
- }
974
- function isAppend(v) {
975
- return v._kind === "append-placeholder";
976
- }
977
- function isResObj(v) {
978
- return v._kind === "res-obj";
979
- }
980
- function makeResObj() {
981
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
982
- }
983
-
984
- // src/internal/resolver/utils.ts
985
- function segmentsToKey(segments) {
986
- return segments.map((s) => {
987
- const t = s.text;
988
- if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
989
- return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
990
- }
991
- return t;
992
- }).join(".");
993
- }
994
- function lookupPath(root, segments) {
995
- const [head, ...tail] = segments;
996
- if (head === void 0) return void 0;
997
- const key = head.text;
998
- const val = root.fields.get(key);
999
- if (val === void 0) return void 0;
1000
- if (tail.length === 0) return val;
1001
- if (isResObj(val)) return lookupPath(val, tail);
1002
- return void 0;
1003
- }
1004
- function lookupResObj(root, segments) {
1005
- let cur = root;
1006
- for (const seg of segments) {
1007
- const val = cur.fields.get(seg.text);
1008
- if (val === void 0 || !isResObj(val)) return void 0;
1009
- cur = val;
1010
- }
1011
- return cur;
1012
- }
1013
- function deepMergeHoconValues(base, overlay) {
1014
- const merged = new Map(base.fields);
1015
- for (const [k, v] of overlay.fields) {
1016
- const existing = merged.get(k);
1017
- if (existing?.kind === "object" && v.kind === "object") {
1018
- merged.set(k, deepMergeHoconValues(existing, v));
1019
- } else {
1020
- merged.set(k, v);
1021
- }
1022
- }
1023
- return { kind: "object", fields: merged };
1024
- }
1025
- function deepMergeResObjInto(dst, src) {
1026
- for (const [k, srcVal] of src.fields) {
1027
- const dstVal = dst.fields.get(k);
1028
- if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
1029
- deepMergeResObjInto(dstVal, srcVal);
1030
- } else {
1031
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
1032
- dst.fields.set(k, srcVal);
1033
- }
1034
- }
1035
- for (const [k, srcPrior] of src.priorValues) {
1036
- if (!dst.priorValues.has(k)) {
1037
- dst.priorValues.set(k, srcPrior);
1038
- }
1039
- }
1040
- }
1041
- function hoconValueToResObj(hv) {
1042
- const obj = makeResObj();
1043
- if (hv.kind !== "object") return obj;
1044
- for (const [key, val] of hv.fields) {
1045
- if (val.kind === "object") {
1046
- obj.fields.set(key, hoconValueToResObj(val));
1047
- } else {
1048
- obj.fields.set(key, val);
1049
- }
1050
- }
1051
- return obj;
1052
- }
1053
- function isFileNotFoundError(e) {
1054
- if (!(e instanceof Error)) return false;
1055
- const code = e.code;
1056
- if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1057
- const msg = e.message.toLowerCase();
1058
- return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
1059
- }
1060
-
1061
- // src/internal/resolver/include-loader.ts
1062
- var nodePath = __toESM(require("path"), 1);
1063
-
1064
1195
  // src/internal/properties/properties.ts
1065
1196
  function parseProperties(input) {
1066
1197
  const root = /* @__PURE__ */ Object.create(null);
@@ -1120,6 +1251,85 @@ function recordToHoconValue(obj) {
1120
1251
  }
1121
1252
 
1122
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
+ }
1123
1333
  var IncludeLoader = class {
1124
1334
  constructor(opts) {
1125
1335
  this.opts = opts;
@@ -1199,8 +1409,77 @@ var IncludeLoader = class {
1199
1409
  }
1200
1410
  return merged;
1201
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
+ }
1202
1481
  loadSingle(candidate) {
1203
- const { readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1482
+ const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
1204
1483
  if (includeStack.includes(candidate)) {
1205
1484
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1206
1485
  }
@@ -1218,14 +1497,13 @@ var IncludeLoader = class {
1218
1497
  assertNonEmptyDocument(tokens, candidate);
1219
1498
  const ast = parseTokens(tokens);
1220
1499
  return this.onBuildResObj(ast, {
1221
- env,
1500
+ ...this.opts,
1222
1501
  baseDir: nodePath.dirname(candidate),
1223
- readFileSync: readFileSync2,
1224
1502
  includeStack: [...includeStack, candidate]
1225
1503
  });
1226
1504
  }
1227
1505
  async loadSingleAsync(candidate) {
1228
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1506
+ const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
1229
1507
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1230
1508
  if (includeStack.includes(candidate)) {
1231
1509
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
@@ -1244,10 +1522,8 @@ var IncludeLoader = class {
1244
1522
  assertNonEmptyDocument(tokens, candidate);
1245
1523
  const ast = parseTokens(tokens);
1246
1524
  return this.onBuildResObjAsync(ast, {
1247
- env,
1525
+ ...this.opts,
1248
1526
  baseDir: nodePath.dirname(candidate),
1249
- readFileSync: readFileSync2,
1250
- readFile,
1251
1527
  includeStack: [...includeStack, candidate]
1252
1528
  });
1253
1529
  }
@@ -1256,7 +1532,6 @@ var IncludeLoader = class {
1256
1532
  // src/internal/resolver/structure-builder.ts
1257
1533
  var StructureBuilder = class _StructureBuilder {
1258
1534
  constructor(opts) {
1259
- this.opts = opts;
1260
1535
  this.loader = new IncludeLoader(opts);
1261
1536
  this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1262
1537
  this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
@@ -1283,7 +1558,13 @@ var StructureBuilder = class _StructureBuilder {
1283
1558
  }
1284
1559
  applyField(obj, field, pathPrefix) {
1285
1560
  if (field.key.length === 0 && field.value.kind === "include") {
1286
- const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
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
+ }
1287
1568
  if (pathPrefix.length > 0) {
1288
1569
  this.relativizeResObj(included, pathPrefix);
1289
1570
  }
@@ -1325,7 +1606,13 @@ var StructureBuilder = class _StructureBuilder {
1325
1606
  }
1326
1607
  async applyFieldAsync(obj, field, pathPrefix) {
1327
1608
  if (field.key.length === 0 && field.value.kind === "include") {
1328
- const included = await this.loader.loadAsync(field.value.path, field.value.required, field.value.isFile);
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
+ }
1329
1616
  if (pathPrefix.length > 0) {
1330
1617
  this.relativizeResObj(included, pathPrefix);
1331
1618
  }
@@ -1493,6 +1780,10 @@ var SubstitutionResolver = class {
1493
1780
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1494
1781
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1495
1782
  }
1783
+ /** Returns a prefix for error messages, including origin info when set. */
1784
+ originPrefix() {
1785
+ return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
1786
+ }
1496
1787
  resolve() {
1497
1788
  return this.resolveResObj(this.root);
1498
1789
  }
@@ -1533,7 +1824,8 @@ var SubstitutionResolver = class {
1533
1824
  if (isConcat(v)) {
1534
1825
  this.resolvingConcats.add(v);
1535
1826
  try {
1536
- return this.resolveConcat(v.nodes, scope, v.line, v.col);
1827
+ const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
1828
+ return concatResult;
1537
1829
  } finally {
1538
1830
  this.resolvingConcats.delete(v);
1539
1831
  }
@@ -1578,7 +1870,7 @@ var SubstitutionResolver = class {
1578
1870
  }
1579
1871
  if (s.optional) return void 0;
1580
1872
  throw new ResolveError(
1581
- `circular substitution: ${key}`,
1873
+ this.originPrefix() + `circular substitution: ${key}`,
1582
1874
  key,
1583
1875
  s.line,
1584
1876
  s.col
@@ -1608,7 +1900,7 @@ var SubstitutionResolver = class {
1608
1900
  }
1609
1901
  if (s.optional) return void 0;
1610
1902
  throw new ResolveError(
1611
- `could not resolve substitution: \${${key}}`,
1903
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1612
1904
  key,
1613
1905
  s.line,
1614
1906
  s.col
@@ -1641,7 +1933,7 @@ var SubstitutionResolver = class {
1641
1933
  if (result !== void 0) this.cache.set(key, result);
1642
1934
  return result;
1643
1935
  }
1644
- if (s.listSuffix) {
1936
+ if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1645
1937
  const result = this.resolveEnvList(s);
1646
1938
  if (result !== void 0) {
1647
1939
  this.cache.set(key, result);
@@ -1650,22 +1942,36 @@ var SubstitutionResolver = class {
1650
1942
  if (s.optional) return void 0;
1651
1943
  const envBase = s.segments.map((seg) => seg.text).join(".");
1652
1944
  throw new ResolveError(
1653
- `could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
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}}`,
1654
1955
  key,
1655
1956
  s.line,
1656
1957
  s.col
1657
1958
  );
1658
1959
  }
1659
- const envKey = s.segments.map((seg) => seg.text).join(".");
1660
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1661
- if (envVal !== void 0) {
1662
- const result = { kind: "scalar", raw: envVal, valueType: "string" };
1663
- this.cache.set(key, result);
1664
- return result;
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
+ }
1665
1968
  }
1666
1969
  if (s.optional) return void 0;
1970
+ if (this.opts.allowUnresolved) {
1971
+ return s;
1972
+ }
1667
1973
  throw new ResolveError(
1668
- `could not resolve substitution: \${${key}}`,
1974
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1669
1975
  key,
1670
1976
  s.line,
1671
1977
  s.col
@@ -1710,8 +2016,16 @@ var SubstitutionResolver = class {
1710
2016
  return [full, bare];
1711
2017
  }
1712
2018
  resolveConcat(nodes, scope, line = 0, col = 0) {
1713
- const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1714
- if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
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;
1715
2029
  if (resolved.length === 1) return resolved[0];
1716
2030
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1717
2031
  if (nonSep.length === 0) {
@@ -1785,15 +2099,89 @@ var SubstitutionResolver = class {
1785
2099
  };
1786
2100
 
1787
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
+ }
1788
2150
  function resolve2(ast, opts) {
1789
- const root = new StructureBuilder(opts).build(ast);
1790
- return new SubstitutionResolver(root, opts).resolve();
2151
+ const tree = buildTree(ast, opts);
2152
+ return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
1791
2153
  }
1792
2154
 
1793
2155
  // src/config.ts
1794
2156
  var Config = class _Config {
1795
- constructor(root) {
2157
+ constructor(root, opts) {
1796
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
+ });
1797
2185
  }
1798
2186
  get(path2) {
1799
2187
  const v = this.lookupNode(path2);
@@ -1837,13 +2225,29 @@ var Config = class _Config {
1837
2225
  }
1838
2226
  getConfig(path2) {
1839
2227
  const v = this.lookupNode(path2);
1840
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
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
+ }
1841
2234
  if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
1842
- return new _Config(v);
2235
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2236
+ throw new NotResolvedError(path2);
2237
+ }
2238
+ return new _Config(v, { resolved: this._resolved });
1843
2239
  }
1844
2240
  getList(path2) {
1845
2241
  const v = this.lookupNode(path2);
1846
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
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
+ }
1847
2251
  if (v.kind === "object") {
1848
2252
  const converted = numericObjectToArray(v);
1849
2253
  if (converted !== null) return converted.map(hoconToJs);
@@ -1858,12 +2262,138 @@ var Config = class _Config {
1858
2262
  return [...this.root.fields.keys()];
1859
2263
  }
1860
2264
  withFallback(fallback) {
1861
- const merged = deepMergeHocon(this.root, fallback.root);
1862
- return new _Config(merged);
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
+ });
1863
2383
  }
1864
2384
  toObject() {
1865
2385
  return hoconToJs(this.root);
1866
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
+ }
1867
2397
  lookupNode(path2) {
1868
2398
  const segments = splitConfigPath(path2);
1869
2399
  let current = this.root;
@@ -1875,9 +2405,40 @@ var Config = class _Config {
1875
2405
  }
1876
2406
  return current;
1877
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
+ }
1878
2434
  requireScalar(path2) {
1879
2435
  const v = this.lookupNode(path2);
1880
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
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
+ }
1881
2442
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
1882
2443
  return v;
1883
2444
  }
@@ -1940,28 +2501,89 @@ function hoconToJs(v) {
1940
2501
  case "array":
1941
2502
  return v.items.map(hoconToJs);
1942
2503
  case "object": {
1943
- const obj = {};
2504
+ const obj = /* @__PURE__ */ Object.create(null);
1944
2505
  for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
1945
- return obj;
2506
+ return Object.assign({}, obj);
2507
+ }
2508
+ }
2509
+ }
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));
2517
+ } else {
2518
+ fields.set(k, v);
1946
2519
  }
1947
2520
  }
2521
+ return { kind: "object", fields };
1948
2522
  }
1949
- function deepMergeHocon(receiver, fallback) {
1950
- const merged = new Map(fallback.fields);
1951
- for (const [k, v] of receiver.fields) {
1952
- const fb = merged.get(k);
1953
- if (fb?.kind === "object" && v.kind === "object") {
1954
- merged.set(k, deepMergeHocon(v, fb));
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);
1955
2533
  } else {
1956
- merged.set(k, v);
2534
+ fields.set(k, val);
1957
2535
  }
1958
2536
  }
1959
- return { kind: "object", fields: merged };
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?)`);
1960
2584
  }
1961
2585
 
1962
2586
  // src/parse.ts
1963
- var fs = __toESM(require("fs"), 1);
1964
- var path = __toESM(require("path"), 1);
1965
2587
  function getEnv(opts) {
1966
2588
  if (opts.env !== void 0) return opts.env;
1967
2589
  if (typeof process !== "undefined" && process.env) return process.env;
@@ -1978,7 +2600,10 @@ function buildResolveContext(input, opts) {
1978
2600
  env: getEnv(opts),
1979
2601
  baseDir: opts.baseDir,
1980
2602
  readFileSync: opts.readFileSync ?? defaultReadFileSync,
1981
- readFile: opts.readFile
2603
+ readFile: opts.readFile,
2604
+ originDescription: opts.originDescription,
2605
+ resolveFrom: opts.resolveFrom,
2606
+ packageResolver: opts.packageResolver
1982
2607
  };
1983
2608
  return { ast, resolveOpts };
1984
2609
  }
@@ -1986,7 +2611,7 @@ function parse(input, opts = {}) {
1986
2611
  const { ast, resolveOpts } = buildResolveContext(input, opts);
1987
2612
  const value = resolve2(ast, resolveOpts);
1988
2613
  if (value.kind !== "object") throw new Error("resolved value is not an object");
1989
- return new Config(value);
2614
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
1990
2615
  }
1991
2616
  function parseFile(filePath, opts = {}) {
1992
2617
  if (typeof process === "undefined" && !opts.readFileSync) {