@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.js CHANGED
@@ -195,6 +195,10 @@ function parseBytes(value, outputUnit = "B") {
195
195
  return outputUnit === "B" ? Math.round(result) : result;
196
196
  }
197
197
 
198
+ // src/parse.ts
199
+ import * as fs from "fs";
200
+ import * as path from "path";
201
+
198
202
  // src/errors.ts
199
203
  var ParseError = class extends Error {
200
204
  constructor(message, line, col, file) {
@@ -215,6 +219,14 @@ var ResolveError = class extends Error {
215
219
  this.name = "ResolveError";
216
220
  }
217
221
  };
222
+ var PackageLookupError = class extends ResolveError {
223
+ constructor(message, identifier, packageFile, line, col) {
224
+ super(message, `${identifier}/${packageFile}`, line, col);
225
+ this.identifier = identifier;
226
+ this.packageFile = packageFile;
227
+ this.name = "PackageLookupError";
228
+ }
229
+ };
218
230
  var ConfigError = class extends Error {
219
231
  constructor(message, path2) {
220
232
  super(message);
@@ -222,6 +234,156 @@ var ConfigError = class extends Error {
222
234
  this.name = "ConfigError";
223
235
  }
224
236
  };
237
+ var NotResolvedError = class extends ConfigError {
238
+ constructor(path2) {
239
+ super(`value at path "${path2}" is not resolved (call resolve() first)`, path2);
240
+ this.name = "NotResolvedError";
241
+ }
242
+ };
243
+
244
+ // src/internal/resolver/types.ts
245
+ var separatorValues = /* @__PURE__ */ new WeakSet();
246
+ function isSubst(v) {
247
+ return v._kind === "subst-placeholder";
248
+ }
249
+ function isConcat(v) {
250
+ return v._kind === "concat-placeholder";
251
+ }
252
+ function isAppend(v) {
253
+ return v._kind === "append-placeholder";
254
+ }
255
+ function isResObj(v) {
256
+ return v._kind === "res-obj";
257
+ }
258
+ function makeResObj() {
259
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
260
+ }
261
+ function mergeUnresolved(receiver, fallback) {
262
+ const result = makeResObj();
263
+ for (const [k, v] of fallback.fields) {
264
+ result.fields.set(k, v);
265
+ }
266
+ for (const [k, v] of fallback.priorValues) {
267
+ result.priorValues.set(k, v);
268
+ }
269
+ for (const [k, rv] of receiver.fields) {
270
+ const existing = result.fields.get(k);
271
+ if (existing !== void 0) {
272
+ const recPrior = receiver.priorValues.get(k);
273
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
274
+ result.fields.set(k, rv);
275
+ continue;
276
+ }
277
+ if (isResObj(rv) && isResObj(existing)) {
278
+ result.fields.set(k, mergeUnresolved(rv, existing));
279
+ continue;
280
+ }
281
+ result.priorValues.set(k, existing);
282
+ }
283
+ result.fields.set(k, rv);
284
+ }
285
+ for (const [k, v] of receiver.priorValues) {
286
+ if (receiver.fields.has(k)) {
287
+ result.priorValues.set(k, v);
288
+ }
289
+ }
290
+ return result;
291
+ }
292
+
293
+ // src/internal/resolver/utils.ts
294
+ function segmentsToKey(segments) {
295
+ return segments.map((s) => {
296
+ const t = s.text;
297
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
298
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
299
+ }
300
+ return t;
301
+ }).join(".");
302
+ }
303
+ function lookupPath(root, segments) {
304
+ const [head, ...tail] = segments;
305
+ if (head === void 0) return void 0;
306
+ const key = head.text;
307
+ const val = root.fields.get(key);
308
+ if (val === void 0) return void 0;
309
+ if (tail.length === 0) return val;
310
+ if (isResObj(val)) return lookupPath(val, tail);
311
+ return void 0;
312
+ }
313
+ function lookupResObj(root, segments) {
314
+ let cur = root;
315
+ for (const seg of segments) {
316
+ const val = cur.fields.get(seg.text);
317
+ if (val === void 0 || !isResObj(val)) return void 0;
318
+ cur = val;
319
+ }
320
+ return cur;
321
+ }
322
+ function deepMergeHoconValues(base, overlay) {
323
+ const merged = new Map(base.fields);
324
+ for (const [k, v] of overlay.fields) {
325
+ const existing = merged.get(k);
326
+ if (existing?.kind === "object" && v.kind === "object") {
327
+ merged.set(k, deepMergeHoconValues(existing, v));
328
+ } else {
329
+ merged.set(k, v);
330
+ }
331
+ }
332
+ return { kind: "object", fields: merged };
333
+ }
334
+ function deepMergeResObjInto(dst, src) {
335
+ for (const [k, srcVal] of src.fields) {
336
+ const dstVal = dst.fields.get(k);
337
+ if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
338
+ deepMergeResObjInto(dstVal, srcVal);
339
+ } else {
340
+ if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
341
+ dst.fields.set(k, srcVal);
342
+ }
343
+ }
344
+ for (const [k, srcPrior] of src.priorValues) {
345
+ if (!dst.priorValues.has(k)) {
346
+ dst.priorValues.set(k, srcPrior);
347
+ }
348
+ }
349
+ }
350
+ function hoconValueToResObj(hv) {
351
+ const obj = makeResObj();
352
+ if (hv.kind !== "object") return obj;
353
+ for (const [key, val] of hv.fields) {
354
+ if (val.kind === "object") {
355
+ obj.fields.set(key, hoconValueToResObj(val));
356
+ } else {
357
+ obj.fields.set(key, val);
358
+ }
359
+ }
360
+ return obj;
361
+ }
362
+ function isFileNotFoundError(e) {
363
+ if (!(e instanceof Error)) return false;
364
+ const code = e.code;
365
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
366
+ const msg = e.message.toLowerCase();
367
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
368
+ }
369
+
370
+ // src/internal/resolver/include-loader.ts
371
+ import { createRequire } from "module";
372
+ import * as nodePath from "path";
373
+
374
+ // src/internal/parser/empty-check.ts
375
+ function hasContentTokens(tokens) {
376
+ return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
377
+ }
378
+ function assertNonEmptyDocument(tokens, sourceDescriptor) {
379
+ if (!hasContentTokens(tokens)) {
380
+ throw new ParseError(
381
+ `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
382
+ 1,
383
+ 1
384
+ );
385
+ }
386
+ }
225
387
 
226
388
  // src/internal/lexer/lexer.ts
227
389
  function isHoconWhitespace(ch) {
@@ -786,58 +948,137 @@ var Parser = class {
786
948
  const p = this.currentPos();
787
949
  this.skip("newline");
788
950
  const t = this.peek();
789
- let path2;
790
- let required = false;
791
- let isFile = false;
792
951
  if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
793
952
  if (t.value === "required") {
794
- const next2 = this.peek(1);
795
- if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
953
+ const next = this.peek(1);
954
+ if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
796
955
  throw new ParseError("include required must be followed by (", t.line, t.col);
797
956
  }
798
957
  }
799
- required = true;
800
958
  this.advance();
801
959
  const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
802
- if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
960
+ if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
803
961
  throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
804
962
  }
805
- if (innerPrefix.startsWith("file")) {
806
- isFile = true;
963
+ const nextTok = this.peek();
964
+ if (innerPrefix === "package" || innerPrefix.startsWith("package(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "package(" || nextTok.value.startsWith("package("))) {
965
+ const consumePackageToken = innerPrefix === "";
966
+ const { qualifier, path: path3 } = this.parsePackageArgs(consumePackageToken);
967
+ return this.makeIncludeField(qualifier, path3, true, p);
807
968
  }
808
- const next = this.peek();
809
- if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
810
- throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
811
- }
812
- if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
813
- isFile = true;
814
- }
815
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
816
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
817
- path2 = this.advance().value;
818
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
819
- } else if (t.kind === "string") {
820
- path2 = this.advance().value;
821
- } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
822
- isFile = true;
969
+ if (innerPrefix === "file" || innerPrefix.startsWith("file(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "file(" || nextTok.value === "file")) {
970
+ if (innerPrefix === "") {
971
+ this.advance();
972
+ }
973
+ const path3 = this.parseQuotedPathSkipWrapper(t);
974
+ return this.makeIncludeField({ kind: "file" }, path3, true, p);
975
+ }
976
+ if (innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "url" || nextTok.value.startsWith("url(") || nextTok.value === "classpath" || nextTok.value.startsWith("classpath("))) {
977
+ throw new ParseError("include url(...) and classpath(...) are not supported", nextTok.line, nextTok.col);
978
+ }
979
+ if (innerPrefix !== "") {
980
+ if (innerPrefix === ")") {
981
+ throw new ParseError("include required(...) must contain a path", t.line, t.col);
982
+ }
983
+ const name = innerPrefix.replace(/\(.*$/, "").replace(/\).*$/, "");
984
+ throw new ParseError(`unknown include qualifier inside required(): "${name}"`, t.line, t.col);
985
+ }
986
+ const path2 = this.parseQuotedPathSkipWrapper(t);
987
+ return this.makeIncludeField({ kind: "bare" }, path2, true, p);
988
+ }
989
+ if (t.kind === "string") {
990
+ const path2 = this.advance().value;
991
+ return this.makeIncludeField({ kind: "bare" }, path2, false, p);
992
+ }
993
+ if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
823
994
  this.advance();
824
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
825
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
826
- path2 = this.advance().value;
827
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
828
- } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
995
+ const path2 = this.parseQuotedPathSkipWrapper(t);
996
+ return this.makeIncludeField({ kind: "file" }, path2, false, p);
997
+ }
998
+ if (t.kind === "unquoted" && (t.value === "package(" || t.value.startsWith("package("))) {
999
+ const { qualifier, path: path2 } = this.parsePackageArgs();
1000
+ return this.makeIncludeField(qualifier, path2, false, p);
1001
+ }
1002
+ if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
829
1003
  throw new ParseError("include url(...) is not supported", t.line, t.col);
830
- } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
1004
+ }
1005
+ if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
831
1006
  throw new ParseError("include classpath(...) is not supported", t.line, t.col);
832
- } else {
833
- throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
834
1007
  }
835
- return {
836
- key: [],
837
- value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
838
- append: false,
839
- pos: p
840
- };
1008
+ throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
1009
+ }
1010
+ makeIncludeField(qualifier, path2, required, pos) {
1011
+ return { key: [], value: { kind: "include", qualifier, path: path2, required, pos }, append: false, pos };
1012
+ }
1013
+ /**
1014
+ * Skip past include-syntax wrapper tokens to find the quoted path string.
1015
+ * Strict: only allows `(`, `)`, and bare qualifier keywords (file / file( /
1016
+ * package / package( / url / url( / classpath / classpath() between the
1017
+ * directive and the quoted path. Anything else is a parse error rather than
1018
+ * silently swallowed (see ts#113).
1019
+ */
1020
+ parseQuotedPathSkipWrapper(errTok) {
1021
+ while (this.isIncludeWrapperToken(this.peek())) this.advance();
1022
+ if (this.peek().kind !== "string") {
1023
+ const tok = this.peek();
1024
+ if (tok.kind === "eof") throw new ParseError("expected include path", errTok.line, errTok.col);
1025
+ const desc = tok.kind === "unquoted" ? `unquoted "${tok.value}"` : tok.kind;
1026
+ throw new ParseError(`expected quoted include path, got ${desc}`, tok.line, tok.col);
1027
+ }
1028
+ const path2 = this.advance().value;
1029
+ while (this.peek().kind === "unquoted" && (this.peek().value === ")" || this.peek().value === "))")) {
1030
+ this.advance();
1031
+ }
1032
+ const after = this.peek();
1033
+ if (after.kind !== "newline" && after.kind !== "rbrace" && after.kind !== "eof" && after.kind !== "comma") {
1034
+ const desc = after.kind === "unquoted" ? `unquoted "${after.value}"` : after.kind;
1035
+ throw new ParseError(`unexpected token after include path: ${desc}`, after.line, after.col);
1036
+ }
1037
+ return path2;
1038
+ }
1039
+ isIncludeWrapperToken(tok) {
1040
+ return tok.kind === "unquoted" && tok.value === "(";
1041
+ }
1042
+ /**
1043
+ * Parse the body of `package("identifier", "file")`.
1044
+ *
1045
+ * @param consumePackageToken - When true (default), the current token is `package(` and
1046
+ * must be consumed first. When false, the lexer already bundled `package(` into the
1047
+ * preceding token (e.g. `required(package("`), so we are already positioned at the
1048
+ * first string argument.
1049
+ */
1050
+ parsePackageArgs(consumePackageToken = true) {
1051
+ const pkgTok = consumePackageToken ? this.advance() : this.peek();
1052
+ this.skip("newline");
1053
+ const idTok = this.peek();
1054
+ if (idTok.kind !== "string") {
1055
+ throw new ParseError("include package: expected quoted identifier as first argument", pkgTok.line, pkgTok.col);
1056
+ }
1057
+ const identifier = this.advance().value;
1058
+ this.skip("newline");
1059
+ if (this.peek().kind !== "comma") {
1060
+ const after = this.peek();
1061
+ throw new ParseError(
1062
+ after.kind === "string" ? "include package: missing comma between identifier and file arguments" : "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
1063
+ pkgTok.line,
1064
+ pkgTok.col
1065
+ );
1066
+ }
1067
+ this.advance();
1068
+ this.skip("newline");
1069
+ const fileTok = this.peek();
1070
+ if (fileTok.kind !== "string") {
1071
+ throw new ParseError(
1072
+ "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
1073
+ pkgTok.line,
1074
+ pkgTok.col
1075
+ );
1076
+ }
1077
+ const filePath = this.advance().value;
1078
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") {
1079
+ this.advance();
1080
+ }
1081
+ return { qualifier: { kind: "package", identifier }, path: filePath };
841
1082
  }
842
1083
  parseValue() {
843
1084
  const p = this.currentPos();
@@ -878,7 +1119,9 @@ var Parser = class {
878
1119
  parts.push(node);
879
1120
  }
880
1121
  if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
881
- if (parts.length === 1) return parts[0];
1122
+ if (parts.length === 1) {
1123
+ return parts[0];
1124
+ }
882
1125
  return { kind: "concat", nodes: parts, pos: p };
883
1126
  }
884
1127
  parseArray() {
@@ -910,118 +1153,6 @@ function parseTokens(tokens) {
910
1153
  return new Parser(tokens).parse();
911
1154
  }
912
1155
 
913
- // src/internal/parser/empty-check.ts
914
- function hasContentTokens(tokens) {
915
- return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
916
- }
917
- function assertNonEmptyDocument(tokens, sourceDescriptor) {
918
- if (!hasContentTokens(tokens)) {
919
- throw new ParseError(
920
- `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
921
- 1,
922
- 1
923
- );
924
- }
925
- }
926
-
927
- // src/internal/resolver/types.ts
928
- var separatorValues = /* @__PURE__ */ new WeakSet();
929
- function isSubst(v) {
930
- return v._kind === "subst-placeholder";
931
- }
932
- function isConcat(v) {
933
- return v._kind === "concat-placeholder";
934
- }
935
- function isAppend(v) {
936
- return v._kind === "append-placeholder";
937
- }
938
- function isResObj(v) {
939
- return v._kind === "res-obj";
940
- }
941
- function makeResObj() {
942
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
943
- }
944
-
945
- // src/internal/resolver/utils.ts
946
- function segmentsToKey(segments) {
947
- return segments.map((s) => {
948
- const t = s.text;
949
- if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
950
- return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
951
- }
952
- return t;
953
- }).join(".");
954
- }
955
- function lookupPath(root, segments) {
956
- const [head, ...tail] = segments;
957
- if (head === void 0) return void 0;
958
- const key = head.text;
959
- const val = root.fields.get(key);
960
- if (val === void 0) return void 0;
961
- if (tail.length === 0) return val;
962
- if (isResObj(val)) return lookupPath(val, tail);
963
- return void 0;
964
- }
965
- function lookupResObj(root, segments) {
966
- let cur = root;
967
- for (const seg of segments) {
968
- const val = cur.fields.get(seg.text);
969
- if (val === void 0 || !isResObj(val)) return void 0;
970
- cur = val;
971
- }
972
- return cur;
973
- }
974
- function deepMergeHoconValues(base, overlay) {
975
- const merged = new Map(base.fields);
976
- for (const [k, v] of overlay.fields) {
977
- const existing = merged.get(k);
978
- if (existing?.kind === "object" && v.kind === "object") {
979
- merged.set(k, deepMergeHoconValues(existing, v));
980
- } else {
981
- merged.set(k, v);
982
- }
983
- }
984
- return { kind: "object", fields: merged };
985
- }
986
- function deepMergeResObjInto(dst, src) {
987
- for (const [k, srcVal] of src.fields) {
988
- const dstVal = dst.fields.get(k);
989
- if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
990
- deepMergeResObjInto(dstVal, srcVal);
991
- } else {
992
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
993
- dst.fields.set(k, srcVal);
994
- }
995
- }
996
- for (const [k, srcPrior] of src.priorValues) {
997
- if (!dst.priorValues.has(k)) {
998
- dst.priorValues.set(k, srcPrior);
999
- }
1000
- }
1001
- }
1002
- function hoconValueToResObj(hv) {
1003
- const obj = makeResObj();
1004
- if (hv.kind !== "object") return obj;
1005
- for (const [key, val] of hv.fields) {
1006
- if (val.kind === "object") {
1007
- obj.fields.set(key, hoconValueToResObj(val));
1008
- } else {
1009
- obj.fields.set(key, val);
1010
- }
1011
- }
1012
- return obj;
1013
- }
1014
- function isFileNotFoundError(e) {
1015
- if (!(e instanceof Error)) return false;
1016
- const code = e.code;
1017
- if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1018
- const msg = e.message.toLowerCase();
1019
- return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
1020
- }
1021
-
1022
- // src/internal/resolver/include-loader.ts
1023
- import * as nodePath from "path";
1024
-
1025
1156
  // src/internal/properties/properties.ts
1026
1157
  function parseProperties(input) {
1027
1158
  const root = /* @__PURE__ */ Object.create(null);
@@ -1081,6 +1212,84 @@ function recordToHoconValue(obj) {
1081
1212
  }
1082
1213
 
1083
1214
  // src/internal/resolver/include-loader.ts
1215
+ var _require = createRequire(import.meta.url);
1216
+ function isPnpPath(p) {
1217
+ return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1218
+ }
1219
+ function validatePackageFile(file, identifier) {
1220
+ if (file.length === 0) {
1221
+ throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
1222
+ }
1223
+ if (file.includes("\\")) {
1224
+ throw new ParseError(
1225
+ `include package("${identifier}", "${file}"): backslash not allowed in file argument (use forward slash)`,
1226
+ 0,
1227
+ 0
1228
+ );
1229
+ }
1230
+ if (file.startsWith("/")) {
1231
+ throw new ParseError(
1232
+ `include package("${identifier}", "${file}"): absolute path not allowed in file argument`,
1233
+ 0,
1234
+ 0
1235
+ );
1236
+ }
1237
+ if (/(?:^|\/)\.\.?(?:\/|$)/u.test(file)) {
1238
+ throw new ParseError(
1239
+ `include package("${identifier}", "${file}"): path traversal (. or ..) not allowed in file argument`,
1240
+ 0,
1241
+ 0
1242
+ );
1243
+ }
1244
+ if (file.includes("//")) {
1245
+ throw new ParseError(
1246
+ `include package("${identifier}", "${file}"): consecutive slashes not allowed in file argument`,
1247
+ 0,
1248
+ 0
1249
+ );
1250
+ }
1251
+ }
1252
+ function validatePackageIdentifier(identifier) {
1253
+ if (identifier.length === 0) {
1254
+ throw new ParseError("include package: identifier argument must be non-empty", 0, 0);
1255
+ }
1256
+ }
1257
+ function defaultPackageResolver(identifier, file, includingFile, baseDir, resolveFrom) {
1258
+ const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
1259
+ let resolved;
1260
+ try {
1261
+ resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
1262
+ } catch {
1263
+ throw new PackageLookupError(
1264
+ `include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
1265
+ identifier,
1266
+ file,
1267
+ 0,
1268
+ 0
1269
+ );
1270
+ }
1271
+ if (isPnpPath(resolved)) {
1272
+ throw new ResolveError(
1273
+ `include package("${identifier}", "${file}"): Yarn Berry PnP store paths are not supported. Provide a custom packageResolver option to handle PnP resolution.`,
1274
+ `${identifier}/${file}`,
1275
+ 0,
1276
+ 0
1277
+ );
1278
+ }
1279
+ const fileParts = file.split("/");
1280
+ const expectedBasename = fileParts[fileParts.length - 1] ?? file;
1281
+ const resolvedBasename = nodePath.basename(resolved);
1282
+ if (resolvedBasename !== expectedBasename) {
1283
+ throw new PackageLookupError(
1284
+ `include package("${identifier}", "${file}"): case mismatch \u2014 resolved "${resolvedBasename}" but expected "${expectedBasename}" (E11 decision 5: case-sensitive)`,
1285
+ identifier,
1286
+ file,
1287
+ 0,
1288
+ 0
1289
+ );
1290
+ }
1291
+ return resolved;
1292
+ }
1084
1293
  var IncludeLoader = class {
1085
1294
  constructor(opts) {
1086
1295
  this.opts = opts;
@@ -1160,8 +1369,77 @@ var IncludeLoader = class {
1160
1369
  }
1161
1370
  return merged;
1162
1371
  }
1372
+ /**
1373
+ * Resolve and load `include package("identifier", "file")`.
1374
+ * Validates the identifier and file arguments, resolves via the package resolver,
1375
+ * performs cycle detection using a JSON-serialised cycle key, and parses the content.
1376
+ */
1377
+ // `required` is reserved for future optional-package semantics (E11 decision 7);
1378
+ // currently all package lookup failures throw regardless of this flag.
1379
+ loadPackage(identifier, file, _required) {
1380
+ validatePackageIdentifier(identifier);
1381
+ validatePackageFile(file, identifier);
1382
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFileSync: readFileSync2 } = this.opts;
1383
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1384
+ if (includeStack.includes(cycleKey)) {
1385
+ throw new ResolveError(
1386
+ `circular include: package("${identifier}", "${file}")`,
1387
+ `${identifier}/${file}`,
1388
+ 0,
1389
+ 0
1390
+ );
1391
+ }
1392
+ if (includeStack.length >= 50) {
1393
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1394
+ }
1395
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1396
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1397
+ const content = readFileSync2(resolvedPath);
1398
+ if (content.length === 0) return makeResObj();
1399
+ const tokens = tokenize(content);
1400
+ assertNonEmptyDocument(tokens, resolvedPath);
1401
+ const ast = parseTokens(tokens);
1402
+ return this.onBuildResObj(ast, {
1403
+ ...this.opts,
1404
+ baseDir: nodePath.dirname(resolvedPath),
1405
+ includeStack: [...includeStack, cycleKey]
1406
+ });
1407
+ }
1408
+ /**
1409
+ * Async variant of `loadPackage`.
1410
+ */
1411
+ async loadPackageAsync(identifier, file, _required) {
1412
+ validatePackageIdentifier(identifier);
1413
+ validatePackageFile(file, identifier);
1414
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFile, readFileSync: readFileSync2 } = this.opts;
1415
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1416
+ if (includeStack.includes(cycleKey)) {
1417
+ throw new ResolveError(
1418
+ `circular include: package("${identifier}", "${file}")`,
1419
+ `${identifier}/${file}`,
1420
+ 0,
1421
+ 0
1422
+ );
1423
+ }
1424
+ if (includeStack.length >= 50) {
1425
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1426
+ }
1427
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1428
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1429
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1430
+ const content = await read(resolvedPath);
1431
+ if (content.length === 0) return makeResObj();
1432
+ const tokens = tokenize(content);
1433
+ assertNonEmptyDocument(tokens, resolvedPath);
1434
+ const ast = parseTokens(tokens);
1435
+ return this.onBuildResObjAsync(ast, {
1436
+ ...this.opts,
1437
+ baseDir: nodePath.dirname(resolvedPath),
1438
+ includeStack: [...includeStack, cycleKey]
1439
+ });
1440
+ }
1163
1441
  loadSingle(candidate) {
1164
- const { readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1442
+ const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
1165
1443
  if (includeStack.includes(candidate)) {
1166
1444
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1167
1445
  }
@@ -1176,17 +1454,18 @@ var IncludeLoader = class {
1176
1454
  return hoconValueToResObj(propertiesToHoconValue(content));
1177
1455
  }
1178
1456
  const tokens = tokenize(content);
1179
- assertNonEmptyDocument(tokens, candidate);
1457
+ if (!hasContentTokens(tokens)) {
1458
+ return makeResObj();
1459
+ }
1180
1460
  const ast = parseTokens(tokens);
1181
1461
  return this.onBuildResObj(ast, {
1182
- env,
1462
+ ...this.opts,
1183
1463
  baseDir: nodePath.dirname(candidate),
1184
- readFileSync: readFileSync2,
1185
1464
  includeStack: [...includeStack, candidate]
1186
1465
  });
1187
1466
  }
1188
1467
  async loadSingleAsync(candidate) {
1189
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1468
+ const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
1190
1469
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1191
1470
  if (includeStack.includes(candidate)) {
1192
1471
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
@@ -1202,13 +1481,13 @@ var IncludeLoader = class {
1202
1481
  return hoconValueToResObj(propertiesToHoconValue(content));
1203
1482
  }
1204
1483
  const tokens = tokenize(content);
1205
- assertNonEmptyDocument(tokens, candidate);
1484
+ if (!hasContentTokens(tokens)) {
1485
+ return makeResObj();
1486
+ }
1206
1487
  const ast = parseTokens(tokens);
1207
1488
  return this.onBuildResObjAsync(ast, {
1208
- env,
1489
+ ...this.opts,
1209
1490
  baseDir: nodePath.dirname(candidate),
1210
- readFileSync: readFileSync2,
1211
- readFile,
1212
1491
  includeStack: [...includeStack, candidate]
1213
1492
  });
1214
1493
  }
@@ -1217,7 +1496,6 @@ var IncludeLoader = class {
1217
1496
  // src/internal/resolver/structure-builder.ts
1218
1497
  var StructureBuilder = class _StructureBuilder {
1219
1498
  constructor(opts) {
1220
- this.opts = opts;
1221
1499
  this.loader = new IncludeLoader(opts);
1222
1500
  this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1223
1501
  this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
@@ -1244,7 +1522,13 @@ var StructureBuilder = class _StructureBuilder {
1244
1522
  }
1245
1523
  applyField(obj, field, pathPrefix) {
1246
1524
  if (field.key.length === 0 && field.value.kind === "include") {
1247
- const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
1525
+ const { qualifier, path: path2, required } = field.value;
1526
+ let included;
1527
+ if (qualifier.kind === "package") {
1528
+ included = this.loader.loadPackage(qualifier.identifier, path2, required);
1529
+ } else {
1530
+ included = this.loader.load(path2, required, qualifier.kind === "file");
1531
+ }
1248
1532
  if (pathPrefix.length > 0) {
1249
1533
  this.relativizeResObj(included, pathPrefix);
1250
1534
  }
@@ -1286,7 +1570,13 @@ var StructureBuilder = class _StructureBuilder {
1286
1570
  }
1287
1571
  async applyFieldAsync(obj, field, pathPrefix) {
1288
1572
  if (field.key.length === 0 && field.value.kind === "include") {
1289
- const included = await this.loader.loadAsync(field.value.path, field.value.required, field.value.isFile);
1573
+ const { qualifier, path: path2, required } = field.value;
1574
+ let included;
1575
+ if (qualifier.kind === "package") {
1576
+ included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
1577
+ } else {
1578
+ included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
1579
+ }
1290
1580
  if (pathPrefix.length > 0) {
1291
1581
  this.relativizeResObj(included, pathPrefix);
1292
1582
  }
@@ -1454,6 +1744,10 @@ var SubstitutionResolver = class {
1454
1744
  // xx.hocon PR (see Phase 6 #3f close-out notes).
1455
1745
  this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1456
1746
  }
1747
+ /** Returns a prefix for error messages, including origin info when set. */
1748
+ originPrefix() {
1749
+ return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
1750
+ }
1457
1751
  resolve() {
1458
1752
  return this.resolveResObj(this.root);
1459
1753
  }
@@ -1494,7 +1788,8 @@ var SubstitutionResolver = class {
1494
1788
  if (isConcat(v)) {
1495
1789
  this.resolvingConcats.add(v);
1496
1790
  try {
1497
- return this.resolveConcat(v.nodes, scope, v.line, v.col);
1791
+ const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
1792
+ return concatResult;
1498
1793
  } finally {
1499
1794
  this.resolvingConcats.delete(v);
1500
1795
  }
@@ -1539,7 +1834,7 @@ var SubstitutionResolver = class {
1539
1834
  }
1540
1835
  if (s.optional) return void 0;
1541
1836
  throw new ResolveError(
1542
- `circular substitution: ${key}`,
1837
+ this.originPrefix() + `circular substitution: ${key}`,
1543
1838
  key,
1544
1839
  s.line,
1545
1840
  s.col
@@ -1569,7 +1864,7 @@ var SubstitutionResolver = class {
1569
1864
  }
1570
1865
  if (s.optional) return void 0;
1571
1866
  throw new ResolveError(
1572
- `could not resolve substitution: \${${key}}`,
1867
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1573
1868
  key,
1574
1869
  s.line,
1575
1870
  s.col
@@ -1602,7 +1897,7 @@ var SubstitutionResolver = class {
1602
1897
  if (result !== void 0) this.cache.set(key, result);
1603
1898
  return result;
1604
1899
  }
1605
- if (s.listSuffix) {
1900
+ if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1606
1901
  const result = this.resolveEnvList(s);
1607
1902
  if (result !== void 0) {
1608
1903
  this.cache.set(key, result);
@@ -1611,22 +1906,36 @@ var SubstitutionResolver = class {
1611
1906
  if (s.optional) return void 0;
1612
1907
  const envBase = s.segments.map((seg) => seg.text).join(".");
1613
1908
  throw new ResolveError(
1614
- `could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1909
+ `${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1910
+ key,
1911
+ s.line,
1912
+ s.col
1913
+ );
1914
+ } else if (s.listSuffix) {
1915
+ if (s.optional) return void 0;
1916
+ if (this.opts.allowUnresolved) return s;
1917
+ throw new ResolveError(
1918
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1615
1919
  key,
1616
1920
  s.line,
1617
1921
  s.col
1618
1922
  );
1619
1923
  }
1620
- const envKey = s.segments.map((seg) => seg.text).join(".");
1621
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1622
- if (envVal !== void 0) {
1623
- const result = { kind: "scalar", raw: envVal, valueType: "string" };
1624
- this.cache.set(key, result);
1625
- return result;
1924
+ if (this.opts.useSystemEnvironment !== false) {
1925
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1926
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1927
+ if (envVal !== void 0) {
1928
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1929
+ this.cache.set(key, result);
1930
+ return result;
1931
+ }
1626
1932
  }
1627
1933
  if (s.optional) return void 0;
1934
+ if (this.opts.allowUnresolved) {
1935
+ return s;
1936
+ }
1628
1937
  throw new ResolveError(
1629
- `could not resolve substitution: \${${key}}`,
1938
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1630
1939
  key,
1631
1940
  s.line,
1632
1941
  s.col
@@ -1671,8 +1980,16 @@ var SubstitutionResolver = class {
1671
1980
  return [full, bare];
1672
1981
  }
1673
1982
  resolveConcat(nodes, scope, line = 0, col = 0) {
1674
- const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1675
- if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1983
+ const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
1984
+ if (this.opts.allowUnresolved) {
1985
+ for (const v of rawResolved) {
1986
+ if (v !== void 0 && isSubst(v)) {
1987
+ return v;
1988
+ }
1989
+ }
1990
+ }
1991
+ const resolved = rawResolved.filter((v) => v !== void 0);
1992
+ if (resolved.length === 0) return void 0;
1676
1993
  if (resolved.length === 1) return resolved[0];
1677
1994
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1678
1995
  if (nonSep.length === 0) {
@@ -1746,15 +2063,89 @@ var SubstitutionResolver = class {
1746
2063
  };
1747
2064
 
1748
2065
  // src/internal/resolver/resolver.ts
2066
+ function buildTree(ast, opts) {
2067
+ return new StructureBuilder(opts).build(ast);
2068
+ }
2069
+ function resolveTree(tree, opts) {
2070
+ return new SubstitutionResolver(tree, opts).resolve();
2071
+ }
2072
+ function containsPlaceholders(tree) {
2073
+ for (const val of tree.fields.values()) {
2074
+ if (valContainsPlaceholders(val)) return true;
2075
+ }
2076
+ return false;
2077
+ }
2078
+ function valContainsPlaceholders(v) {
2079
+ const rv = v;
2080
+ if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2081
+ if (isResObj(rv)) return containsPlaceholders(rv);
2082
+ const hv = v;
2083
+ if (hv?.kind === "array") {
2084
+ return hv.items.some((item) => valContainsPlaceholders(item));
2085
+ }
2086
+ return false;
2087
+ }
2088
+ function buildPartialHoconFromResObj(tree) {
2089
+ const fields = /* @__PURE__ */ new Map();
2090
+ for (const [k, v] of tree.fields) {
2091
+ if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2092
+ if (isResObj(v)) {
2093
+ fields.set(k, buildPartialHoconFromResObj(v));
2094
+ } else {
2095
+ if (!valContainsPlaceholders(v)) {
2096
+ fields.set(k, v);
2097
+ }
2098
+ }
2099
+ }
2100
+ }
2101
+ return { kind: "object", fields };
2102
+ }
2103
+ function hoconValueToResObj2(value) {
2104
+ const obj = makeResObj();
2105
+ for (const [k, v] of value.fields) {
2106
+ if (v.kind === "object") {
2107
+ obj.fields.set(k, hoconValueToResObj2(v));
2108
+ } else {
2109
+ obj.fields.set(k, v);
2110
+ }
2111
+ }
2112
+ return obj;
2113
+ }
1749
2114
  function resolve2(ast, opts) {
1750
- const root = new StructureBuilder(opts).build(ast);
1751
- return new SubstitutionResolver(root, opts).resolve();
2115
+ const tree = buildTree(ast, opts);
2116
+ return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
1752
2117
  }
1753
2118
 
1754
2119
  // src/config.ts
1755
2120
  var Config = class _Config {
1756
- constructor(root) {
2121
+ constructor(root, opts) {
1757
2122
  this.root = root;
2123
+ this._resolved = opts?.resolved ?? true;
2124
+ this._parseBaseDir = opts?.parseBaseDir;
2125
+ this._originDescription = opts?.originDescription;
2126
+ this._resObjRoot = opts?.resObjRoot;
2127
+ this._resolveOpts = opts?.resolveOpts;
2128
+ }
2129
+ /** Returns true when the Config's value tree contains no unresolved
2130
+ * substitution placeholders. Whole-config granularity (E12 decision 11). */
2131
+ isResolved() {
2132
+ return this._resolved;
2133
+ }
2134
+ /** @internal Test-only: construct a resolved Config from a HoconValue. */
2135
+ static _fromResolvedValue(root, opts) {
2136
+ return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
2137
+ }
2138
+ /** @internal Construct an unresolved Config carrying a ResObj tree.
2139
+ * Used by parseStringWithOptions (deferred path) in T6. */
2140
+ static _fromUnresolvedResObj(tree, opts) {
2141
+ const partialRoot = buildPartialHoconFromResObj(tree);
2142
+ return new _Config(partialRoot, {
2143
+ resolved: opts.resolved,
2144
+ parseBaseDir: opts.parseBaseDir,
2145
+ originDescription: opts.originDescription,
2146
+ resObjRoot: tree,
2147
+ resolveOpts: opts.resolveOpts
2148
+ });
1758
2149
  }
1759
2150
  get(path2) {
1760
2151
  const v = this.lookupNode(path2);
@@ -1798,13 +2189,29 @@ var Config = class _Config {
1798
2189
  }
1799
2190
  getConfig(path2) {
1800
2191
  const v = this.lookupNode(path2);
1801
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2192
+ if (v === void 0) {
2193
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2194
+ throw new NotResolvedError(path2);
2195
+ }
2196
+ throw new ConfigError(`path not found: ${path2}`, path2);
2197
+ }
1802
2198
  if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
1803
- return new _Config(v);
2199
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2200
+ throw new NotResolvedError(path2);
2201
+ }
2202
+ return new _Config(v, { resolved: this._resolved });
1804
2203
  }
1805
2204
  getList(path2) {
1806
2205
  const v = this.lookupNode(path2);
1807
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2206
+ if (v === void 0) {
2207
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2208
+ throw new NotResolvedError(path2);
2209
+ }
2210
+ throw new ConfigError(`path not found: ${path2}`, path2);
2211
+ }
2212
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2213
+ throw new NotResolvedError(path2);
2214
+ }
1808
2215
  if (v.kind === "object") {
1809
2216
  const converted = numericObjectToArray(v);
1810
2217
  if (converted !== null) return converted.map(hoconToJs);
@@ -1819,12 +2226,138 @@ var Config = class _Config {
1819
2226
  return [...this.root.fields.keys()];
1820
2227
  }
1821
2228
  withFallback(fallback) {
1822
- const merged = deepMergeHocon(this.root, fallback.root);
1823
- return new _Config(merged);
2229
+ if (fallback == null) return this;
2230
+ const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2231
+ const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
2232
+ const merged = mergeUnresolved(selfTree, fbTree);
2233
+ const hasPlaceholders = containsPlaceholders(merged);
2234
+ const partialRoot = buildPartialHoconFromResObj(merged);
2235
+ return new _Config(partialRoot, {
2236
+ resolved: !hasPlaceholders,
2237
+ parseBaseDir: this._parseBaseDir,
2238
+ originDescription: this._originDescription,
2239
+ resObjRoot: merged,
2240
+ resolveOpts: this._resolveOpts ?? fallback._resolveOpts
2241
+ });
2242
+ }
2243
+ /**
2244
+ * Runs substitution resolution (phase 2) on the stored unresolved tree.
2245
+ * Idempotent: calling resolve() on an already-resolved Config returns an
2246
+ * equivalent resolved Config.
2247
+ *
2248
+ * opts.allowUnresolved (default false) — when true, leaves unresolved
2249
+ * non-optional placeholders in place rather than throwing.
2250
+ * opts.useSystemEnvironment (default true) — when false, env var lookups
2251
+ * are suppressed (hermetic resolution).
2252
+ *
2253
+ * E12 decision 3.
2254
+ */
2255
+ resolve(opts = {}) {
2256
+ if (this._resolved) {
2257
+ return new _Config(this.root, {
2258
+ resolved: true,
2259
+ parseBaseDir: this._parseBaseDir,
2260
+ originDescription: this._originDescription
2261
+ });
2262
+ }
2263
+ const tree = this._resObjRoot;
2264
+ if (!tree) {
2265
+ return new _Config(this.root, { resolved: true });
2266
+ }
2267
+ const resolveOpts = {
2268
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2269
+ throw new Error("no files");
2270
+ } },
2271
+ allowUnresolved: opts.allowUnresolved ?? false,
2272
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2273
+ originDescription: this._originDescription
2274
+ };
2275
+ const resolved = resolveTree(tree, resolveOpts);
2276
+ if (resolved.kind !== "object") throw new Error("resolve: expected object root");
2277
+ if (opts.allowUnresolved) {
2278
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
2279
+ return new _Config(stripped, {
2280
+ resolved: !hadPlaceholders,
2281
+ parseBaseDir: this._parseBaseDir,
2282
+ originDescription: this._originDescription,
2283
+ resObjRoot: hadPlaceholders ? tree : void 0,
2284
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2285
+ });
2286
+ }
2287
+ return new _Config(resolved, {
2288
+ resolved: true,
2289
+ parseBaseDir: this._parseBaseDir,
2290
+ originDescription: this._originDescription
2291
+ });
2292
+ }
2293
+ /**
2294
+ * Resolves receiver substitutions using source as lookup context.
2295
+ * Source's keys are NOT merged into the result — differs from
2296
+ * `this.withFallback(source).resolve()` which includes source keys.
2297
+ *
2298
+ * Precondition (E12 decision 10): source must be resolved. If
2299
+ * source.isResolved() is false, throws NotResolvedError.
2300
+ *
2301
+ * On an already-resolved receiver, resolveWith is a no-op (returns an
2302
+ * equivalent resolved Config, source keys not included).
2303
+ *
2304
+ * E12 decisions 9, 10.
2305
+ */
2306
+ resolveWith(source, opts = {}) {
2307
+ if (!source.isResolved()) {
2308
+ throw new NotResolvedError("source");
2309
+ }
2310
+ if (this._resolved) {
2311
+ return new _Config(this.root, {
2312
+ resolved: true,
2313
+ parseBaseDir: this._parseBaseDir,
2314
+ originDescription: this._originDescription
2315
+ });
2316
+ }
2317
+ const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2318
+ const srcTree = hoconValueToResObj2(source.root);
2319
+ const merged = mergeUnresolved(receiverTree, srcTree);
2320
+ const resolveOpts = {
2321
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2322
+ throw new Error("no files");
2323
+ } },
2324
+ allowUnresolved: opts.allowUnresolved ?? false,
2325
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2326
+ originDescription: this._originDescription
2327
+ };
2328
+ const resolved = resolveTree(merged, resolveOpts);
2329
+ if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
2330
+ const receiverShapeForFilter = resObjToKeyShape(receiverTree);
2331
+ const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
2332
+ if (opts.allowUnresolved) {
2333
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
2334
+ return new _Config(stripped, {
2335
+ resolved: !hadPlaceholders,
2336
+ parseBaseDir: this._parseBaseDir,
2337
+ originDescription: this._originDescription,
2338
+ resObjRoot: hadPlaceholders ? receiverTree : void 0,
2339
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2340
+ });
2341
+ }
2342
+ return new _Config(filtered, {
2343
+ resolved: true,
2344
+ parseBaseDir: this._parseBaseDir,
2345
+ originDescription: this._originDescription
2346
+ });
1824
2347
  }
1825
2348
  toObject() {
1826
2349
  return hoconToJs(this.root);
1827
2350
  }
2351
+ /**
2352
+ * @internal Test-only: renders this resolved Config's value tree as canonical
2353
+ * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
2354
+ * against Lightbend ground truth. NOT part of the public API.
2355
+ *
2356
+ * Throws if the Config is unresolved (contains placeholders).
2357
+ */
2358
+ _renderJSONForTest() {
2359
+ return renderHoconAsJSON(this.root);
2360
+ }
1828
2361
  lookupNode(path2) {
1829
2362
  const segments = splitConfigPath(path2);
1830
2363
  let current = this.root;
@@ -1836,9 +2369,40 @@ var Config = class _Config {
1836
2369
  }
1837
2370
  return current;
1838
2371
  }
2372
+ /**
2373
+ * _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
2374
+ * return true if the subtree there contains any unresolved placeholder.
2375
+ * Used by getConfig / getList (T2) to detect transitive unresolved placeholders
2376
+ * in object / array subtrees that would otherwise appear to be "resolved" in the
2377
+ * partial HoconValue root (because individual leaf fields are omitted rather than
2378
+ * the whole subtree when their value is a direct placeholder scalar, but object
2379
+ * sub-containers are retained in the partial root with the placeholder-keyed
2380
+ * fields omitted from them — so the object appears "present but empty-ish").
2381
+ */
2382
+ _resObjRootSubtreeHasPlaceholders(path2) {
2383
+ const tree = this._resObjRoot;
2384
+ if (!tree) return false;
2385
+ const segments = splitConfigPath(path2);
2386
+ let cur = tree;
2387
+ for (const seg of segments) {
2388
+ const val = cur.fields.get(seg);
2389
+ if (val === void 0) return false;
2390
+ if (isResObj(val)) {
2391
+ cur = val;
2392
+ } else {
2393
+ return valContainsPlaceholders(val);
2394
+ }
2395
+ }
2396
+ return containsPlaceholders(cur);
2397
+ }
1839
2398
  requireScalar(path2) {
1840
2399
  const v = this.lookupNode(path2);
1841
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
2400
+ if (v === void 0) {
2401
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2402
+ throw new NotResolvedError(path2);
2403
+ }
2404
+ throw new ConfigError(`path not found: ${path2}`, path2);
2405
+ }
1842
2406
  if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
1843
2407
  return v;
1844
2408
  }
@@ -1901,28 +2465,89 @@ function hoconToJs(v) {
1901
2465
  case "array":
1902
2466
  return v.items.map(hoconToJs);
1903
2467
  case "object": {
1904
- const obj = {};
2468
+ const obj = /* @__PURE__ */ Object.create(null);
1905
2469
  for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
1906
- return obj;
2470
+ return Object.assign({}, obj);
1907
2471
  }
1908
2472
  }
1909
2473
  }
1910
- function deepMergeHocon(receiver, fallback) {
1911
- const merged = new Map(fallback.fields);
1912
- for (const [k, v] of receiver.fields) {
1913
- const fb = merged.get(k);
1914
- if (fb?.kind === "object" && v.kind === "object") {
1915
- merged.set(k, deepMergeHocon(v, fb));
2474
+ function resObjToKeyShape(tree) {
2475
+ const fields = /* @__PURE__ */ new Map();
2476
+ for (const [k, v] of tree.fields) {
2477
+ if (isSubst(v) || isConcat(v) || isAppend(v)) {
2478
+ fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2479
+ } else if (isResObj(v)) {
2480
+ fields.set(k, resObjToKeyShape(v));
1916
2481
  } else {
1917
- merged.set(k, v);
2482
+ fields.set(k, v);
1918
2483
  }
1919
2484
  }
1920
- return { kind: "object", fields: merged };
2485
+ return { kind: "object", fields };
2486
+ }
2487
+ function stripPlaceholderFields(v) {
2488
+ const fields = /* @__PURE__ */ new Map();
2489
+ let hadPlaceholders = false;
2490
+ for (const [k, val] of v.fields) {
2491
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2492
+ hadPlaceholders = true;
2493
+ } else if (val.kind === "object") {
2494
+ const inner = stripPlaceholderFields(val);
2495
+ if (inner.hadPlaceholders) hadPlaceholders = true;
2496
+ fields.set(k, inner.stripped);
2497
+ } else {
2498
+ fields.set(k, val);
2499
+ }
2500
+ }
2501
+ return { stripped: { kind: "object", fields }, hadPlaceholders };
2502
+ }
2503
+ function filterByReceiverShape(resolved, receiverShape) {
2504
+ const fields = /* @__PURE__ */ new Map();
2505
+ for (const [k, rv] of resolved.fields) {
2506
+ if (!receiverShape.fields.has(k)) continue;
2507
+ const receiverVal = receiverShape.fields.get(k);
2508
+ if (rv.kind === "object" && receiverVal.kind === "object") {
2509
+ fields.set(k, filterByReceiverShape(rv, receiverVal));
2510
+ } else {
2511
+ fields.set(k, rv);
2512
+ }
2513
+ }
2514
+ return { kind: "object", fields };
2515
+ }
2516
+ function renderHoconAsJSON(v) {
2517
+ switch (v.kind) {
2518
+ case "scalar": {
2519
+ switch (v.valueType) {
2520
+ case "null":
2521
+ return "null";
2522
+ case "boolean":
2523
+ return v.raw;
2524
+ case "number": {
2525
+ const n = Number(v.raw);
2526
+ if (Number.isFinite(n)) return v.raw;
2527
+ return JSON.stringify(v.raw);
2528
+ }
2529
+ case "string":
2530
+ return JSON.stringify(v.raw);
2531
+ }
2532
+ break;
2533
+ }
2534
+ case "array": {
2535
+ const items = v.items.map(renderHoconAsJSON);
2536
+ return `[${items.join(",")}]`;
2537
+ }
2538
+ case "object": {
2539
+ const keys = [...v.fields.keys()].sort();
2540
+ const pairs = keys.map((k) => {
2541
+ const val = v.fields.get(k);
2542
+ return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
2543
+ });
2544
+ return `{${pairs.join(",")}}`;
2545
+ }
2546
+ }
2547
+ throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
1921
2548
  }
1922
2549
 
1923
2550
  // src/parse.ts
1924
- import * as fs from "fs";
1925
- import * as path from "path";
1926
2551
  function getEnv(opts) {
1927
2552
  if (opts.env !== void 0) return opts.env;
1928
2553
  if (typeof process !== "undefined" && process.env) return process.env;
@@ -1939,7 +2564,10 @@ function buildResolveContext(input, opts) {
1939
2564
  env: getEnv(opts),
1940
2565
  baseDir: opts.baseDir,
1941
2566
  readFileSync: opts.readFileSync ?? defaultReadFileSync,
1942
- readFile: opts.readFile
2567
+ readFile: opts.readFile,
2568
+ originDescription: opts.originDescription,
2569
+ resolveFrom: opts.resolveFrom,
2570
+ packageResolver: opts.packageResolver
1943
2571
  };
1944
2572
  return { ast, resolveOpts };
1945
2573
  }
@@ -1947,7 +2575,7 @@ function parse(input, opts = {}) {
1947
2575
  const { ast, resolveOpts } = buildResolveContext(input, opts);
1948
2576
  const value = resolve2(ast, resolveOpts);
1949
2577
  if (value.kind !== "object") throw new Error("resolved value is not an object");
1950
- return new Config(value);
2578
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
1951
2579
  }
1952
2580
  function parseFile(filePath, opts = {}) {
1953
2581
  if (typeof process === "undefined" && !opts.readFileSync) {