@o3co/ts.hocon 1.3.0 → 1.4.1

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
  }
@@ -1215,17 +1494,18 @@ var IncludeLoader = class {
1215
1494
  return hoconValueToResObj(propertiesToHoconValue(content));
1216
1495
  }
1217
1496
  const tokens = tokenize(content);
1218
- assertNonEmptyDocument(tokens, candidate);
1497
+ if (!hasContentTokens(tokens)) {
1498
+ return makeResObj();
1499
+ }
1219
1500
  const ast = parseTokens(tokens);
1220
1501
  return this.onBuildResObj(ast, {
1221
- env,
1502
+ ...this.opts,
1222
1503
  baseDir: nodePath.dirname(candidate),
1223
- readFileSync: readFileSync2,
1224
1504
  includeStack: [...includeStack, candidate]
1225
1505
  });
1226
1506
  }
1227
1507
  async loadSingleAsync(candidate) {
1228
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1508
+ const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
1229
1509
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1230
1510
  if (includeStack.includes(candidate)) {
1231
1511
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
@@ -1241,13 +1521,13 @@ var IncludeLoader = class {
1241
1521
  return hoconValueToResObj(propertiesToHoconValue(content));
1242
1522
  }
1243
1523
  const tokens = tokenize(content);
1244
- assertNonEmptyDocument(tokens, candidate);
1524
+ if (!hasContentTokens(tokens)) {
1525
+ return makeResObj();
1526
+ }
1245
1527
  const ast = parseTokens(tokens);
1246
1528
  return this.onBuildResObjAsync(ast, {
1247
- env,
1529
+ ...this.opts,
1248
1530
  baseDir: nodePath.dirname(candidate),
1249
- readFileSync: readFileSync2,
1250
- readFile,
1251
1531
  includeStack: [...includeStack, candidate]
1252
1532
  });
1253
1533
  }
@@ -1256,7 +1536,6 @@ var IncludeLoader = class {
1256
1536
  // src/internal/resolver/structure-builder.ts
1257
1537
  var StructureBuilder = class _StructureBuilder {
1258
1538
  constructor(opts) {
1259
- this.opts = opts;
1260
1539
  this.loader = new IncludeLoader(opts);
1261
1540
  this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1262
1541
  this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
@@ -1283,7 +1562,13 @@ var StructureBuilder = class _StructureBuilder {
1283
1562
  }
1284
1563
  applyField(obj, field, pathPrefix) {
1285
1564
  if (field.key.length === 0 && field.value.kind === "include") {
1286
- const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
1565
+ const { qualifier, path: path2, required } = field.value;
1566
+ let included;
1567
+ if (qualifier.kind === "package") {
1568
+ included = this.loader.loadPackage(qualifier.identifier, path2, required);
1569
+ } else {
1570
+ included = this.loader.load(path2, required, qualifier.kind === "file");
1571
+ }
1287
1572
  if (pathPrefix.length > 0) {
1288
1573
  this.relativizeResObj(included, pathPrefix);
1289
1574
  }
@@ -1325,7 +1610,13 @@ var StructureBuilder = class _StructureBuilder {
1325
1610
  }
1326
1611
  async applyFieldAsync(obj, field, pathPrefix) {
1327
1612
  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);
1613
+ const { qualifier, path: path2, required } = field.value;
1614
+ let included;
1615
+ if (qualifier.kind === "package") {
1616
+ included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
1617
+ } else {
1618
+ included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
1619
+ }
1329
1620
  if (pathPrefix.length > 0) {
1330
1621
  this.relativizeResObj(included, pathPrefix);
1331
1622
  }
@@ -1493,6 +1784,10 @@ var SubstitutionResolver = class {
1493
1784
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1494
1785
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1495
1786
  }
1787
+ /** Returns a prefix for error messages, including origin info when set. */
1788
+ originPrefix() {
1789
+ return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
1790
+ }
1496
1791
  resolve() {
1497
1792
  return this.resolveResObj(this.root);
1498
1793
  }
@@ -1533,7 +1828,8 @@ var SubstitutionResolver = class {
1533
1828
  if (isConcat(v)) {
1534
1829
  this.resolvingConcats.add(v);
1535
1830
  try {
1536
- return this.resolveConcat(v.nodes, scope, v.line, v.col);
1831
+ const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
1832
+ return concatResult;
1537
1833
  } finally {
1538
1834
  this.resolvingConcats.delete(v);
1539
1835
  }
@@ -1578,7 +1874,7 @@ var SubstitutionResolver = class {
1578
1874
  }
1579
1875
  if (s.optional) return void 0;
1580
1876
  throw new ResolveError(
1581
- `circular substitution: ${key}`,
1877
+ this.originPrefix() + `circular substitution: ${key}`,
1582
1878
  key,
1583
1879
  s.line,
1584
1880
  s.col
@@ -1608,7 +1904,7 @@ var SubstitutionResolver = class {
1608
1904
  }
1609
1905
  if (s.optional) return void 0;
1610
1906
  throw new ResolveError(
1611
- `could not resolve substitution: \${${key}}`,
1907
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1612
1908
  key,
1613
1909
  s.line,
1614
1910
  s.col
@@ -1641,7 +1937,7 @@ var SubstitutionResolver = class {
1641
1937
  if (result !== void 0) this.cache.set(key, result);
1642
1938
  return result;
1643
1939
  }
1644
- if (s.listSuffix) {
1940
+ if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1645
1941
  const result = this.resolveEnvList(s);
1646
1942
  if (result !== void 0) {
1647
1943
  this.cache.set(key, result);
@@ -1650,22 +1946,36 @@ var SubstitutionResolver = class {
1650
1946
  if (s.optional) return void 0;
1651
1947
  const envBase = s.segments.map((seg) => seg.text).join(".");
1652
1948
  throw new ResolveError(
1653
- `could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1949
+ `${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1950
+ key,
1951
+ s.line,
1952
+ s.col
1953
+ );
1954
+ } else if (s.listSuffix) {
1955
+ if (s.optional) return void 0;
1956
+ if (this.opts.allowUnresolved) return s;
1957
+ throw new ResolveError(
1958
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1654
1959
  key,
1655
1960
  s.line,
1656
1961
  s.col
1657
1962
  );
1658
1963
  }
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;
1964
+ if (this.opts.useSystemEnvironment !== false) {
1965
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1966
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1967
+ if (envVal !== void 0) {
1968
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1969
+ this.cache.set(key, result);
1970
+ return result;
1971
+ }
1665
1972
  }
1666
1973
  if (s.optional) return void 0;
1974
+ if (this.opts.allowUnresolved) {
1975
+ return s;
1976
+ }
1667
1977
  throw new ResolveError(
1668
- `could not resolve substitution: \${${key}}`,
1978
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1669
1979
  key,
1670
1980
  s.line,
1671
1981
  s.col
@@ -1710,8 +2020,16 @@ var SubstitutionResolver = class {
1710
2020
  return [full, bare];
1711
2021
  }
1712
2022
  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" };
2023
+ const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
2024
+ if (this.opts.allowUnresolved) {
2025
+ for (const v of rawResolved) {
2026
+ if (v !== void 0 && isSubst(v)) {
2027
+ return v;
2028
+ }
2029
+ }
2030
+ }
2031
+ const resolved = rawResolved.filter((v) => v !== void 0);
2032
+ if (resolved.length === 0) return void 0;
1715
2033
  if (resolved.length === 1) return resolved[0];
1716
2034
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1717
2035
  if (nonSep.length === 0) {
@@ -1785,15 +2103,89 @@ var SubstitutionResolver = class {
1785
2103
  };
1786
2104
 
1787
2105
  // src/internal/resolver/resolver.ts
2106
+ function buildTree(ast, opts) {
2107
+ return new StructureBuilder(opts).build(ast);
2108
+ }
2109
+ function resolveTree(tree, opts) {
2110
+ return new SubstitutionResolver(tree, opts).resolve();
2111
+ }
2112
+ function containsPlaceholders(tree) {
2113
+ for (const val of tree.fields.values()) {
2114
+ if (valContainsPlaceholders(val)) return true;
2115
+ }
2116
+ return false;
2117
+ }
2118
+ function valContainsPlaceholders(v) {
2119
+ const rv = v;
2120
+ if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2121
+ if (isResObj(rv)) return containsPlaceholders(rv);
2122
+ const hv = v;
2123
+ if (hv?.kind === "array") {
2124
+ return hv.items.some((item) => valContainsPlaceholders(item));
2125
+ }
2126
+ return false;
2127
+ }
2128
+ function buildPartialHoconFromResObj(tree) {
2129
+ const fields = /* @__PURE__ */ new Map();
2130
+ for (const [k, v] of tree.fields) {
2131
+ if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2132
+ if (isResObj(v)) {
2133
+ fields.set(k, buildPartialHoconFromResObj(v));
2134
+ } else {
2135
+ if (!valContainsPlaceholders(v)) {
2136
+ fields.set(k, v);
2137
+ }
2138
+ }
2139
+ }
2140
+ }
2141
+ return { kind: "object", fields };
2142
+ }
2143
+ function hoconValueToResObj2(value) {
2144
+ const obj = makeResObj();
2145
+ for (const [k, v] of value.fields) {
2146
+ if (v.kind === "object") {
2147
+ obj.fields.set(k, hoconValueToResObj2(v));
2148
+ } else {
2149
+ obj.fields.set(k, v);
2150
+ }
2151
+ }
2152
+ return obj;
2153
+ }
1788
2154
  function resolve2(ast, opts) {
1789
- const root = new StructureBuilder(opts).build(ast);
1790
- return new SubstitutionResolver(root, opts).resolve();
2155
+ const tree = buildTree(ast, opts);
2156
+ return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
1791
2157
  }
1792
2158
 
1793
2159
  // src/config.ts
1794
2160
  var Config = class _Config {
1795
- constructor(root) {
2161
+ constructor(root, opts) {
1796
2162
  this.root = root;
2163
+ this._resolved = opts?.resolved ?? true;
2164
+ this._parseBaseDir = opts?.parseBaseDir;
2165
+ this._originDescription = opts?.originDescription;
2166
+ this._resObjRoot = opts?.resObjRoot;
2167
+ this._resolveOpts = opts?.resolveOpts;
2168
+ }
2169
+ /** Returns true when the Config's value tree contains no unresolved
2170
+ * substitution placeholders. Whole-config granularity (E12 decision 11). */
2171
+ isResolved() {
2172
+ return this._resolved;
2173
+ }
2174
+ /** @internal Test-only: construct a resolved Config from a HoconValue. */
2175
+ static _fromResolvedValue(root, opts) {
2176
+ return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
2177
+ }
2178
+ /** @internal Construct an unresolved Config carrying a ResObj tree.
2179
+ * Used by parseStringWithOptions (deferred path) in T6. */
2180
+ static _fromUnresolvedResObj(tree, opts) {
2181
+ const partialRoot = buildPartialHoconFromResObj(tree);
2182
+ return new _Config(partialRoot, {
2183
+ resolved: opts.resolved,
2184
+ parseBaseDir: opts.parseBaseDir,
2185
+ originDescription: opts.originDescription,
2186
+ resObjRoot: tree,
2187
+ resolveOpts: opts.resolveOpts
2188
+ });
1797
2189
  }
1798
2190
  get(path2) {
1799
2191
  const v = this.lookupNode(path2);
@@ -1837,13 +2229,29 @@ var Config = class _Config {
1837
2229
  }
1838
2230
  getConfig(path2) {
1839
2231
  const v = this.lookupNode(path2);
1840
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2232
+ if (v === void 0) {
2233
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2234
+ throw new NotResolvedError(path2);
2235
+ }
2236
+ throw new ConfigError(`path not found: ${path2}`, path2);
2237
+ }
1841
2238
  if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
1842
- return new _Config(v);
2239
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2240
+ throw new NotResolvedError(path2);
2241
+ }
2242
+ return new _Config(v, { resolved: this._resolved });
1843
2243
  }
1844
2244
  getList(path2) {
1845
2245
  const v = this.lookupNode(path2);
1846
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2246
+ if (v === void 0) {
2247
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2248
+ throw new NotResolvedError(path2);
2249
+ }
2250
+ throw new ConfigError(`path not found: ${path2}`, path2);
2251
+ }
2252
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2253
+ throw new NotResolvedError(path2);
2254
+ }
1847
2255
  if (v.kind === "object") {
1848
2256
  const converted = numericObjectToArray(v);
1849
2257
  if (converted !== null) return converted.map(hoconToJs);
@@ -1858,12 +2266,138 @@ var Config = class _Config {
1858
2266
  return [...this.root.fields.keys()];
1859
2267
  }
1860
2268
  withFallback(fallback) {
1861
- const merged = deepMergeHocon(this.root, fallback.root);
1862
- return new _Config(merged);
2269
+ if (fallback == null) return this;
2270
+ const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2271
+ const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
2272
+ const merged = mergeUnresolved(selfTree, fbTree);
2273
+ const hasPlaceholders = containsPlaceholders(merged);
2274
+ const partialRoot = buildPartialHoconFromResObj(merged);
2275
+ return new _Config(partialRoot, {
2276
+ resolved: !hasPlaceholders,
2277
+ parseBaseDir: this._parseBaseDir,
2278
+ originDescription: this._originDescription,
2279
+ resObjRoot: merged,
2280
+ resolveOpts: this._resolveOpts ?? fallback._resolveOpts
2281
+ });
2282
+ }
2283
+ /**
2284
+ * Runs substitution resolution (phase 2) on the stored unresolved tree.
2285
+ * Idempotent: calling resolve() on an already-resolved Config returns an
2286
+ * equivalent resolved Config.
2287
+ *
2288
+ * opts.allowUnresolved (default false) — when true, leaves unresolved
2289
+ * non-optional placeholders in place rather than throwing.
2290
+ * opts.useSystemEnvironment (default true) — when false, env var lookups
2291
+ * are suppressed (hermetic resolution).
2292
+ *
2293
+ * E12 decision 3.
2294
+ */
2295
+ resolve(opts = {}) {
2296
+ if (this._resolved) {
2297
+ return new _Config(this.root, {
2298
+ resolved: true,
2299
+ parseBaseDir: this._parseBaseDir,
2300
+ originDescription: this._originDescription
2301
+ });
2302
+ }
2303
+ const tree = this._resObjRoot;
2304
+ if (!tree) {
2305
+ return new _Config(this.root, { resolved: true });
2306
+ }
2307
+ const resolveOpts = {
2308
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2309
+ throw new Error("no files");
2310
+ } },
2311
+ allowUnresolved: opts.allowUnresolved ?? false,
2312
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2313
+ originDescription: this._originDescription
2314
+ };
2315
+ const resolved = resolveTree(tree, resolveOpts);
2316
+ if (resolved.kind !== "object") throw new Error("resolve: expected object root");
2317
+ if (opts.allowUnresolved) {
2318
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
2319
+ return new _Config(stripped, {
2320
+ resolved: !hadPlaceholders,
2321
+ parseBaseDir: this._parseBaseDir,
2322
+ originDescription: this._originDescription,
2323
+ resObjRoot: hadPlaceholders ? tree : void 0,
2324
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2325
+ });
2326
+ }
2327
+ return new _Config(resolved, {
2328
+ resolved: true,
2329
+ parseBaseDir: this._parseBaseDir,
2330
+ originDescription: this._originDescription
2331
+ });
2332
+ }
2333
+ /**
2334
+ * Resolves receiver substitutions using source as lookup context.
2335
+ * Source's keys are NOT merged into the result — differs from
2336
+ * `this.withFallback(source).resolve()` which includes source keys.
2337
+ *
2338
+ * Precondition (E12 decision 10): source must be resolved. If
2339
+ * source.isResolved() is false, throws NotResolvedError.
2340
+ *
2341
+ * On an already-resolved receiver, resolveWith is a no-op (returns an
2342
+ * equivalent resolved Config, source keys not included).
2343
+ *
2344
+ * E12 decisions 9, 10.
2345
+ */
2346
+ resolveWith(source, opts = {}) {
2347
+ if (!source.isResolved()) {
2348
+ throw new NotResolvedError("source");
2349
+ }
2350
+ if (this._resolved) {
2351
+ return new _Config(this.root, {
2352
+ resolved: true,
2353
+ parseBaseDir: this._parseBaseDir,
2354
+ originDescription: this._originDescription
2355
+ });
2356
+ }
2357
+ const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2358
+ const srcTree = hoconValueToResObj2(source.root);
2359
+ const merged = mergeUnresolved(receiverTree, srcTree);
2360
+ const resolveOpts = {
2361
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2362
+ throw new Error("no files");
2363
+ } },
2364
+ allowUnresolved: opts.allowUnresolved ?? false,
2365
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2366
+ originDescription: this._originDescription
2367
+ };
2368
+ const resolved = resolveTree(merged, resolveOpts);
2369
+ if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
2370
+ const receiverShapeForFilter = resObjToKeyShape(receiverTree);
2371
+ const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
2372
+ if (opts.allowUnresolved) {
2373
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
2374
+ return new _Config(stripped, {
2375
+ resolved: !hadPlaceholders,
2376
+ parseBaseDir: this._parseBaseDir,
2377
+ originDescription: this._originDescription,
2378
+ resObjRoot: hadPlaceholders ? receiverTree : void 0,
2379
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2380
+ });
2381
+ }
2382
+ return new _Config(filtered, {
2383
+ resolved: true,
2384
+ parseBaseDir: this._parseBaseDir,
2385
+ originDescription: this._originDescription
2386
+ });
1863
2387
  }
1864
2388
  toObject() {
1865
2389
  return hoconToJs(this.root);
1866
2390
  }
2391
+ /**
2392
+ * @internal Test-only: renders this resolved Config's value tree as canonical
2393
+ * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
2394
+ * against Lightbend ground truth. NOT part of the public API.
2395
+ *
2396
+ * Throws if the Config is unresolved (contains placeholders).
2397
+ */
2398
+ _renderJSONForTest() {
2399
+ return renderHoconAsJSON(this.root);
2400
+ }
1867
2401
  lookupNode(path2) {
1868
2402
  const segments = splitConfigPath(path2);
1869
2403
  let current = this.root;
@@ -1875,9 +2409,40 @@ var Config = class _Config {
1875
2409
  }
1876
2410
  return current;
1877
2411
  }
2412
+ /**
2413
+ * _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
2414
+ * return true if the subtree there contains any unresolved placeholder.
2415
+ * Used by getConfig / getList (T2) to detect transitive unresolved placeholders
2416
+ * in object / array subtrees that would otherwise appear to be "resolved" in the
2417
+ * partial HoconValue root (because individual leaf fields are omitted rather than
2418
+ * the whole subtree when their value is a direct placeholder scalar, but object
2419
+ * sub-containers are retained in the partial root with the placeholder-keyed
2420
+ * fields omitted from them — so the object appears "present but empty-ish").
2421
+ */
2422
+ _resObjRootSubtreeHasPlaceholders(path2) {
2423
+ const tree = this._resObjRoot;
2424
+ if (!tree) return false;
2425
+ const segments = splitConfigPath(path2);
2426
+ let cur = tree;
2427
+ for (const seg of segments) {
2428
+ const val = cur.fields.get(seg);
2429
+ if (val === void 0) return false;
2430
+ if (isResObj(val)) {
2431
+ cur = val;
2432
+ } else {
2433
+ return valContainsPlaceholders(val);
2434
+ }
2435
+ }
2436
+ return containsPlaceholders(cur);
2437
+ }
1878
2438
  requireScalar(path2) {
1879
2439
  const v = this.lookupNode(path2);
1880
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2440
+ if (v === void 0) {
2441
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2442
+ throw new NotResolvedError(path2);
2443
+ }
2444
+ throw new ConfigError(`path not found: ${path2}`, path2);
2445
+ }
1881
2446
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
1882
2447
  return v;
1883
2448
  }
@@ -1940,28 +2505,89 @@ function hoconToJs(v) {
1940
2505
  case "array":
1941
2506
  return v.items.map(hoconToJs);
1942
2507
  case "object": {
1943
- const obj = {};
2508
+ const obj = /* @__PURE__ */ Object.create(null);
1944
2509
  for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
1945
- return obj;
2510
+ return Object.assign({}, obj);
1946
2511
  }
1947
2512
  }
1948
2513
  }
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));
2514
+ function resObjToKeyShape(tree) {
2515
+ const fields = /* @__PURE__ */ new Map();
2516
+ for (const [k, v] of tree.fields) {
2517
+ if (isSubst(v) || isConcat(v) || isAppend(v)) {
2518
+ fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2519
+ } else if (isResObj(v)) {
2520
+ fields.set(k, resObjToKeyShape(v));
1955
2521
  } else {
1956
- merged.set(k, v);
2522
+ fields.set(k, v);
1957
2523
  }
1958
2524
  }
1959
- return { kind: "object", fields: merged };
2525
+ return { kind: "object", fields };
2526
+ }
2527
+ function stripPlaceholderFields(v) {
2528
+ const fields = /* @__PURE__ */ new Map();
2529
+ let hadPlaceholders = false;
2530
+ for (const [k, val] of v.fields) {
2531
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2532
+ hadPlaceholders = true;
2533
+ } else if (val.kind === "object") {
2534
+ const inner = stripPlaceholderFields(val);
2535
+ if (inner.hadPlaceholders) hadPlaceholders = true;
2536
+ fields.set(k, inner.stripped);
2537
+ } else {
2538
+ fields.set(k, val);
2539
+ }
2540
+ }
2541
+ return { stripped: { kind: "object", fields }, hadPlaceholders };
2542
+ }
2543
+ function filterByReceiverShape(resolved, receiverShape) {
2544
+ const fields = /* @__PURE__ */ new Map();
2545
+ for (const [k, rv] of resolved.fields) {
2546
+ if (!receiverShape.fields.has(k)) continue;
2547
+ const receiverVal = receiverShape.fields.get(k);
2548
+ if (rv.kind === "object" && receiverVal.kind === "object") {
2549
+ fields.set(k, filterByReceiverShape(rv, receiverVal));
2550
+ } else {
2551
+ fields.set(k, rv);
2552
+ }
2553
+ }
2554
+ return { kind: "object", fields };
2555
+ }
2556
+ function renderHoconAsJSON(v) {
2557
+ switch (v.kind) {
2558
+ case "scalar": {
2559
+ switch (v.valueType) {
2560
+ case "null":
2561
+ return "null";
2562
+ case "boolean":
2563
+ return v.raw;
2564
+ case "number": {
2565
+ const n = Number(v.raw);
2566
+ if (Number.isFinite(n)) return v.raw;
2567
+ return JSON.stringify(v.raw);
2568
+ }
2569
+ case "string":
2570
+ return JSON.stringify(v.raw);
2571
+ }
2572
+ break;
2573
+ }
2574
+ case "array": {
2575
+ const items = v.items.map(renderHoconAsJSON);
2576
+ return `[${items.join(",")}]`;
2577
+ }
2578
+ case "object": {
2579
+ const keys = [...v.fields.keys()].sort();
2580
+ const pairs = keys.map((k) => {
2581
+ const val = v.fields.get(k);
2582
+ return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
2583
+ });
2584
+ return `{${pairs.join(",")}}`;
2585
+ }
2586
+ }
2587
+ throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
1960
2588
  }
1961
2589
 
1962
2590
  // src/parse.ts
1963
- var fs = __toESM(require("fs"), 1);
1964
- var path = __toESM(require("path"), 1);
1965
2591
  function getEnv(opts) {
1966
2592
  if (opts.env !== void 0) return opts.env;
1967
2593
  if (typeof process !== "undefined" && process.env) return process.env;
@@ -1978,7 +2604,10 @@ function buildResolveContext(input, opts) {
1978
2604
  env: getEnv(opts),
1979
2605
  baseDir: opts.baseDir,
1980
2606
  readFileSync: opts.readFileSync ?? defaultReadFileSync,
1981
- readFile: opts.readFile
2607
+ readFile: opts.readFile,
2608
+ originDescription: opts.originDescription,
2609
+ resolveFrom: opts.resolveFrom,
2610
+ packageResolver: opts.packageResolver
1982
2611
  };
1983
2612
  return { ast, resolveOpts };
1984
2613
  }
@@ -1986,7 +2615,7 @@ function parse(input, opts = {}) {
1986
2615
  const { ast, resolveOpts } = buildResolveContext(input, opts);
1987
2616
  const value = resolve2(ast, resolveOpts);
1988
2617
  if (value.kind !== "object") throw new Error("resolved value is not an object");
1989
- return new Config(value);
2618
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
1990
2619
  }
1991
2620
  function parseFile(filePath, opts = {}) {
1992
2621
  if (typeof process === "undefined" && !opts.readFileSync) {