@o3co/ts.hocon 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -32,17 +32,47 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Config: () => Config,
34
34
  ConfigError: () => ConfigError,
35
+ NotResolvedError: () => NotResolvedError,
36
+ PackageLookupError: () => PackageLookupError,
35
37
  ParseError: () => ParseError,
36
38
  ResolveError: () => ResolveError,
39
+ defaultParseOptions: () => defaultParseOptions,
40
+ defaultResolveOptions: () => defaultResolveOptions,
41
+ empty: () => empty,
42
+ fromMap: () => fromMap,
37
43
  parse: () => parse,
38
44
  parseAsync: () => parseAsync,
39
45
  parseFile: () => parseFile,
40
- parseFileAsync: () => parseFileAsync
46
+ parseFileAsync: () => parseFileAsync,
47
+ parseFileWithOptions: () => parseFileWithOptions,
48
+ parseString: () => parseString,
49
+ parseStringWithOptions: () => parseStringWithOptions
41
50
  });
42
51
  module.exports = __toCommonJS(src_exports);
43
52
 
44
53
  // src/coerce.ts
45
54
  var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
55
+ function isHoconWs(cp) {
56
+ if (cp === 9 || cp === 10 || cp === 11 || cp === 12 || cp === 13) return true;
57
+ if (cp >= 28 && cp <= 31) return true;
58
+ if (cp === 32 || cp === 160 || cp === 65279) return true;
59
+ if (cp === 5760) return true;
60
+ if (cp >= 8192 && cp <= 8202) return true;
61
+ if (cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287) return true;
62
+ if (cp === 12288) return true;
63
+ return false;
64
+ }
65
+ function trimHoconWs(s) {
66
+ let start = 0;
67
+ let end = s.length;
68
+ while (start < end && isHoconWs(s.charCodeAt(start))) {
69
+ start++;
70
+ }
71
+ while (end > start && isHoconWs(s.charCodeAt(end - 1))) {
72
+ end--;
73
+ }
74
+ return s.slice(start, end);
75
+ }
46
76
  var TRUTHY = /* @__PURE__ */ new Set(["true", "yes", "on"]);
47
77
  var FALSY = /* @__PURE__ */ new Set(["false", "no", "off"]);
48
78
  function coerceBoolean(value) {
@@ -88,22 +118,29 @@ var OUTPUT_DURATION_UNITS = {
88
118
  d: 864e5
89
119
  };
90
120
  function parseDuration(value, outputUnit = "ms") {
91
- const trimmed = value.trim();
121
+ const trimmed = trimHoconWs(value);
92
122
  let i = 0;
93
123
  while (i < trimmed.length) {
94
124
  const ch = trimmed[i];
95
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
125
+ if (i === 0 && (ch === "-" || ch === "+")) {
126
+ i++;
127
+ continue;
128
+ }
129
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
96
130
  i++;
97
131
  }
98
132
  if (i === 0) return NaN;
99
133
  const num = Number(trimmed.slice(0, i));
100
134
  if (Number.isNaN(num)) return NaN;
101
- const unit = trimmed.slice(i).trim().toLowerCase();
135
+ const unit = trimHoconWs(trimmed.slice(i)).toLowerCase();
136
+ const divisor = OUTPUT_DURATION_UNITS[outputUnit];
137
+ if (divisor === void 0) return NaN;
138
+ if (unit === "") {
139
+ return num / divisor;
140
+ }
102
141
  const mult = DURATION_UNITS[unit];
103
142
  if (mult === void 0) return NaN;
104
143
  const ms = num * mult;
105
- const divisor = OUTPUT_DURATION_UNITS[outputUnit];
106
- if (divisor === void 0) return NaN;
107
144
  return ms / divisor;
108
145
  }
109
146
  var BYTE_UNITS = {
@@ -134,6 +171,22 @@ var BYTE_UNITS = {
134
171
  TiB: 1099511627776,
135
172
  tebibyte: 1099511627776,
136
173
  tebibytes: 1099511627776,
174
+ // S21.4 — HOCON.md L1385: single-letter abbreviations → powers of two (java -Xmx convention).
175
+ // Lightbend typesafe-config 1.4.3 verified: 1K=1024, 1M=1048576, etc.
176
+ // Both upper- and lowercase accepted per Lightbend behaviour.
177
+ // Z/Y (2^70/2^80) are deferred — they overflow MAX_SAFE_INTEGER and require BigInt accessor.
178
+ K: 1024,
179
+ k: 1024,
180
+ M: 1024 ** 2,
181
+ m: 1024 ** 2,
182
+ G: 1024 ** 3,
183
+ g: 1024 ** 3,
184
+ T: 1024 ** 4,
185
+ t: 1024 ** 4,
186
+ P: 1024 ** 5,
187
+ p: 1024 ** 5,
188
+ E: 1024 ** 6,
189
+ e: 1024 ** 6,
137
190
  // lowercase short-form aliases
138
191
  b: 1,
139
192
  kb: 1e3,
@@ -157,25 +210,39 @@ var OUTPUT_BYTE_UNITS = {
157
210
  TiB: 1099511627776
158
211
  };
159
212
  function parseBytes(value, outputUnit = "B") {
160
- const trimmed = value.trim();
213
+ const trimmed = trimHoconWs(value);
161
214
  let i = 0;
162
215
  while (i < trimmed.length) {
163
216
  const ch = trimmed[i];
164
- if (ch !== "-" && ch !== "." && (ch < "0" || ch > "9")) break;
217
+ if (i === 0 && (ch === "-" || ch === "+")) {
218
+ i++;
219
+ continue;
220
+ }
221
+ if (ch !== "." && (ch < "0" || ch > "9")) break;
165
222
  i++;
166
223
  }
167
224
  if (i === 0) return NaN;
168
225
  const num = Number(trimmed.slice(0, i));
169
226
  if (Number.isNaN(num)) return NaN;
170
- const unit = trimmed.slice(i).trim();
227
+ const unit = trimHoconWs(trimmed.slice(i));
228
+ const divisor = OUTPUT_BYTE_UNITS[outputUnit];
229
+ if (divisor === void 0) return NaN;
230
+ if (unit === "") {
231
+ const bytes2 = Math.trunc(num);
232
+ if (Math.abs(bytes2) > Number.MAX_SAFE_INTEGER) {
233
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
234
+ }
235
+ return bytes2 / divisor;
236
+ }
171
237
  let mult = BYTE_UNITS[unit];
172
238
  if (mult === void 0) {
173
239
  mult = BYTE_UNITS[unit.toLowerCase()];
174
240
  }
175
241
  if (mult === void 0) return NaN;
176
242
  const bytes = num * mult;
177
- const divisor = OUTPUT_BYTE_UNITS[outputUnit];
178
- if (divisor === void 0) return NaN;
243
+ if (Math.abs(bytes) > Number.MAX_SAFE_INTEGER) {
244
+ throw new RangeError("byte size overflows representable range (max 2^53-1 bytes)");
245
+ }
179
246
  const result = bytes / divisor;
180
247
  return outputUnit === "B" ? Math.round(result) : result;
181
248
  }
@@ -200,6 +267,14 @@ var ResolveError = class extends Error {
200
267
  this.name = "ResolveError";
201
268
  }
202
269
  };
270
+ var PackageLookupError = class extends ResolveError {
271
+ constructor(message, identifier, packageFile, line, col) {
272
+ super(message, `${identifier}/${packageFile}`, line, col);
273
+ this.identifier = identifier;
274
+ this.packageFile = packageFile;
275
+ this.name = "PackageLookupError";
276
+ }
277
+ };
203
278
  var ConfigError = class extends Error {
204
279
  constructor(message, path2) {
205
280
  super(message);
@@ -207,190 +282,155 @@ var ConfigError = class extends Error {
207
282
  this.name = "ConfigError";
208
283
  }
209
284
  };
210
-
211
- // src/value/numeric-array.ts
212
- function numericObjectToArray(value) {
213
- if (value.kind !== "object") return null;
214
- if (value.fields.size === 0) return null;
215
- const canonicalInt = /^(0|[1-9][0-9]*)$/;
216
- const eligible = [];
217
- for (const [key, val] of value.fields) {
218
- if (!canonicalInt.test(key)) continue;
219
- const n = Number(key);
220
- if (!Number.isInteger(n) || n > 2147483647) continue;
221
- eligible.push({ n, v: val });
285
+ var NotResolvedError = class extends ConfigError {
286
+ constructor(path2) {
287
+ super(`value at path "${path2}" is not resolved (call resolve() first)`, path2);
288
+ this.name = "NotResolvedError";
222
289
  }
223
- if (eligible.length === 0) return null;
224
- eligible.sort((a, b) => a.n - b.n);
225
- return eligible.map((e) => e.v);
226
- }
290
+ };
227
291
 
228
- // src/config.ts
229
- var Config = class _Config {
230
- constructor(root) {
231
- this.root = root;
232
- }
233
- get(path2) {
234
- const v = this.lookupNode(path2);
235
- if (v === void 0) return void 0;
236
- return hoconToJs(v);
237
- }
238
- getString(path2) {
239
- const v = this.requireScalar(path2);
240
- return v.raw;
241
- }
242
- getNumber(path2) {
243
- const v = this.requireScalar(path2);
244
- const coerced = coerceNumber(v.raw);
245
- if (coerced !== void 0) return coerced;
246
- throw new ConfigError(`expected number at ${path2}, got ${v.valueType}`, path2);
247
- }
248
- getBoolean(path2) {
249
- const v = this.requireScalar(path2);
250
- const coerced = coerceBoolean(v.raw);
251
- if (coerced !== void 0) return coerced;
252
- throw new ConfigError(`expected boolean at ${path2}, got ${v.valueType}`, path2);
292
+ // src/internal/resolver/types.ts
293
+ var separatorValues = /* @__PURE__ */ new WeakSet();
294
+ function isSubst(v) {
295
+ return v._kind === "subst-placeholder";
296
+ }
297
+ function isConcat(v) {
298
+ return v._kind === "concat-placeholder";
299
+ }
300
+ function isAppend(v) {
301
+ return v._kind === "append-placeholder";
302
+ }
303
+ function isResObj(v) {
304
+ return v._kind === "res-obj";
305
+ }
306
+ function makeResObj() {
307
+ return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
308
+ }
309
+ function mergeUnresolved(receiver, fallback) {
310
+ const result = makeResObj();
311
+ for (const [k, v] of fallback.fields) {
312
+ result.fields.set(k, v);
253
313
  }
254
- getDuration(path2, unit) {
255
- const v = this.requireScalar(path2);
256
- if (v.valueType !== "string" && v.valueType !== "number") {
257
- throw new ConfigError(`expected duration at ${path2}, got ${v.valueType}`, path2);
258
- }
259
- const result = parseDuration(v.raw, unit);
260
- if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v.raw)}`, path2);
261
- return result;
314
+ for (const [k, v] of fallback.priorValues) {
315
+ result.priorValues.set(k, v);
262
316
  }
263
- getBytes(path2, unit) {
264
- const v = this.requireScalar(path2);
265
- if (v.valueType !== "string" && v.valueType !== "number") {
266
- throw new ConfigError(`expected byte size at ${path2}, got ${v.valueType}`, path2);
317
+ for (const [k, rv] of receiver.fields) {
318
+ const existing = result.fields.get(k);
319
+ if (existing !== void 0) {
320
+ const recPrior = receiver.priorValues.get(k);
321
+ if (recPrior !== void 0 && !isResObj(recPrior)) {
322
+ result.fields.set(k, rv);
323
+ continue;
324
+ }
325
+ if (isResObj(rv) && isResObj(existing)) {
326
+ result.fields.set(k, mergeUnresolved(rv, existing));
327
+ continue;
328
+ }
329
+ result.priorValues.set(k, existing);
267
330
  }
268
- const result = parseBytes(v.raw, unit);
269
- if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
270
- return result;
271
- }
272
- getConfig(path2) {
273
- const v = this.lookupNode(path2);
274
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
275
- if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
276
- return new _Config(v);
331
+ result.fields.set(k, rv);
277
332
  }
278
- getList(path2) {
279
- const v = this.lookupNode(path2);
280
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
281
- if (v.kind === "object") {
282
- const converted = numericObjectToArray(v);
283
- if (converted !== null) return converted.map(hoconToJs);
333
+ for (const [k, v] of receiver.priorValues) {
334
+ if (receiver.fields.has(k)) {
335
+ result.priorValues.set(k, v);
284
336
  }
285
- if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
286
- return v.items.map(hoconToJs);
287
- }
288
- has(path2) {
289
- return this.lookupNode(path2) !== void 0;
290
- }
291
- keys() {
292
- return [...this.root.fields.keys()];
293
- }
294
- withFallback(fallback) {
295
- const merged = deepMergeHocon(this.root, fallback.root);
296
- return new _Config(merged);
297
- }
298
- toObject() {
299
- return hoconToJs(this.root);
300
337
  }
301
- lookupNode(path2) {
302
- const segments = splitConfigPath(path2);
303
- let current = this.root;
304
- for (const seg of segments) {
305
- if (current.kind !== "object") return void 0;
306
- const next = current.fields.get(seg);
307
- if (next === void 0) return void 0;
308
- current = next;
338
+ return result;
339
+ }
340
+
341
+ // src/internal/resolver/utils.ts
342
+ function segmentsToKey(segments) {
343
+ return segments.map((s) => {
344
+ const t = s.text;
345
+ if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
346
+ return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
309
347
  }
310
- return current;
311
- }
312
- requireScalar(path2) {
313
- const v = this.lookupNode(path2);
314
- if (v === void 0) throw new ConfigError(`path not found: ${path2}`, path2);
315
- if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
316
- return v;
348
+ return t;
349
+ }).join(".");
350
+ }
351
+ function lookupPath(root, segments) {
352
+ const [head, ...tail] = segments;
353
+ if (head === void 0) return void 0;
354
+ const key = head.text;
355
+ const val = root.fields.get(key);
356
+ if (val === void 0) return void 0;
357
+ if (tail.length === 0) return val;
358
+ if (isResObj(val)) return lookupPath(val, tail);
359
+ return void 0;
360
+ }
361
+ function lookupResObj(root, segments) {
362
+ let cur = root;
363
+ for (const seg of segments) {
364
+ const val = cur.fields.get(seg.text);
365
+ if (val === void 0 || !isResObj(val)) return void 0;
366
+ cur = val;
317
367
  }
318
- };
319
- function splitConfigPath(path2) {
320
- const segments = [];
321
- let i = 0;
322
- while (i < path2.length) {
323
- if (path2[i] === '"') {
324
- i++;
325
- let segment = "";
326
- let closed = false;
327
- while (i < path2.length) {
328
- const ch = path2[i];
329
- if (ch === "\\" && i + 1 < path2.length) {
330
- const next = path2[i + 1];
331
- segment += next;
332
- i += 2;
333
- continue;
334
- }
335
- if (ch === '"') {
336
- closed = true;
337
- i++;
338
- break;
339
- }
340
- segment += ch;
341
- i++;
342
- }
343
- if (!closed) throw new ConfigError(`unterminated quoted path segment: ${path2}`, path2);
344
- segments.push(segment);
345
- if (i < path2.length && path2[i] === ".") i++;
368
+ return cur;
369
+ }
370
+ function deepMergeHoconValues(base, overlay) {
371
+ const merged = new Map(base.fields);
372
+ for (const [k, v] of overlay.fields) {
373
+ const existing = merged.get(k);
374
+ if (existing?.kind === "object" && v.kind === "object") {
375
+ merged.set(k, deepMergeHoconValues(existing, v));
346
376
  } else {
347
- const dot = path2.indexOf(".", i);
348
- if (dot === -1) {
349
- segments.push(path2.slice(i));
350
- break;
351
- }
352
- segments.push(path2.slice(i, dot));
353
- i = dot + 1;
377
+ merged.set(k, v);
354
378
  }
355
379
  }
356
- return segments;
380
+ return { kind: "object", fields: merged };
357
381
  }
358
- function scalarToJs(raw, valueType) {
359
- switch (valueType) {
360
- case "null":
361
- return null;
362
- case "boolean":
363
- return raw === "true";
364
- case "number":
365
- return Number(raw);
366
- case "string":
367
- return raw;
382
+ function deepMergeResObjInto(dst, src) {
383
+ for (const [k, srcVal] of src.fields) {
384
+ const dstVal = dst.fields.get(k);
385
+ if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
386
+ deepMergeResObjInto(dstVal, srcVal);
387
+ } else {
388
+ if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
389
+ dst.fields.set(k, srcVal);
390
+ }
368
391
  }
369
- }
370
- function hoconToJs(v) {
371
- switch (v.kind) {
372
- case "scalar":
373
- return scalarToJs(v.raw, v.valueType);
374
- case "array":
375
- return v.items.map(hoconToJs);
376
- case "object": {
377
- const obj = {};
378
- for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
379
- return obj;
392
+ for (const [k, srcPrior] of src.priorValues) {
393
+ if (!dst.priorValues.has(k)) {
394
+ dst.priorValues.set(k, srcPrior);
380
395
  }
381
396
  }
382
397
  }
383
- function deepMergeHocon(receiver, fallback) {
384
- const merged = new Map(fallback.fields);
385
- for (const [k, v] of receiver.fields) {
386
- const fb = merged.get(k);
387
- if (fb?.kind === "object" && v.kind === "object") {
388
- merged.set(k, deepMergeHocon(v, fb));
398
+ function hoconValueToResObj(hv) {
399
+ const obj = makeResObj();
400
+ if (hv.kind !== "object") return obj;
401
+ for (const [key, val] of hv.fields) {
402
+ if (val.kind === "object") {
403
+ obj.fields.set(key, hoconValueToResObj(val));
389
404
  } else {
390
- merged.set(k, v);
405
+ obj.fields.set(key, val);
391
406
  }
392
407
  }
393
- return { kind: "object", fields: merged };
408
+ return obj;
409
+ }
410
+ function isFileNotFoundError(e) {
411
+ if (!(e instanceof Error)) return false;
412
+ const code = e.code;
413
+ if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
414
+ const msg = e.message.toLowerCase();
415
+ return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
416
+ }
417
+
418
+ // src/internal/resolver/include-loader.ts
419
+ var import_node_module = require("module");
420
+ var nodePath = __toESM(require("path"), 1);
421
+
422
+ // src/internal/parser/empty-check.ts
423
+ function hasContentTokens(tokens) {
424
+ return tokens.some((t) => t.kind !== "eof" && t.kind !== "newline");
425
+ }
426
+ function assertNonEmptyDocument(tokens, sourceDescriptor) {
427
+ if (!hasContentTokens(tokens)) {
428
+ throw new ParseError(
429
+ `empty file is not a valid HOCON document (HOCON.md L130): ${sourceDescriptor}`,
430
+ 1,
431
+ 1
432
+ );
433
+ }
394
434
  }
395
435
 
396
436
  // src/internal/lexer/lexer.ts
@@ -520,14 +560,6 @@ var Lexer = class {
520
560
  continue;
521
561
  }
522
562
  if (isUnquotedStart(ch)) {
523
- if (ch === "-" && !isDecimalDigit(this.peek(1))) {
524
- const after = this.peek(1) === "" ? "EOF" : JSON.stringify(this.peek(1));
525
- throw new ParseError(
526
- `unquoted string cannot begin with '-' unless followed by a digit (got '-' then ${after}, HOCON.md L270-276)`,
527
- sl,
528
- sc
529
- );
530
- }
531
563
  let value = "";
532
564
  while (this.pos < this.input.length && isUnquotedContinue(this.peek(), () => this.peek(1))) {
533
565
  value += this.advance();
@@ -631,6 +663,10 @@ var Lexer = class {
631
663
  /**
632
664
  * Parse the body of a `${...}` substitution (called after `${` has been consumed).
633
665
  * Implements Appendix A state machine from the cross-language spec.
666
+ *
667
+ * S13c extension: if the segment-collection loop encounters `[`, it delegates to
668
+ * `parseListSuffix()` which consumes the literal `[]` and sets `listSuffix=true`.
669
+ * ASCII space/tab before `[` is buffered in `pendingWs` and discarded (E7).
634
670
  */
635
671
  parseSubstBody(startLine, startCol) {
636
672
  let optional = false;
@@ -645,6 +681,7 @@ var Lexer = class {
645
681
  let pendingWs = "";
646
682
  const segments = [];
647
683
  let lastDot = null;
684
+ let listSuffix = false;
648
685
  while (true) {
649
686
  if (this.pos >= this.input.length) {
650
687
  throw new ParseError("unterminated substitution", startLine, startCol);
@@ -654,6 +691,23 @@ var Lexer = class {
654
691
  this.advance();
655
692
  pendingWs = "";
656
693
  break;
694
+ } else if (ch === "[") {
695
+ if (!curStarted) {
696
+ throw new ParseError("empty segment before '[]' suffix in substitution path", startLine, this.col);
697
+ }
698
+ for (const w of pendingWs) {
699
+ if (w !== " " && w !== " ") {
700
+ throw new ParseError(
701
+ `only ASCII space or tab allowed between substitution path and '[]' suffix (got ${JSON.stringify(w)}, HOCON extra-spec E7)`,
702
+ startLine,
703
+ this.col
704
+ );
705
+ }
706
+ }
707
+ segments.push({ text: curText, line: curLine, col: curCol });
708
+ pendingWs = "";
709
+ listSuffix = this.parseListSuffix(startLine);
710
+ break;
657
711
  } else if (ch === '"') {
658
712
  const qLine = startLine;
659
713
  const qCol = this.col;
@@ -713,15 +767,47 @@ var Lexer = class {
713
767
  throw new ParseError(`unexpected character in substitution path: ${JSON.stringify(ch)}`, startLine, this.col);
714
768
  }
715
769
  }
716
- if (curStarted) {
717
- segments.push({ text: curText, line: curLine, col: curCol });
718
- } else if (segments.length === 0) {
719
- throw new ParseError("empty substitution path", startLine, startCol);
720
- } else {
721
- const [errLine, errCol] = lastDot ?? [startLine, startCol];
722
- throw new ParseError("empty segment in path", errLine, errCol);
770
+ if (!listSuffix) {
771
+ if (curStarted) {
772
+ segments.push({ text: curText, line: curLine, col: curCol });
773
+ } else if (segments.length === 0) {
774
+ throw new ParseError("empty substitution path", startLine, startCol);
775
+ } else {
776
+ const [errLine, errCol] = lastDot ?? [startLine, startCol];
777
+ throw new ParseError("empty segment in path", errLine, errCol);
778
+ }
779
+ }
780
+ return { segments, optional, listSuffix };
781
+ }
782
+ /**
783
+ * Consume the literal `[]` suffix inside a substitution body.
784
+ * Called after the `[` has been detected (but not yet consumed).
785
+ * Requires the next character to be `[`, then exactly `]`, then `}`.
786
+ * Whitespace inside `[]` is not permitted (per spec Decision §1).
787
+ * Returns true (always — only called when listSuffix path is taken).
788
+ */
789
+ parseListSuffix(startLine) {
790
+ this.advance();
791
+ const afterBracket = this.peek();
792
+ if (afterBracket !== "]") {
793
+ const desc = afterBracket === "" ? "EOF" : JSON.stringify(afterBracket);
794
+ throw new ParseError(
795
+ `expected ']' after '[' in substitution list suffix (got ${desc})`,
796
+ startLine,
797
+ this.col
798
+ );
799
+ }
800
+ this.advance();
801
+ if (this.peek() !== "}") {
802
+ const desc = this.peek() === "" ? "EOF" : JSON.stringify(this.peek());
803
+ throw new ParseError(
804
+ `expected '}' after '[]' in substitution list suffix (got ${desc})`,
805
+ startLine,
806
+ this.col
807
+ );
723
808
  }
724
- return { segments, optional };
809
+ this.advance();
810
+ return true;
725
811
  }
726
812
  };
727
813
  function tokenize(input) {
@@ -809,6 +895,14 @@ var Parser = class {
809
895
  const t = this.peek();
810
896
  if (t.kind === "eof" || t.kind === "rbrace") break;
811
897
  if (t.kind === "unquoted" && t.value === "include") {
898
+ const next = this.peek(1);
899
+ if (next.kind === "equals" || next.kind === "colon" || next.kind === "plus_equals" || next.kind === "lbrace") {
900
+ throw new ParseError(
901
+ `'include' is reserved at the start of a key path expression; use "include" (quoted) to use it as a key`,
902
+ t.line,
903
+ t.col
904
+ );
905
+ }
812
906
  this.advance();
813
907
  fields.push(this.parseInclude());
814
908
  this.skip("newline");
@@ -817,7 +911,14 @@ var Parser = class {
817
911
  continue;
818
912
  }
819
913
  const keyPos = this.currentPos();
820
- const key = this.parseKey();
914
+ const { segments: key, firstWasQuoted } = this.parseKey();
915
+ if (key[0] === "include" && !firstWasQuoted) {
916
+ throw new ParseError(
917
+ `'include' is reserved at the start of a key path expression; use "include".foo (quoted) or rename the key`,
918
+ keyPos.line,
919
+ keyPos.col
920
+ );
921
+ }
821
922
  this.skip("newline");
822
923
  let append = false;
823
924
  const sep = this.peek();
@@ -847,10 +948,12 @@ var Parser = class {
847
948
  parseKey() {
848
949
  const segments = [];
849
950
  let trailingDot = false;
951
+ let firstWasQuoted = false;
850
952
  while (true) {
851
953
  const t = this.peek();
852
954
  if (t.kind === "string") {
853
955
  this.advance();
956
+ if (segments.length === 0) firstWasQuoted = true;
854
957
  segments.push(t.value);
855
958
  trailingDot = false;
856
959
  } else if (t.kind === "unquoted") {
@@ -887,64 +990,143 @@ var Parser = class {
887
990
  }
888
991
  break;
889
992
  }
890
- return segments;
993
+ return { segments, firstWasQuoted };
891
994
  }
892
995
  parseInclude() {
893
996
  const p = this.currentPos();
894
997
  this.skip("newline");
895
998
  const t = this.peek();
896
- let path2;
897
- let required = false;
898
- let isFile = false;
899
999
  if (t.kind === "unquoted" && (t.value === "required(" || t.value === "required" || t.value.startsWith("required("))) {
900
1000
  if (t.value === "required") {
901
- const next2 = this.peek(1);
902
- if (next2.kind !== "unquoted" || !next2.value.startsWith("(")) {
1001
+ const next = this.peek(1);
1002
+ if (next.kind !== "unquoted" || !next.value.startsWith("(")) {
903
1003
  throw new ParseError("include required must be followed by (", t.line, t.col);
904
1004
  }
905
1005
  }
906
- required = true;
907
1006
  this.advance();
908
1007
  const innerPrefix = t.value.startsWith("required(") ? t.value.slice("required(".length) : "";
909
- if (innerPrefix.startsWith("url") || innerPrefix.startsWith("classpath")) {
1008
+ if (innerPrefix === "url" || innerPrefix.startsWith("url(") || innerPrefix === "classpath" || innerPrefix.startsWith("classpath(")) {
910
1009
  throw new ParseError("include url(...) and classpath(...) are not supported", t.line, t.col);
911
1010
  }
912
- if (innerPrefix.startsWith("file")) {
913
- isFile = true;
1011
+ const nextTok = this.peek();
1012
+ if (innerPrefix === "package" || innerPrefix.startsWith("package(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "package(" || nextTok.value.startsWith("package("))) {
1013
+ const consumePackageToken = innerPrefix === "";
1014
+ const { qualifier, path: path3 } = this.parsePackageArgs(consumePackageToken);
1015
+ return this.makeIncludeField(qualifier, path3, true, p);
914
1016
  }
915
- const next = this.peek();
916
- if (next.kind === "unquoted" && (next.value === "url" || next.value.startsWith("url(") || next.value === "classpath" || next.value.startsWith("classpath("))) {
917
- throw new ParseError("include url(...) and classpath(...) are not supported", next.line, next.col);
918
- }
919
- if (next.kind === "unquoted" && (next.value === "file(" || next.value === "file" || next.value.startsWith("file("))) {
920
- isFile = true;
921
- }
922
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
923
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
924
- path2 = this.advance().value;
925
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
926
- } else if (t.kind === "string") {
927
- path2 = this.advance().value;
928
- } else if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
929
- isFile = true;
1017
+ if (innerPrefix === "file" || innerPrefix.startsWith("file(") || innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "file(" || nextTok.value === "file")) {
1018
+ if (innerPrefix === "") {
1019
+ this.advance();
1020
+ }
1021
+ const path3 = this.parseQuotedPathSkipWrapper(t);
1022
+ return this.makeIncludeField({ kind: "file" }, path3, true, p);
1023
+ }
1024
+ if (innerPrefix === "" && nextTok.kind === "unquoted" && (nextTok.value === "url" || nextTok.value.startsWith("url(") || nextTok.value === "classpath" || nextTok.value.startsWith("classpath("))) {
1025
+ throw new ParseError("include url(...) and classpath(...) are not supported", nextTok.line, nextTok.col);
1026
+ }
1027
+ if (innerPrefix !== "") {
1028
+ if (innerPrefix === ")") {
1029
+ throw new ParseError("include required(...) must contain a path", t.line, t.col);
1030
+ }
1031
+ const name = innerPrefix.replace(/\(.*$/, "").replace(/\).*$/, "");
1032
+ throw new ParseError(`unknown include qualifier inside required(): "${name}"`, t.line, t.col);
1033
+ }
1034
+ const path2 = this.parseQuotedPathSkipWrapper(t);
1035
+ return this.makeIncludeField({ kind: "bare" }, path2, true, p);
1036
+ }
1037
+ if (t.kind === "string") {
1038
+ const path2 = this.advance().value;
1039
+ return this.makeIncludeField({ kind: "bare" }, path2, false, p);
1040
+ }
1041
+ if (t.kind === "unquoted" && (t.value === "file(" || t.value === "file")) {
930
1042
  this.advance();
931
- while (this.peek().kind !== "string" && this.peek().kind !== "eof") this.advance();
932
- if (this.peek().kind === "eof") throw new ParseError("expected include path", t.line, t.col);
933
- path2 = this.advance().value;
934
- while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") this.advance();
935
- } else if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
1043
+ const path2 = this.parseQuotedPathSkipWrapper(t);
1044
+ return this.makeIncludeField({ kind: "file" }, path2, false, p);
1045
+ }
1046
+ if (t.kind === "unquoted" && (t.value === "package(" || t.value.startsWith("package("))) {
1047
+ const { qualifier, path: path2 } = this.parsePackageArgs();
1048
+ return this.makeIncludeField(qualifier, path2, false, p);
1049
+ }
1050
+ if (t.kind === "unquoted" && (t.value === "url" || t.value.startsWith("url("))) {
936
1051
  throw new ParseError("include url(...) is not supported", t.line, t.col);
937
- } else if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
1052
+ }
1053
+ if (t.kind === "unquoted" && (t.value === "classpath" || t.value.startsWith("classpath("))) {
938
1054
  throw new ParseError("include classpath(...) is not supported", t.line, t.col);
939
- } else {
940
- throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
941
1055
  }
942
- return {
943
- key: [],
944
- value: { kind: "include", path: path2, required, ...isFile ? { isFile: true } : {}, pos: p },
945
- append: false,
946
- pos: p
947
- };
1056
+ throw new ParseError(`expected include path, got ${t.kind}`, t.line, t.col);
1057
+ }
1058
+ makeIncludeField(qualifier, path2, required, pos) {
1059
+ return { key: [], value: { kind: "include", qualifier, path: path2, required, pos }, append: false, pos };
1060
+ }
1061
+ /**
1062
+ * Skip past include-syntax wrapper tokens to find the quoted path string.
1063
+ * Strict: only allows `(`, `)`, and bare qualifier keywords (file / file( /
1064
+ * package / package( / url / url( / classpath / classpath() between the
1065
+ * directive and the quoted path. Anything else is a parse error rather than
1066
+ * silently swallowed (see ts#113).
1067
+ */
1068
+ parseQuotedPathSkipWrapper(errTok) {
1069
+ while (this.isIncludeWrapperToken(this.peek())) this.advance();
1070
+ if (this.peek().kind !== "string") {
1071
+ const tok = this.peek();
1072
+ if (tok.kind === "eof") throw new ParseError("expected include path", errTok.line, errTok.col);
1073
+ const desc = tok.kind === "unquoted" ? `unquoted "${tok.value}"` : tok.kind;
1074
+ throw new ParseError(`expected quoted include path, got ${desc}`, tok.line, tok.col);
1075
+ }
1076
+ const path2 = this.advance().value;
1077
+ while (this.peek().kind === "unquoted" && (this.peek().value === ")" || this.peek().value === "))")) {
1078
+ this.advance();
1079
+ }
1080
+ const after = this.peek();
1081
+ if (after.kind !== "newline" && after.kind !== "rbrace" && after.kind !== "eof" && after.kind !== "comma") {
1082
+ const desc = after.kind === "unquoted" ? `unquoted "${after.value}"` : after.kind;
1083
+ throw new ParseError(`unexpected token after include path: ${desc}`, after.line, after.col);
1084
+ }
1085
+ return path2;
1086
+ }
1087
+ isIncludeWrapperToken(tok) {
1088
+ return tok.kind === "unquoted" && tok.value === "(";
1089
+ }
1090
+ /**
1091
+ * Parse the body of `package("identifier", "file")`.
1092
+ *
1093
+ * @param consumePackageToken - When true (default), the current token is `package(` and
1094
+ * must be consumed first. When false, the lexer already bundled `package(` into the
1095
+ * preceding token (e.g. `required(package("`), so we are already positioned at the
1096
+ * first string argument.
1097
+ */
1098
+ parsePackageArgs(consumePackageToken = true) {
1099
+ const pkgTok = consumePackageToken ? this.advance() : this.peek();
1100
+ this.skip("newline");
1101
+ const idTok = this.peek();
1102
+ if (idTok.kind !== "string") {
1103
+ throw new ParseError("include package: expected quoted identifier as first argument", pkgTok.line, pkgTok.col);
1104
+ }
1105
+ const identifier = this.advance().value;
1106
+ this.skip("newline");
1107
+ if (this.peek().kind !== "comma") {
1108
+ const after = this.peek();
1109
+ throw new ParseError(
1110
+ 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",
1111
+ pkgTok.line,
1112
+ pkgTok.col
1113
+ );
1114
+ }
1115
+ this.advance();
1116
+ this.skip("newline");
1117
+ const fileTok = this.peek();
1118
+ if (fileTok.kind !== "string") {
1119
+ throw new ParseError(
1120
+ "include package: requires exactly two arguments (identifier, file) \u2014 one-arg form is not supported",
1121
+ pkgTok.line,
1122
+ pkgTok.col
1123
+ );
1124
+ }
1125
+ const filePath = this.advance().value;
1126
+ while (this.peek().kind !== "newline" && this.peek().kind !== "rbrace" && this.peek().kind !== "eof" && this.peek().kind !== "comma") {
1127
+ this.advance();
1128
+ }
1129
+ return { qualifier: { kind: "package", identifier }, path: filePath };
948
1130
  }
949
1131
  parseValue() {
950
1132
  const p = this.currentPos();
@@ -966,7 +1148,7 @@ var Parser = class {
966
1148
  throw new ParseError(`internal: subst token missing payload`, t.line, t.col);
967
1149
  }
968
1150
  const payload = t.subst;
969
- node = { kind: "subst", segments: payload.segments, optional: payload.optional, pos: { line: t.line, col: t.col } };
1151
+ node = { kind: "subst", segments: payload.segments, optional: payload.optional, listSuffix: payload.listSuffix, pos: { line: t.line, col: t.col } };
970
1152
  } else if (t.kind === "string" || t.kind === "triple_string") {
971
1153
  this.advance();
972
1154
  node = { kind: "scalar", raw: t.value, valueType: "string", pos: { line: t.line, col: t.col } };
@@ -985,7 +1167,9 @@ var Parser = class {
985
1167
  parts.push(node);
986
1168
  }
987
1169
  if (parts.length === 0) throw new ParseError("expected value", this.peek().line, this.peek().col);
988
- if (parts.length === 1) return parts[0];
1170
+ if (parts.length === 1) {
1171
+ return parts[0];
1172
+ }
989
1173
  return { kind: "concat", nodes: parts, pos: p };
990
1174
  }
991
1175
  parseArray() {
@@ -1017,107 +1201,10 @@ function parseTokens(tokens) {
1017
1201
  return new Parser(tokens).parse();
1018
1202
  }
1019
1203
 
1020
- // src/internal/resolver/types.ts
1021
- var separatorValues = /* @__PURE__ */ new WeakSet();
1022
- function isSubst(v) {
1023
- return v._kind === "subst-placeholder";
1024
- }
1025
- function isConcat(v) {
1026
- return v._kind === "concat-placeholder";
1027
- }
1028
- function isAppend(v) {
1029
- return v._kind === "append-placeholder";
1030
- }
1031
- function isResObj(v) {
1032
- return v._kind === "res-obj";
1033
- }
1034
- function makeResObj() {
1035
- return { _kind: "res-obj", fields: /* @__PURE__ */ new Map(), priorValues: /* @__PURE__ */ new Map() };
1036
- }
1037
-
1038
- // src/internal/resolver/utils.ts
1039
- function segmentsToKey(segments) {
1040
- return segments.map((s) => {
1041
- const t = s.text;
1042
- if (t === "" || /[^a-zA-Z0-9\-_]/.test(t)) {
1043
- return `"${t.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1044
- }
1045
- return t;
1046
- }).join(".");
1047
- }
1048
- function lookupPath(root, segments) {
1049
- const [head, ...tail] = segments;
1050
- if (head === void 0) return void 0;
1051
- const key = head.text;
1052
- const val = root.fields.get(key);
1053
- if (val === void 0) return void 0;
1054
- if (tail.length === 0) return val;
1055
- if (isResObj(val)) return lookupPath(val, tail);
1056
- return void 0;
1057
- }
1058
- function lookupResObj(root, segments) {
1059
- let cur = root;
1060
- for (const seg of segments) {
1061
- const val = cur.fields.get(seg.text);
1062
- if (val === void 0 || !isResObj(val)) return void 0;
1063
- cur = val;
1064
- }
1065
- return cur;
1066
- }
1067
- function deepMergeHoconValues(base, overlay) {
1068
- const merged = new Map(base.fields);
1069
- for (const [k, v] of overlay.fields) {
1070
- const existing = merged.get(k);
1071
- if (existing?.kind === "object" && v.kind === "object") {
1072
- merged.set(k, deepMergeHoconValues(existing, v));
1073
- } else {
1074
- merged.set(k, v);
1075
- }
1076
- }
1077
- return { kind: "object", fields: merged };
1078
- }
1079
- function deepMergeResObjInto(dst, src) {
1080
- for (const [k, srcVal] of src.fields) {
1081
- const dstVal = dst.fields.get(k);
1082
- if (dstVal !== void 0 && isResObj(dstVal) && isResObj(srcVal)) {
1083
- deepMergeResObjInto(dstVal, srcVal);
1084
- } else {
1085
- if (dstVal !== void 0) dst.priorValues.set(k, dstVal);
1086
- dst.fields.set(k, srcVal);
1087
- }
1088
- }
1089
- for (const [k, srcPrior] of src.priorValues) {
1090
- if (!dst.priorValues.has(k)) {
1091
- dst.priorValues.set(k, srcPrior);
1092
- }
1093
- }
1094
- }
1095
- function hoconValueToResObj(hv) {
1096
- const obj = makeResObj();
1097
- if (hv.kind !== "object") return obj;
1098
- for (const [key, val] of hv.fields) {
1099
- if (val.kind === "object") {
1100
- obj.fields.set(key, hoconValueToResObj(val));
1101
- } else {
1102
- obj.fields.set(key, val);
1103
- }
1104
- }
1105
- return obj;
1106
- }
1107
- function isFileNotFoundError(e) {
1108
- if (!(e instanceof Error)) return false;
1109
- const code = e.code;
1110
- if (code === "ENOENT" || code === "MODULE_NOT_FOUND") return true;
1111
- const msg = e.message.toLowerCase();
1112
- return msg.includes("not found") || msg.includes("no such file") || msg.includes("enoent");
1113
- }
1114
-
1115
- // src/internal/resolver/include-loader.ts
1116
- var nodePath = __toESM(require("path"), 1);
1117
-
1118
1204
  // src/internal/properties/properties.ts
1119
1205
  function parseProperties(input) {
1120
1206
  const root = /* @__PURE__ */ Object.create(null);
1207
+ const pairs = [];
1121
1208
  for (const line of input.split("\n")) {
1122
1209
  const trimmed = line.trim();
1123
1210
  if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
@@ -1126,6 +1213,10 @@ function parseProperties(input) {
1126
1213
  const key = trimmed.slice(0, sepIdx).trim();
1127
1214
  const value = trimmed.slice(sepIdx + 1).trim();
1128
1215
  if (key === "") continue;
1216
+ pairs.push([key, value]);
1217
+ }
1218
+ pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1219
+ for (const [key, value] of pairs) {
1129
1220
  setNested(root, key.split("."), value);
1130
1221
  }
1131
1222
  return root;
@@ -1149,6 +1240,7 @@ function setNested(obj, segments, value) {
1149
1240
  }
1150
1241
  const last = segments[segments.length - 1];
1151
1242
  if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1243
+ if (typeof current[last] === "object" && current[last] !== null) return;
1152
1244
  current[last] = value;
1153
1245
  }
1154
1246
  function propertiesToHoconValue(input) {
@@ -1168,6 +1260,85 @@ function recordToHoconValue(obj) {
1168
1260
  }
1169
1261
 
1170
1262
  // src/internal/resolver/include-loader.ts
1263
+ var import_meta = {};
1264
+ var _require = (0, import_node_module.createRequire)(import_meta.url);
1265
+ function isPnpPath(p) {
1266
+ return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1267
+ }
1268
+ function validatePackageFile(file, identifier) {
1269
+ if (file.length === 0) {
1270
+ throw new ParseError(`include package("${identifier}", ...): file argument must be non-empty`, 0, 0);
1271
+ }
1272
+ if (file.includes("\\")) {
1273
+ throw new ParseError(
1274
+ `include package("${identifier}", "${file}"): backslash not allowed in file argument (use forward slash)`,
1275
+ 0,
1276
+ 0
1277
+ );
1278
+ }
1279
+ if (file.startsWith("/")) {
1280
+ throw new ParseError(
1281
+ `include package("${identifier}", "${file}"): absolute path not allowed in file argument`,
1282
+ 0,
1283
+ 0
1284
+ );
1285
+ }
1286
+ if (/(?:^|\/)\.\.?(?:\/|$)/u.test(file)) {
1287
+ throw new ParseError(
1288
+ `include package("${identifier}", "${file}"): path traversal (. or ..) not allowed in file argument`,
1289
+ 0,
1290
+ 0
1291
+ );
1292
+ }
1293
+ if (file.includes("//")) {
1294
+ throw new ParseError(
1295
+ `include package("${identifier}", "${file}"): consecutive slashes not allowed in file argument`,
1296
+ 0,
1297
+ 0
1298
+ );
1299
+ }
1300
+ }
1301
+ function validatePackageIdentifier(identifier) {
1302
+ if (identifier.length === 0) {
1303
+ throw new ParseError("include package: identifier argument must be non-empty", 0, 0);
1304
+ }
1305
+ }
1306
+ function defaultPackageResolver(identifier, file, includingFile, baseDir, resolveFrom) {
1307
+ const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
1308
+ let resolved;
1309
+ try {
1310
+ resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
1311
+ } catch {
1312
+ throw new PackageLookupError(
1313
+ `include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
1314
+ identifier,
1315
+ file,
1316
+ 0,
1317
+ 0
1318
+ );
1319
+ }
1320
+ if (isPnpPath(resolved)) {
1321
+ throw new ResolveError(
1322
+ `include package("${identifier}", "${file}"): Yarn Berry PnP store paths are not supported. Provide a custom packageResolver option to handle PnP resolution.`,
1323
+ `${identifier}/${file}`,
1324
+ 0,
1325
+ 0
1326
+ );
1327
+ }
1328
+ const fileParts = file.split("/");
1329
+ const expectedBasename = fileParts[fileParts.length - 1] ?? file;
1330
+ const resolvedBasename = nodePath.basename(resolved);
1331
+ if (resolvedBasename !== expectedBasename) {
1332
+ throw new PackageLookupError(
1333
+ `include package("${identifier}", "${file}"): case mismatch \u2014 resolved "${resolvedBasename}" but expected "${expectedBasename}" (E11 decision 5: case-sensitive)`,
1334
+ identifier,
1335
+ file,
1336
+ 0,
1337
+ 0
1338
+ );
1339
+ }
1340
+ return resolved;
1341
+ }
1171
1342
  var IncludeLoader = class {
1172
1343
  constructor(opts) {
1173
1344
  this.opts = opts;
@@ -1247,8 +1418,77 @@ var IncludeLoader = class {
1247
1418
  }
1248
1419
  return merged;
1249
1420
  }
1421
+ /**
1422
+ * Resolve and load `include package("identifier", "file")`.
1423
+ * Validates the identifier and file arguments, resolves via the package resolver,
1424
+ * performs cycle detection using a JSON-serialised cycle key, and parses the content.
1425
+ */
1426
+ // `required` is reserved for future optional-package semantics (E11 decision 7);
1427
+ // currently all package lookup failures throw regardless of this flag.
1428
+ loadPackage(identifier, file, _required) {
1429
+ validatePackageIdentifier(identifier);
1430
+ validatePackageFile(file, identifier);
1431
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFileSync: readFileSync2 } = this.opts;
1432
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1433
+ if (includeStack.includes(cycleKey)) {
1434
+ throw new ResolveError(
1435
+ `circular include: package("${identifier}", "${file}")`,
1436
+ `${identifier}/${file}`,
1437
+ 0,
1438
+ 0
1439
+ );
1440
+ }
1441
+ if (includeStack.length >= 50) {
1442
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1443
+ }
1444
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1445
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1446
+ const content = readFileSync2(resolvedPath);
1447
+ if (content.length === 0) return makeResObj();
1448
+ const tokens = tokenize(content);
1449
+ assertNonEmptyDocument(tokens, resolvedPath);
1450
+ const ast = parseTokens(tokens);
1451
+ return this.onBuildResObj(ast, {
1452
+ ...this.opts,
1453
+ baseDir: nodePath.dirname(resolvedPath),
1454
+ includeStack: [...includeStack, cycleKey]
1455
+ });
1456
+ }
1457
+ /**
1458
+ * Async variant of `loadPackage`.
1459
+ */
1460
+ async loadPackageAsync(identifier, file, _required) {
1461
+ validatePackageIdentifier(identifier);
1462
+ validatePackageFile(file, identifier);
1463
+ const { includeStack = [], baseDir, resolveFrom, packageResolver, readFile, readFileSync: readFileSync2 } = this.opts;
1464
+ const cycleKey = JSON.stringify(["package", identifier, file]);
1465
+ if (includeStack.includes(cycleKey)) {
1466
+ throw new ResolveError(
1467
+ `circular include: package("${identifier}", "${file}")`,
1468
+ `${identifier}/${file}`,
1469
+ 0,
1470
+ 0
1471
+ );
1472
+ }
1473
+ if (includeStack.length >= 50) {
1474
+ throw new ResolveError(`include depth limit exceeded (max 50)`, `${identifier}/${file}`, 0, 0);
1475
+ }
1476
+ const resolver = packageResolver ?? ((id, f, includingFile, bDir) => defaultPackageResolver(id, f, includingFile, bDir, resolveFrom));
1477
+ const resolvedPath = resolver(identifier, file, void 0, baseDir);
1478
+ const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1479
+ const content = await read(resolvedPath);
1480
+ if (content.length === 0) return makeResObj();
1481
+ const tokens = tokenize(content);
1482
+ assertNonEmptyDocument(tokens, resolvedPath);
1483
+ const ast = parseTokens(tokens);
1484
+ return this.onBuildResObjAsync(ast, {
1485
+ ...this.opts,
1486
+ baseDir: nodePath.dirname(resolvedPath),
1487
+ includeStack: [...includeStack, cycleKey]
1488
+ });
1489
+ }
1250
1490
  loadSingle(candidate) {
1251
- const { readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1491
+ const { readFileSync: readFileSync2, includeStack = [] } = this.opts;
1252
1492
  if (includeStack.includes(candidate)) {
1253
1493
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
1254
1494
  }
@@ -1262,16 +1502,17 @@ var IncludeLoader = class {
1262
1502
  if (candidate.endsWith(".properties")) {
1263
1503
  return hoconValueToResObj(propertiesToHoconValue(content));
1264
1504
  }
1265
- const ast = parseTokens(tokenize(content));
1505
+ const tokens = tokenize(content);
1506
+ assertNonEmptyDocument(tokens, candidate);
1507
+ const ast = parseTokens(tokens);
1266
1508
  return this.onBuildResObj(ast, {
1267
- env,
1509
+ ...this.opts,
1268
1510
  baseDir: nodePath.dirname(candidate),
1269
- readFileSync: readFileSync2,
1270
1511
  includeStack: [...includeStack, candidate]
1271
1512
  });
1272
1513
  }
1273
1514
  async loadSingleAsync(candidate) {
1274
- const { readFile, readFileSync: readFileSync2, includeStack = [], env } = this.opts;
1515
+ const { readFile, readFileSync: readFileSync2, includeStack = [] } = this.opts;
1275
1516
  const read = readFile ? async (p) => readFile(p) : async (p) => readFileSync2(p);
1276
1517
  if (includeStack.includes(candidate)) {
1277
1518
  throw new ResolveError(`circular include: ${candidate}`, candidate, 0, 0);
@@ -1286,12 +1527,12 @@ var IncludeLoader = class {
1286
1527
  if (candidate.endsWith(".properties")) {
1287
1528
  return hoconValueToResObj(propertiesToHoconValue(content));
1288
1529
  }
1289
- const ast = parseTokens(tokenize(content));
1530
+ const tokens = tokenize(content);
1531
+ assertNonEmptyDocument(tokens, candidate);
1532
+ const ast = parseTokens(tokens);
1290
1533
  return this.onBuildResObjAsync(ast, {
1291
- env,
1534
+ ...this.opts,
1292
1535
  baseDir: nodePath.dirname(candidate),
1293
- readFileSync: readFileSync2,
1294
- readFile,
1295
1536
  includeStack: [...includeStack, candidate]
1296
1537
  });
1297
1538
  }
@@ -1300,7 +1541,6 @@ var IncludeLoader = class {
1300
1541
  // src/internal/resolver/structure-builder.ts
1301
1542
  var StructureBuilder = class _StructureBuilder {
1302
1543
  constructor(opts) {
1303
- this.opts = opts;
1304
1544
  this.loader = new IncludeLoader(opts);
1305
1545
  this.loader.onBuildResObj = (a, o) => new _StructureBuilder(o).build(a);
1306
1546
  this.loader.onBuildResObjAsync = async (a, o) => new _StructureBuilder(o).buildAsync(a);
@@ -1327,7 +1567,13 @@ var StructureBuilder = class _StructureBuilder {
1327
1567
  }
1328
1568
  applyField(obj, field, pathPrefix) {
1329
1569
  if (field.key.length === 0 && field.value.kind === "include") {
1330
- const included = this.loader.load(field.value.path, field.value.required, field.value.isFile);
1570
+ const { qualifier, path: path2, required } = field.value;
1571
+ let included;
1572
+ if (qualifier.kind === "package") {
1573
+ included = this.loader.loadPackage(qualifier.identifier, path2, required);
1574
+ } else {
1575
+ included = this.loader.load(path2, required, qualifier.kind === "file");
1576
+ }
1331
1577
  if (pathPrefix.length > 0) {
1332
1578
  this.relativizeResObj(included, pathPrefix);
1333
1579
  }
@@ -1369,7 +1615,13 @@ var StructureBuilder = class _StructureBuilder {
1369
1615
  }
1370
1616
  async applyFieldAsync(obj, field, pathPrefix) {
1371
1617
  if (field.key.length === 0 && field.value.kind === "include") {
1372
- const included = await this.loader.loadAsync(field.value.path, field.value.required, field.value.isFile);
1618
+ const { qualifier, path: path2, required } = field.value;
1619
+ let included;
1620
+ if (qualifier.kind === "package") {
1621
+ included = await this.loader.loadPackageAsync(qualifier.identifier, path2, required);
1622
+ } else {
1623
+ included = await this.loader.loadAsync(path2, required, qualifier.kind === "file");
1624
+ }
1373
1625
  if (pathPrefix.length > 0) {
1374
1626
  this.relativizeResObj(included, pathPrefix);
1375
1627
  }
@@ -1423,9 +1675,9 @@ var StructureBuilder = class _StructureBuilder {
1423
1675
  return inner;
1424
1676
  }
1425
1677
  case "subst":
1426
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1678
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1427
1679
  case "concat":
1428
- return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)) };
1680
+ return { _kind: "concat-placeholder", nodes: ast.nodes.map((n) => this.astToResolverValue(n, pathPrefix)), line: ast.pos.line, col: ast.pos.col };
1429
1681
  case "include":
1430
1682
  return { kind: "scalar", raw: "null", valueType: "null" };
1431
1683
  }
@@ -1447,13 +1699,13 @@ var StructureBuilder = class _StructureBuilder {
1447
1699
  case "object":
1448
1700
  return await this.buildAsync(ast, pathPrefix);
1449
1701
  case "subst":
1450
- return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1702
+ return { _kind: "subst-placeholder", segments: ast.segments, optional: ast.optional, listSuffix: ast.listSuffix, line: ast.pos.line, col: ast.pos.col, prefixLen: 0 };
1451
1703
  case "concat": {
1452
1704
  const nodes = [];
1453
1705
  for (const n of ast.nodes) {
1454
1706
  nodes.push(await this.astToResolverValueAsync(n, pathPrefix));
1455
1707
  }
1456
- return { _kind: "concat-placeholder", nodes };
1708
+ return { _kind: "concat-placeholder", nodes, line: ast.pos.line, col: ast.pos.col };
1457
1709
  }
1458
1710
  case "include":
1459
1711
  return { kind: "scalar", raw: "null", valueType: "null" };
@@ -1499,6 +1751,23 @@ var StructureBuilder = class _StructureBuilder {
1499
1751
  }
1500
1752
  };
1501
1753
 
1754
+ // src/value/numeric-array.ts
1755
+ function numericObjectToArray(value) {
1756
+ if (value.kind !== "object") return null;
1757
+ if (value.fields.size === 0) return null;
1758
+ const canonicalInt = /^(0|[1-9][0-9]*)$/;
1759
+ const eligible = [];
1760
+ for (const [key, val] of value.fields) {
1761
+ if (!canonicalInt.test(key)) continue;
1762
+ const n = Number(key);
1763
+ if (!Number.isInteger(n) || n > 2147483647) continue;
1764
+ eligible.push({ n, v: val });
1765
+ }
1766
+ if (eligible.length === 0) return null;
1767
+ eligible.sort((a, b) => a.n - b.n);
1768
+ return eligible.map((e) => e.v);
1769
+ }
1770
+
1502
1771
  // src/internal/resolver/substitution-resolver.ts
1503
1772
  var SubstitutionResolver = class {
1504
1773
  constructor(root, opts) {
@@ -1506,6 +1775,23 @@ var SubstitutionResolver = class {
1506
1775
  this.opts = opts;
1507
1776
  this.resolving = /* @__PURE__ */ new Set();
1508
1777
  this.cache = /* @__PURE__ */ new Map();
1778
+ // Tracks ConcatPlaceholder nodes currently being iterated by resolveConcat.
1779
+ // Used by isSelfRef detection: fires only when found === a concat currently
1780
+ // mid-iteration (i.e. we are inside found's own resolution), preventing the
1781
+ // "foofoo" double-count without misfiring when an external field looks up the
1782
+ // same concat via a substitution (multi-reviewer convergence fix).
1783
+ //
1784
+ // Spec deviation: the S13a.13 spec ★1 decision #1 specified path-equality
1785
+ // preservation for self-ref detection. Round-2 multi-agent-review surfaced a
1786
+ // false-positive on external lookups (`a = ${?a}foo; b = ${a}`), so the
1787
+ // criterion was tightened to node-membership-in-iterating-set — strictly
1788
+ // narrower than path-equality. Spec amendment deferred to a follow-up
1789
+ // xx.hocon PR (see Phase 6 #3f close-out notes).
1790
+ this.resolvingConcats = /* @__PURE__ */ new WeakSet();
1791
+ }
1792
+ /** Returns a prefix for error messages, including origin info when set. */
1793
+ originPrefix() {
1794
+ return this.opts.originDescription ? `${this.opts.originDescription}: ` : "";
1509
1795
  }
1510
1796
  resolve() {
1511
1797
  return this.resolveResObj(this.root);
@@ -1544,7 +1830,15 @@ var SubstitutionResolver = class {
1544
1830
  }
1545
1831
  resolveVal(v, scope) {
1546
1832
  if (isSubst(v)) return this.resolveSubst(v, scope);
1547
- if (isConcat(v)) return this.resolveConcat(v.nodes, scope);
1833
+ if (isConcat(v)) {
1834
+ this.resolvingConcats.add(v);
1835
+ try {
1836
+ const concatResult = this.resolveConcat(v.nodes, scope, v.line, v.col);
1837
+ return concatResult;
1838
+ } finally {
1839
+ this.resolvingConcats.delete(v);
1840
+ }
1841
+ }
1548
1842
  if (isAppend(v)) return this.resolveAppend(v, scope);
1549
1843
  if (isResObj(v)) return this.resolveResObj(v);
1550
1844
  const hv = v;
@@ -1558,15 +1852,12 @@ var SubstitutionResolver = class {
1558
1852
  }
1559
1853
  return hv;
1560
1854
  }
1561
- segmentsEqual(a, b) {
1562
- return a.length === b.length && a.every((seg, i) => seg.text === b[i]?.text);
1563
- }
1564
1855
  resolveSubst(s, scope) {
1565
- const key = segmentsToKey(s.segments);
1856
+ const key = s.listSuffix ? `${segmentsToKey(s.segments)}[]` : segmentsToKey(s.segments);
1566
1857
  if (this.cache.has(key)) return this.cache.get(key);
1567
1858
  if (this.resolving.has(key)) {
1568
1859
  let prior;
1569
- if (s.prefixLen > 0) {
1860
+ if (s.segments.length > 1) {
1570
1861
  const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1571
1862
  const parentScope = lookupResObj(
1572
1863
  this.root,
@@ -1588,7 +1879,7 @@ var SubstitutionResolver = class {
1588
1879
  }
1589
1880
  if (s.optional) return void 0;
1590
1881
  throw new ResolveError(
1591
- `circular substitution: ${key}`,
1882
+ this.originPrefix() + `circular substitution: ${key}`,
1592
1883
  key,
1593
1884
  s.line,
1594
1885
  s.col
@@ -1598,29 +1889,31 @@ var SubstitutionResolver = class {
1598
1889
  try {
1599
1890
  const found = lookupPath(this.root, s.segments);
1600
1891
  if (found !== void 0) {
1601
- if (isSubst(found) || isConcat(found)) {
1602
- const isSelfRef = isSubst(found) ? this.segmentsEqual(found.segments, s.segments) : isConcat(found) && found.nodes.some(
1603
- (n) => isSubst(n) && this.segmentsEqual(n.segments, s.segments)
1604
- );
1605
- if (isSelfRef) {
1606
- let prior;
1607
- if (s.prefixLen > 0) {
1608
- const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1609
- const parentScope = lookupResObj(
1610
- this.root,
1611
- s.segments.slice(0, s.segments.length - 1)
1612
- );
1613
- prior = parentScope?.priorValues.get(leafSeg);
1614
- } else {
1615
- const rootSeg = s.segments[0]?.text ?? "";
1616
- prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1617
- }
1618
- if (prior !== void 0) {
1619
- const result2 = this.resolveVal(prior, scope);
1620
- if (result2 !== void 0) this.cache.set(key, result2);
1621
- return result2;
1622
- }
1892
+ if (isConcat(found) && this.resolvingConcats.has(found)) {
1893
+ let prior;
1894
+ if (s.segments.length > 1) {
1895
+ const leafSeg = s.segments[s.segments.length - 1]?.text ?? "";
1896
+ const parentScope = lookupResObj(
1897
+ this.root,
1898
+ s.segments.slice(0, s.segments.length - 1)
1899
+ );
1900
+ prior = parentScope?.priorValues.get(leafSeg);
1901
+ } else {
1902
+ const rootSeg = s.segments[0]?.text ?? "";
1903
+ prior = scope.priorValues.get(rootSeg) ?? this.root.priorValues.get(rootSeg);
1904
+ }
1905
+ if (prior !== void 0) {
1906
+ const result2 = this.resolveVal(prior, scope);
1907
+ if (result2 !== void 0) this.cache.set(key, result2);
1908
+ return result2;
1623
1909
  }
1910
+ if (s.optional) return void 0;
1911
+ throw new ResolveError(
1912
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1913
+ key,
1914
+ s.line,
1915
+ s.col
1916
+ );
1624
1917
  }
1625
1918
  let result = this.resolveVal(found, scope);
1626
1919
  const effectiveLen = s.segments.length - s.prefixLen;
@@ -1649,16 +1942,45 @@ var SubstitutionResolver = class {
1649
1942
  if (result !== void 0) this.cache.set(key, result);
1650
1943
  return result;
1651
1944
  }
1652
- const envKey = s.segments.map((seg) => seg.text).join(".");
1653
- const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1654
- if (envVal !== void 0) {
1655
- const result = { kind: "scalar", raw: envVal, valueType: "string" };
1656
- this.cache.set(key, result);
1657
- return result;
1945
+ if (s.listSuffix && this.opts.useSystemEnvironment !== false) {
1946
+ const result = this.resolveEnvList(s);
1947
+ if (result !== void 0) {
1948
+ this.cache.set(key, result);
1949
+ return result;
1950
+ }
1951
+ if (s.optional) return void 0;
1952
+ const envBase = s.segments.map((seg) => seg.text).join(".");
1953
+ throw new ResolveError(
1954
+ `${this.originPrefix()}could not resolve substitution: \${${key}} (no environment variables ${envBase}_0, ${envBase}_1, \u2026 set)`,
1955
+ key,
1956
+ s.line,
1957
+ s.col
1958
+ );
1959
+ } else if (s.listSuffix) {
1960
+ if (s.optional) return void 0;
1961
+ if (this.opts.allowUnresolved) return s;
1962
+ throw new ResolveError(
1963
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1964
+ key,
1965
+ s.line,
1966
+ s.col
1967
+ );
1968
+ }
1969
+ if (this.opts.useSystemEnvironment !== false) {
1970
+ const envKey = s.segments.map((seg) => seg.text).join(".");
1971
+ const envVal = this.opts.env[envKey] ?? (s.prefixLen > 0 ? this.opts.env[s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".")] : void 0);
1972
+ if (envVal !== void 0) {
1973
+ const result = { kind: "scalar", raw: envVal, valueType: "string" };
1974
+ this.cache.set(key, result);
1975
+ return result;
1976
+ }
1658
1977
  }
1659
1978
  if (s.optional) return void 0;
1979
+ if (this.opts.allowUnresolved) {
1980
+ return s;
1981
+ }
1660
1982
  throw new ResolveError(
1661
- `could not resolve substitution: \${${key}}`,
1983
+ `${this.originPrefix()}could not resolve substitution: \${${key}}`,
1662
1984
  key,
1663
1985
  s.line,
1664
1986
  s.col
@@ -1667,9 +1989,52 @@ var SubstitutionResolver = class {
1667
1989
  this.resolving.delete(key);
1668
1990
  }
1669
1991
  }
1670
- resolveConcat(nodes, scope) {
1671
- const resolved = nodes.map((n) => this.resolveVal(n, scope)).filter((v) => v !== void 0);
1672
- if (resolved.length === 0) return { kind: "scalar", raw: "null", valueType: "null" };
1992
+ /**
1993
+ * S13c: scan environment for NAME_0, NAME_1, … and return an Array<Scalar(String)>.
1994
+ *
1995
+ * Candidate bases follow the same fully-qualified→bare order used by the scalar
1996
+ * env fallback. The first base whose _0 key is present wins entirely — no
1997
+ * cross-base merging (spec §Relativized fallback precedence rules).
1998
+ *
1999
+ * Returns the HoconValue Array if any candidate base has _0, or undefined if
2000
+ * none do (caller applies optional/required handling).
2001
+ */
2002
+ resolveEnvList(s) {
2003
+ const bases = this.candidateBases(s);
2004
+ for (const base of bases) {
2005
+ const key0 = base + "_0";
2006
+ if (!(key0 in this.opts.env)) continue;
2007
+ const items = [];
2008
+ for (let i = 0; ; i++) {
2009
+ const k = base + "_" + i;
2010
+ if (!(k in this.opts.env)) break;
2011
+ items.push({ kind: "scalar", raw: this.opts.env[k], valueType: "string" });
2012
+ }
2013
+ return { kind: "array", items };
2014
+ }
2015
+ return void 0;
2016
+ }
2017
+ /**
2018
+ * Return the candidate base names for env-var-list lookup, in precedence order.
2019
+ * Mirrors the fully-qualified→bare fallback used by the scalar env path.
2020
+ */
2021
+ candidateBases(s) {
2022
+ const full = s.segments.map((seg) => seg.text).join(".");
2023
+ if (s.prefixLen === 0) return [full];
2024
+ const bare = s.segments.slice(s.prefixLen).map((seg) => seg.text).join(".");
2025
+ return [full, bare];
2026
+ }
2027
+ resolveConcat(nodes, scope, line = 0, col = 0) {
2028
+ const rawResolved = nodes.map((n) => this.resolveVal(n, scope));
2029
+ if (this.opts.allowUnresolved) {
2030
+ for (const v of rawResolved) {
2031
+ if (v !== void 0 && isSubst(v)) {
2032
+ return v;
2033
+ }
2034
+ }
2035
+ }
2036
+ const resolved = rawResolved.filter((v) => v !== void 0);
2037
+ if (resolved.length === 0) return void 0;
1673
2038
  if (resolved.length === 1) return resolved[0];
1674
2039
  const nonSep = resolved.filter((v) => !separatorValues.has(v));
1675
2040
  if (nonSep.length === 0) {
@@ -1685,27 +2050,45 @@ var SubstitutionResolver = class {
1685
2050
  if (converted !== null) {
1686
2051
  return { kind: "array", items: [...left.items, ...converted] };
1687
2052
  }
1688
- return { kind: "array", items: [...left.items, right] };
2053
+ throw new ResolveError(
2054
+ "cannot concatenate array with object: value concatenation requires same-kind operands (S10.4)",
2055
+ "",
2056
+ line,
2057
+ col
2058
+ );
1689
2059
  }
1690
2060
  if (left.kind === "object" && right.kind === "array") {
1691
2061
  const converted = numericObjectToArray(left);
1692
2062
  if (converted !== null) {
1693
2063
  return { kind: "array", items: [...converted, ...right.items] };
1694
2064
  }
1695
- return { kind: "array", items: [left, ...right.items] };
2065
+ throw new ResolveError(
2066
+ "cannot concatenate object with array: value concatenation requires same-kind operands (S10.4)",
2067
+ "",
2068
+ line,
2069
+ col
2070
+ );
1696
2071
  }
1697
2072
  if (left.kind === "array" && right.kind === "array") {
1698
2073
  return { kind: "array", items: [...left.items, ...right.items] };
1699
2074
  }
1700
- if (left.kind === "array") {
1701
- return { kind: "array", items: [...left.items, right] };
2075
+ if (left.kind === "array" || right.kind === "array") {
2076
+ throw new ResolveError(
2077
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
2078
+ "",
2079
+ line,
2080
+ col
2081
+ );
1702
2082
  }
1703
- if (right.kind === "array") {
1704
- return { kind: "array", items: [left, ...right.items] };
2083
+ if (left.kind === "object" || right.kind === "object") {
2084
+ throw new ResolveError(
2085
+ `cannot concatenate ${left.kind} with ${right.kind}: arrays and objects may not appear in string value concatenation (S10.13)`,
2086
+ "",
2087
+ line,
2088
+ col
2089
+ );
1705
2090
  }
1706
- const leftStr = left.kind === "scalar" ? left.raw : JSON.stringify(left);
1707
- const rightStr = right.kind === "scalar" ? right.raw : JSON.stringify(right);
1708
- return { kind: "scalar", raw: leftStr + rightStr, valueType: "string" };
2091
+ return { kind: "scalar", raw: left.raw + right.raw, valueType: "string" };
1709
2092
  };
1710
2093
  const [head, ...tail] = nonSep;
1711
2094
  const folded = tail.reduce(joinPair, head);
@@ -1725,18 +2108,503 @@ var SubstitutionResolver = class {
1725
2108
  };
1726
2109
 
1727
2110
  // src/internal/resolver/resolver.ts
2111
+ function buildTree(ast, opts) {
2112
+ return new StructureBuilder(opts).build(ast);
2113
+ }
2114
+ function resolveTree(tree, opts) {
2115
+ return new SubstitutionResolver(tree, opts).resolve();
2116
+ }
2117
+ function containsPlaceholders(tree) {
2118
+ for (const val of tree.fields.values()) {
2119
+ if (valContainsPlaceholders(val)) return true;
2120
+ }
2121
+ return false;
2122
+ }
2123
+ function valContainsPlaceholders(v) {
2124
+ const rv = v;
2125
+ if (isSubst(rv) || isConcat(rv) || isAppend(rv)) return true;
2126
+ if (isResObj(rv)) return containsPlaceholders(rv);
2127
+ const hv = v;
2128
+ if (hv?.kind === "array") {
2129
+ return hv.items.some((item) => valContainsPlaceholders(item));
2130
+ }
2131
+ return false;
2132
+ }
2133
+ function buildPartialHoconFromResObj(tree) {
2134
+ const fields = /* @__PURE__ */ new Map();
2135
+ for (const [k, v] of tree.fields) {
2136
+ if (!isSubst(v) && !isConcat(v) && !isAppend(v)) {
2137
+ if (isResObj(v)) {
2138
+ fields.set(k, buildPartialHoconFromResObj(v));
2139
+ } else {
2140
+ if (!valContainsPlaceholders(v)) {
2141
+ fields.set(k, v);
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ return { kind: "object", fields };
2147
+ }
2148
+ function hoconValueToResObj2(value) {
2149
+ const obj = makeResObj();
2150
+ for (const [k, v] of value.fields) {
2151
+ if (v.kind === "object") {
2152
+ obj.fields.set(k, hoconValueToResObj2(v));
2153
+ } else {
2154
+ obj.fields.set(k, v);
2155
+ }
2156
+ }
2157
+ return obj;
2158
+ }
1728
2159
  function resolve2(ast, opts) {
1729
- const root = new StructureBuilder(opts).build(ast);
1730
- return new SubstitutionResolver(root, opts).resolve();
2160
+ const tree = buildTree(ast, opts);
2161
+ return resolveTree(tree, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
1731
2162
  }
1732
2163
  async function resolveAsync(ast, opts) {
1733
2164
  const root = await new StructureBuilder(opts).buildAsync(ast);
1734
- return new SubstitutionResolver(root, opts).resolve();
2165
+ return resolveTree(root, { ...opts, useSystemEnvironment: true, allowUnresolved: false });
2166
+ }
2167
+
2168
+ // src/config.ts
2169
+ var Config = class _Config {
2170
+ constructor(root, opts) {
2171
+ this.root = root;
2172
+ this._resolved = opts?.resolved ?? true;
2173
+ this._parseBaseDir = opts?.parseBaseDir;
2174
+ this._originDescription = opts?.originDescription;
2175
+ this._resObjRoot = opts?.resObjRoot;
2176
+ this._resolveOpts = opts?.resolveOpts;
2177
+ }
2178
+ /** Returns true when the Config's value tree contains no unresolved
2179
+ * substitution placeholders. Whole-config granularity (E12 decision 11). */
2180
+ isResolved() {
2181
+ return this._resolved;
2182
+ }
2183
+ /** @internal Test-only: construct a resolved Config from a HoconValue. */
2184
+ static _fromResolvedValue(root, opts) {
2185
+ return new _Config(root, { resolved: true, originDescription: opts?.originDescription });
2186
+ }
2187
+ /** @internal Construct an unresolved Config carrying a ResObj tree.
2188
+ * Used by parseStringWithOptions (deferred path) in T6. */
2189
+ static _fromUnresolvedResObj(tree, opts) {
2190
+ const partialRoot = buildPartialHoconFromResObj(tree);
2191
+ return new _Config(partialRoot, {
2192
+ resolved: opts.resolved,
2193
+ parseBaseDir: opts.parseBaseDir,
2194
+ originDescription: opts.originDescription,
2195
+ resObjRoot: tree,
2196
+ resolveOpts: opts.resolveOpts
2197
+ });
2198
+ }
2199
+ get(path2) {
2200
+ const v = this.lookupNode(path2);
2201
+ if (v === void 0) return void 0;
2202
+ return hoconToJs(v);
2203
+ }
2204
+ getString(path2) {
2205
+ const v = this.requireScalar(path2);
2206
+ return v.raw;
2207
+ }
2208
+ getNumber(path2) {
2209
+ const v = this.requireScalar(path2);
2210
+ const coerced = coerceNumber(v.raw);
2211
+ if (coerced !== void 0) return coerced;
2212
+ throw new ConfigError(`expected number at ${path2}, got ${v.valueType}`, path2);
2213
+ }
2214
+ getBoolean(path2) {
2215
+ const v = this.requireScalar(path2);
2216
+ const coerced = coerceBoolean(v.raw);
2217
+ if (coerced !== void 0) return coerced;
2218
+ throw new ConfigError(`expected boolean at ${path2}, got ${v.valueType}`, path2);
2219
+ }
2220
+ getDuration(path2, unit) {
2221
+ const v = this.requireScalar(path2);
2222
+ if (v.valueType !== "string" && v.valueType !== "number") {
2223
+ throw new ConfigError(`expected duration at ${path2}, got ${v.valueType}`, path2);
2224
+ }
2225
+ const result = parseDuration(v.raw, unit);
2226
+ if (Number.isNaN(result)) throw new ConfigError(`invalid duration at ${path2}: ${JSON.stringify(v.raw)}`, path2);
2227
+ return result;
2228
+ }
2229
+ getBytes(path2, unit) {
2230
+ const v = this.requireScalar(path2);
2231
+ if (v.valueType !== "string" && v.valueType !== "number") {
2232
+ throw new ConfigError(`expected byte size at ${path2}, got ${v.valueType}`, path2);
2233
+ }
2234
+ const result = parseBytes(v.raw, unit);
2235
+ if (Number.isNaN(result)) throw new ConfigError(`invalid byte size at ${path2}: ${JSON.stringify(v.raw)}`, path2);
2236
+ if (result < 0) throw new ConfigError(`byte size must be non-negative at ${path2}: ${JSON.stringify(v.raw)}`, path2);
2237
+ return result;
2238
+ }
2239
+ getConfig(path2) {
2240
+ const v = this.lookupNode(path2);
2241
+ if (v === void 0) {
2242
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2243
+ throw new NotResolvedError(path2);
2244
+ }
2245
+ throw new ConfigError(`path not found: ${path2}`, path2);
2246
+ }
2247
+ if (v.kind !== "object") throw new ConfigError(`expected object at ${path2}`, path2);
2248
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2249
+ throw new NotResolvedError(path2);
2250
+ }
2251
+ return new _Config(v, { resolved: this._resolved });
2252
+ }
2253
+ getList(path2) {
2254
+ const v = this.lookupNode(path2);
2255
+ if (v === void 0) {
2256
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2257
+ throw new NotResolvedError(path2);
2258
+ }
2259
+ throw new ConfigError(`path not found: ${path2}`, path2);
2260
+ }
2261
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2262
+ throw new NotResolvedError(path2);
2263
+ }
2264
+ if (v.kind === "object") {
2265
+ const converted = numericObjectToArray(v);
2266
+ if (converted !== null) return converted.map(hoconToJs);
2267
+ }
2268
+ if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
2269
+ return v.items.map(hoconToJs);
2270
+ }
2271
+ has(path2) {
2272
+ return this.lookupNode(path2) !== void 0;
2273
+ }
2274
+ keys() {
2275
+ return [...this.root.fields.keys()];
2276
+ }
2277
+ withFallback(fallback) {
2278
+ if (fallback == null) return this;
2279
+ const selfTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2280
+ const fbTree = fallback._resObjRoot ?? hoconValueToResObj2(fallback.root);
2281
+ const merged = mergeUnresolved(selfTree, fbTree);
2282
+ const hasPlaceholders = containsPlaceholders(merged);
2283
+ const partialRoot = buildPartialHoconFromResObj(merged);
2284
+ return new _Config(partialRoot, {
2285
+ resolved: !hasPlaceholders,
2286
+ parseBaseDir: this._parseBaseDir,
2287
+ originDescription: this._originDescription,
2288
+ resObjRoot: merged,
2289
+ resolveOpts: this._resolveOpts ?? fallback._resolveOpts
2290
+ });
2291
+ }
2292
+ /**
2293
+ * Runs substitution resolution (phase 2) on the stored unresolved tree.
2294
+ * Idempotent: calling resolve() on an already-resolved Config returns an
2295
+ * equivalent resolved Config.
2296
+ *
2297
+ * opts.allowUnresolved (default false) — when true, leaves unresolved
2298
+ * non-optional placeholders in place rather than throwing.
2299
+ * opts.useSystemEnvironment (default true) — when false, env var lookups
2300
+ * are suppressed (hermetic resolution).
2301
+ *
2302
+ * E12 decision 3.
2303
+ */
2304
+ resolve(opts = {}) {
2305
+ if (this._resolved) {
2306
+ return new _Config(this.root, {
2307
+ resolved: true,
2308
+ parseBaseDir: this._parseBaseDir,
2309
+ originDescription: this._originDescription
2310
+ });
2311
+ }
2312
+ const tree = this._resObjRoot;
2313
+ if (!tree) {
2314
+ return new _Config(this.root, { resolved: true });
2315
+ }
2316
+ const resolveOpts = {
2317
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2318
+ throw new Error("no files");
2319
+ } },
2320
+ allowUnresolved: opts.allowUnresolved ?? false,
2321
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2322
+ originDescription: this._originDescription
2323
+ };
2324
+ const resolved = resolveTree(tree, resolveOpts);
2325
+ if (resolved.kind !== "object") throw new Error("resolve: expected object root");
2326
+ if (opts.allowUnresolved) {
2327
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(resolved);
2328
+ return new _Config(stripped, {
2329
+ resolved: !hadPlaceholders,
2330
+ parseBaseDir: this._parseBaseDir,
2331
+ originDescription: this._originDescription,
2332
+ resObjRoot: hadPlaceholders ? tree : void 0,
2333
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2334
+ });
2335
+ }
2336
+ return new _Config(resolved, {
2337
+ resolved: true,
2338
+ parseBaseDir: this._parseBaseDir,
2339
+ originDescription: this._originDescription
2340
+ });
2341
+ }
2342
+ /**
2343
+ * Resolves receiver substitutions using source as lookup context.
2344
+ * Source's keys are NOT merged into the result — differs from
2345
+ * `this.withFallback(source).resolve()` which includes source keys.
2346
+ *
2347
+ * Precondition (E12 decision 10): source must be resolved. If
2348
+ * source.isResolved() is false, throws NotResolvedError.
2349
+ *
2350
+ * On an already-resolved receiver, resolveWith is a no-op (returns an
2351
+ * equivalent resolved Config, source keys not included).
2352
+ *
2353
+ * E12 decisions 9, 10.
2354
+ */
2355
+ resolveWith(source, opts = {}) {
2356
+ if (!source.isResolved()) {
2357
+ throw new NotResolvedError("source");
2358
+ }
2359
+ if (this._resolved) {
2360
+ return new _Config(this.root, {
2361
+ resolved: true,
2362
+ parseBaseDir: this._parseBaseDir,
2363
+ originDescription: this._originDescription
2364
+ });
2365
+ }
2366
+ const receiverTree = this._resObjRoot ?? hoconValueToResObj2(this.root);
2367
+ const srcTree = hoconValueToResObj2(source.root);
2368
+ const merged = mergeUnresolved(receiverTree, srcTree);
2369
+ const resolveOpts = {
2370
+ ...this._resolveOpts ?? { env: {}, baseDir: void 0, readFileSync: () => {
2371
+ throw new Error("no files");
2372
+ } },
2373
+ allowUnresolved: opts.allowUnresolved ?? false,
2374
+ useSystemEnvironment: opts.useSystemEnvironment ?? true,
2375
+ originDescription: this._originDescription
2376
+ };
2377
+ const resolved = resolveTree(merged, resolveOpts);
2378
+ if (resolved.kind !== "object") throw new Error("resolveWith: expected object root");
2379
+ const receiverShapeForFilter = resObjToKeyShape(receiverTree);
2380
+ const filtered = filterByReceiverShape(resolved, receiverShapeForFilter);
2381
+ if (opts.allowUnresolved) {
2382
+ const { stripped, hadPlaceholders } = stripPlaceholderFields(filtered);
2383
+ return new _Config(stripped, {
2384
+ resolved: !hadPlaceholders,
2385
+ parseBaseDir: this._parseBaseDir,
2386
+ originDescription: this._originDescription,
2387
+ resObjRoot: hadPlaceholders ? receiverTree : void 0,
2388
+ resolveOpts: hadPlaceholders ? resolveOpts : void 0
2389
+ });
2390
+ }
2391
+ return new _Config(filtered, {
2392
+ resolved: true,
2393
+ parseBaseDir: this._parseBaseDir,
2394
+ originDescription: this._originDescription
2395
+ });
2396
+ }
2397
+ toObject() {
2398
+ return hoconToJs(this.root);
2399
+ }
2400
+ /**
2401
+ * @internal Test-only: renders this resolved Config's value tree as canonical
2402
+ * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
2403
+ * against Lightbend ground truth. NOT part of the public API.
2404
+ *
2405
+ * Throws if the Config is unresolved (contains placeholders).
2406
+ */
2407
+ _renderJSONForTest() {
2408
+ return renderHoconAsJSON(this.root);
2409
+ }
2410
+ lookupNode(path2) {
2411
+ const segments = splitConfigPath(path2);
2412
+ let current = this.root;
2413
+ for (const seg of segments) {
2414
+ if (current.kind !== "object") return void 0;
2415
+ const next = current.fields.get(seg);
2416
+ if (next === void 0) return void 0;
2417
+ current = next;
2418
+ }
2419
+ return current;
2420
+ }
2421
+ /**
2422
+ * _resObjRootSubtreeHasPlaceholders — walk _resObjRoot to the given path and
2423
+ * return true if the subtree there contains any unresolved placeholder.
2424
+ * Used by getConfig / getList (T2) to detect transitive unresolved placeholders
2425
+ * in object / array subtrees that would otherwise appear to be "resolved" in the
2426
+ * partial HoconValue root (because individual leaf fields are omitted rather than
2427
+ * the whole subtree when their value is a direct placeholder scalar, but object
2428
+ * sub-containers are retained in the partial root with the placeholder-keyed
2429
+ * fields omitted from them — so the object appears "present but empty-ish").
2430
+ */
2431
+ _resObjRootSubtreeHasPlaceholders(path2) {
2432
+ const tree = this._resObjRoot;
2433
+ if (!tree) return false;
2434
+ const segments = splitConfigPath(path2);
2435
+ let cur = tree;
2436
+ for (const seg of segments) {
2437
+ const val = cur.fields.get(seg);
2438
+ if (val === void 0) return false;
2439
+ if (isResObj(val)) {
2440
+ cur = val;
2441
+ } else {
2442
+ return valContainsPlaceholders(val);
2443
+ }
2444
+ }
2445
+ return containsPlaceholders(cur);
2446
+ }
2447
+ requireScalar(path2) {
2448
+ const v = this.lookupNode(path2);
2449
+ if (v === void 0) {
2450
+ if (!this._resolved && this._resObjRootSubtreeHasPlaceholders(path2)) {
2451
+ throw new NotResolvedError(path2);
2452
+ }
2453
+ throw new ConfigError(`path not found: ${path2}`, path2);
2454
+ }
2455
+ if (v.kind !== "scalar") throw new ConfigError(`expected scalar at ${path2}, got ${v.kind}`, path2);
2456
+ return v;
2457
+ }
2458
+ };
2459
+ function splitConfigPath(path2) {
2460
+ const segments = [];
2461
+ let i = 0;
2462
+ while (i < path2.length) {
2463
+ if (path2[i] === '"') {
2464
+ i++;
2465
+ let segment = "";
2466
+ let closed = false;
2467
+ while (i < path2.length) {
2468
+ const ch = path2[i];
2469
+ if (ch === "\\" && i + 1 < path2.length) {
2470
+ const next = path2[i + 1];
2471
+ segment += next;
2472
+ i += 2;
2473
+ continue;
2474
+ }
2475
+ if (ch === '"') {
2476
+ closed = true;
2477
+ i++;
2478
+ break;
2479
+ }
2480
+ segment += ch;
2481
+ i++;
2482
+ }
2483
+ if (!closed) throw new ConfigError(`unterminated quoted path segment: ${path2}`, path2);
2484
+ segments.push(segment);
2485
+ if (i < path2.length && path2[i] === ".") i++;
2486
+ } else {
2487
+ const dot = path2.indexOf(".", i);
2488
+ if (dot === -1) {
2489
+ segments.push(path2.slice(i));
2490
+ break;
2491
+ }
2492
+ segments.push(path2.slice(i, dot));
2493
+ i = dot + 1;
2494
+ }
2495
+ }
2496
+ return segments;
2497
+ }
2498
+ function scalarToJs(raw, valueType) {
2499
+ switch (valueType) {
2500
+ case "null":
2501
+ return null;
2502
+ case "boolean":
2503
+ return raw === "true";
2504
+ case "number":
2505
+ return Number(raw);
2506
+ case "string":
2507
+ return raw;
2508
+ }
2509
+ }
2510
+ function hoconToJs(v) {
2511
+ switch (v.kind) {
2512
+ case "scalar":
2513
+ return scalarToJs(v.raw, v.valueType);
2514
+ case "array":
2515
+ return v.items.map(hoconToJs);
2516
+ case "object": {
2517
+ const obj = /* @__PURE__ */ Object.create(null);
2518
+ for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
2519
+ return Object.assign({}, obj);
2520
+ }
2521
+ }
2522
+ }
2523
+ function resObjToKeyShape(tree) {
2524
+ const fields = /* @__PURE__ */ new Map();
2525
+ for (const [k, v] of tree.fields) {
2526
+ if (isSubst(v) || isConcat(v) || isAppend(v)) {
2527
+ fields.set(k, { kind: "scalar", raw: "", valueType: "null" });
2528
+ } else if (isResObj(v)) {
2529
+ fields.set(k, resObjToKeyShape(v));
2530
+ } else {
2531
+ fields.set(k, v);
2532
+ }
2533
+ }
2534
+ return { kind: "object", fields };
2535
+ }
2536
+ function stripPlaceholderFields(v) {
2537
+ const fields = /* @__PURE__ */ new Map();
2538
+ let hadPlaceholders = false;
2539
+ for (const [k, val] of v.fields) {
2540
+ if (val._kind === "subst-placeholder" || val._kind === "concat-placeholder" || val._kind === "append-placeholder") {
2541
+ hadPlaceholders = true;
2542
+ } else if (val.kind === "object") {
2543
+ const inner = stripPlaceholderFields(val);
2544
+ if (inner.hadPlaceholders) hadPlaceholders = true;
2545
+ fields.set(k, inner.stripped);
2546
+ } else {
2547
+ fields.set(k, val);
2548
+ }
2549
+ }
2550
+ return { stripped: { kind: "object", fields }, hadPlaceholders };
2551
+ }
2552
+ function filterByReceiverShape(resolved, receiverShape) {
2553
+ const fields = /* @__PURE__ */ new Map();
2554
+ for (const [k, rv] of resolved.fields) {
2555
+ if (!receiverShape.fields.has(k)) continue;
2556
+ const receiverVal = receiverShape.fields.get(k);
2557
+ if (rv.kind === "object" && receiverVal.kind === "object") {
2558
+ fields.set(k, filterByReceiverShape(rv, receiverVal));
2559
+ } else {
2560
+ fields.set(k, rv);
2561
+ }
2562
+ }
2563
+ return { kind: "object", fields };
2564
+ }
2565
+ function renderHoconAsJSON(v) {
2566
+ switch (v.kind) {
2567
+ case "scalar": {
2568
+ switch (v.valueType) {
2569
+ case "null":
2570
+ return "null";
2571
+ case "boolean":
2572
+ return v.raw;
2573
+ case "number": {
2574
+ const n = Number(v.raw);
2575
+ if (Number.isFinite(n)) return v.raw;
2576
+ return JSON.stringify(v.raw);
2577
+ }
2578
+ case "string":
2579
+ return JSON.stringify(v.raw);
2580
+ }
2581
+ break;
2582
+ }
2583
+ case "array": {
2584
+ const items = v.items.map(renderHoconAsJSON);
2585
+ return `[${items.join(",")}]`;
2586
+ }
2587
+ case "object": {
2588
+ const keys = [...v.fields.keys()].sort();
2589
+ const pairs = keys.map((k) => {
2590
+ const val = v.fields.get(k);
2591
+ return `${JSON.stringify(k)}:${renderHoconAsJSON(val)}`;
2592
+ });
2593
+ return `{${pairs.join(",")}}`;
2594
+ }
2595
+ }
2596
+ throw new Error(`renderHoconAsJSON: unsupported value kind (unresolved placeholder?)`);
1735
2597
  }
1736
2598
 
1737
2599
  // src/parse.ts
1738
2600
  var fs = __toESM(require("fs"), 1);
1739
2601
  var path = __toESM(require("path"), 1);
2602
+ function defaultParseOptions() {
2603
+ return { resolveSubstitutions: true };
2604
+ }
2605
+ function defaultResolveOptions() {
2606
+ return { useSystemEnvironment: true, allowUnresolved: false };
2607
+ }
1740
2608
  function getEnv(opts) {
1741
2609
  if (opts.env !== void 0) return opts.env;
1742
2610
  if (typeof process !== "undefined" && process.env) return process.env;
@@ -1750,12 +2618,16 @@ async function defaultReadFile(filePath) {
1750
2618
  }
1751
2619
  function buildResolveContext(input, opts) {
1752
2620
  const tokens = tokenize(input);
2621
+ assertNonEmptyDocument(tokens, "input");
1753
2622
  const ast = parseTokens(tokens);
1754
2623
  const resolveOpts = {
1755
2624
  env: getEnv(opts),
1756
2625
  baseDir: opts.baseDir,
1757
2626
  readFileSync: opts.readFileSync ?? defaultReadFileSync,
1758
- readFile: opts.readFile
2627
+ readFile: opts.readFile,
2628
+ originDescription: opts.originDescription,
2629
+ resolveFrom: opts.resolveFrom,
2630
+ packageResolver: opts.packageResolver
1759
2631
  };
1760
2632
  return { ast, resolveOpts };
1761
2633
  }
@@ -1763,7 +2635,38 @@ function parse(input, opts = {}) {
1763
2635
  const { ast, resolveOpts } = buildResolveContext(input, opts);
1764
2636
  const value = resolve2(ast, resolveOpts);
1765
2637
  if (value.kind !== "object") throw new Error("resolved value is not an object");
1766
- return new Config(value);
2638
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
2639
+ }
2640
+ var parseString = parse;
2641
+ function parseStringWithOptions(input, opts = {}) {
2642
+ const resolveSubstitutions = opts.resolveSubstitutions ?? true;
2643
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
2644
+ if (resolveSubstitutions) {
2645
+ const value = resolve2(ast, resolveOpts);
2646
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
2647
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
2648
+ }
2649
+ const tree = buildTree(ast, resolveOpts);
2650
+ const hasPlaceholders = containsPlaceholders(tree);
2651
+ return Config._fromUnresolvedResObj(tree, {
2652
+ parseBaseDir: opts.baseDir,
2653
+ originDescription: opts.originDescription,
2654
+ resolved: !hasPlaceholders,
2655
+ resolveOpts
2656
+ });
2657
+ }
2658
+ function parseFileWithOptions(filePath, opts = {}) {
2659
+ if (typeof process === "undefined" && !opts.readFileSync) {
2660
+ throw new Error("parseFileWithOptions is not supported in browser environments without opts.readFileSync.");
2661
+ }
2662
+ const resolvedPath = path.resolve(filePath);
2663
+ const readFileSync2 = opts.readFileSync ?? defaultReadFileSync;
2664
+ const input = readFileSync2(resolvedPath);
2665
+ return parseStringWithOptions(input, {
2666
+ ...opts,
2667
+ baseDir: opts.baseDir ?? path.dirname(resolvedPath),
2668
+ readFileSync: readFileSync2
2669
+ });
1767
2670
  }
1768
2671
  async function parseAsync(input, opts = {}) {
1769
2672
  const { ast, resolveOpts } = buildResolveContext(input, opts);
@@ -1798,14 +2701,90 @@ async function parseFileAsync(filePath, opts = {}) {
1798
2701
  readFileSync: opts.readFileSync ?? defaultReadFileSync
1799
2702
  });
1800
2703
  }
2704
+
2705
+ // src/value-factory.ts
2706
+ function fromMap(values, originDescription) {
2707
+ const fields = coerceObject(values, "");
2708
+ const root = { kind: "object", fields };
2709
+ return Config._fromResolvedValue(root, { originDescription });
2710
+ }
2711
+ function empty(originDescription) {
2712
+ const root = { kind: "object", fields: /* @__PURE__ */ new Map() };
2713
+ return Config._fromResolvedValue(root, { originDescription });
2714
+ }
2715
+ function coerceObject(obj, atPath) {
2716
+ const fields = /* @__PURE__ */ new Map();
2717
+ for (const [k, v] of Object.entries(obj)) {
2718
+ const childPath = atPath ? `${atPath}.${k}` : k;
2719
+ fields.set(k, coerceValue(v, childPath));
2720
+ }
2721
+ return fields;
2722
+ }
2723
+ function coerceValue(v, atPath) {
2724
+ if (v === null) {
2725
+ return { kind: "scalar", raw: "null", valueType: "null" };
2726
+ }
2727
+ if (v === void 0) {
2728
+ throw new ConfigError(
2729
+ `fromMap: undefined is not a valid HOCON value at path "${atPath}"`,
2730
+ atPath
2731
+ );
2732
+ }
2733
+ switch (typeof v) {
2734
+ case "string":
2735
+ return { kind: "scalar", raw: v, valueType: "string" };
2736
+ case "number": {
2737
+ if (Number.isNaN(v) || !Number.isFinite(v)) {
2738
+ throw new ConfigError(
2739
+ `fromMap: ${v} is not representable in HOCON (NaN/Infinity not allowed) at path "${atPath}"`,
2740
+ atPath
2741
+ );
2742
+ }
2743
+ return { kind: "scalar", raw: String(v), valueType: "number" };
2744
+ }
2745
+ case "boolean":
2746
+ return { kind: "scalar", raw: v ? "true" : "false", valueType: "boolean" };
2747
+ case "bigint": {
2748
+ if (v > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER)) {
2749
+ throw new ConfigError(
2750
+ `fromMap: bigint ${v} exceeds Number.MAX_SAFE_INTEGER (not representable as HOCON number) at path "${atPath}"`,
2751
+ atPath
2752
+ );
2753
+ }
2754
+ return { kind: "scalar", raw: String(v), valueType: "number" };
2755
+ }
2756
+ case "object": {
2757
+ if (Array.isArray(v)) {
2758
+ const items = v.map((item, i) => coerceValue(item, `${atPath}[${i}]`));
2759
+ return { kind: "array", items };
2760
+ }
2761
+ const fields = coerceObject(v, atPath);
2762
+ return { kind: "object", fields };
2763
+ }
2764
+ default:
2765
+ throw new ConfigError(
2766
+ `fromMap: unsupported value type "${typeof v}" at path "${atPath}"`,
2767
+ atPath
2768
+ );
2769
+ }
2770
+ }
1801
2771
  // Annotate the CommonJS export names for ESM import in node:
1802
2772
  0 && (module.exports = {
1803
2773
  Config,
1804
2774
  ConfigError,
2775
+ NotResolvedError,
2776
+ PackageLookupError,
1805
2777
  ParseError,
1806
2778
  ResolveError,
2779
+ defaultParseOptions,
2780
+ defaultResolveOptions,
2781
+ empty,
2782
+ fromMap,
1807
2783
  parse,
1808
2784
  parseAsync,
1809
2785
  parseFile,
1810
- parseFileAsync
2786
+ parseFileAsync,
2787
+ parseFileWithOptions,
2788
+ parseString,
2789
+ parseStringWithOptions
1811
2790
  });