@o3co/ts.hocon 1.10.0 → 1.12.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.
@@ -316,7 +316,53 @@ declare class Config {
316
316
  * E12 decisions 9, 10.
317
317
  */
318
318
  resolveWith(source: Config, opts?: ResolveOptions): Config;
319
- toObject(): unknown;
319
+ /**
320
+ * The whole config as plain JS data: objects, arrays, strings, numbers,
321
+ * booleans and `null`.
322
+ *
323
+ * Keys are reproduced exactly as the config carries them, `__proto__`,
324
+ * `constructor` and `prototype` included: each is defined as an **own data
325
+ * property**, so the returned object's prototype is always
326
+ * `Object.prototype`, and this call writes nothing to the global one.
327
+ *
328
+ * ### Passing the result to other code
329
+ *
330
+ * A config file — especially a `.properties` file or an env namespace you
331
+ * merged in — can therefore hand you an own `__proto__` key, and what happens
332
+ * next depends entirely on how the consumer copies it:
333
+ *
334
+ * ```ts
335
+ * const data = cfg.toObject()
336
+ * { ...data } // safe — own properties are copied as data
337
+ * structuredClone(data) // safe
338
+ * Object.assign({}, data) // NOT safe — [[Set]] on the {} target hits
339
+ * // Object.prototype's __proto__ setter: the key
340
+ * // vanishes and config data becomes the result's
341
+ * // prototype
342
+ * deepMerge({}, data) // NOT safe — a naive recursive merge reads
343
+ * // target['__proto__'], gets Object.prototype,
344
+ * // and writes to it: global pollution
345
+ * for (const k in data) … // prefer Object.entries / Object.keys
346
+ * ```
347
+ *
348
+ * Those two hazards come from the **destination** object's prototype, so no
349
+ * option here can disarm them — copy with spread or `structuredClone` instead.
350
+ *
351
+ * What `{ nullPrototype: true }` does give you is a result that inherits
352
+ * nothing: every object comes back with a `null` prototype, so reading
353
+ * `x.__proto__`, `x.constructor` or `x.toString` yields config data or
354
+ * `undefined` rather than something from `Object.prototype`, and code that
355
+ * merges *into* this object cannot climb out of it. Keys are all still
356
+ * present. The trade-off is the usual one for prototype-less objects: not
357
+ * `instanceof Object`, and no `hasOwnProperty` of their own.
358
+ *
359
+ * Numbers follow the JS number model, so an integer wider than 2^53 rounds
360
+ * here even when the config holds it exactly — read those with
361
+ * {@link Config.getString} instead.
362
+ */
363
+ toObject(opts?: {
364
+ nullPrototype?: boolean;
365
+ }): unknown;
320
366
  /**
321
367
  * @internal Test-only: renders this resolved Config's value tree as canonical
322
368
  * JSON (sorted keys, no whitespace). Used by Layer-2 fixture tests to compare
package/dist/index.cjs CHANGED
@@ -700,6 +700,7 @@ function isFileNotFoundError(e) {
700
700
  // src/internal/resolver/include-loader.ts
701
701
  var import_node_module = require("module");
702
702
  var nodePath = __toESM(require("path"), 1);
703
+ var import_node_url = require("url");
703
704
 
704
705
  // src/internal/lexer/lexer.ts
705
706
  function isHoconWhitespace(ch) {
@@ -1542,25 +1543,80 @@ function parseTokens(tokens) {
1542
1543
  return new Parser(tokens).parse();
1543
1544
  }
1544
1545
 
1546
+ // src/internal/strip-bom.ts
1547
+ function stripBom(input) {
1548
+ return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1549
+ }
1550
+
1551
+ // src/internal/depth.ts
1552
+ var MAX_PATH_SEGMENTS = 64;
1553
+ function tooDeep(segments) {
1554
+ return segments > MAX_PATH_SEGMENTS;
1555
+ }
1556
+ function guardStackDepth(fn, wrap) {
1557
+ try {
1558
+ return fn();
1559
+ } catch (e) {
1560
+ if (!isStackOverflow(e)) throw e;
1561
+ throw wrap(STACK_MESSAGE);
1562
+ }
1563
+ }
1564
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
1565
+ function isStackOverflow(e) {
1566
+ if (!(e instanceof RangeError)) return false;
1567
+ const m = e.message.toLowerCase();
1568
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
1569
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
1570
+ m.includes("stack overflow");
1571
+ }
1572
+ function depthError(message) {
1573
+ return new ConfigError(message, "");
1574
+ }
1575
+ async function guardStackDepthAsync(fn, wrap) {
1576
+ try {
1577
+ return await fn();
1578
+ } catch (e) {
1579
+ if (!isStackOverflow(e)) throw e;
1580
+ throw wrap(STACK_MESSAGE);
1581
+ }
1582
+ }
1583
+
1545
1584
  // src/internal/properties/properties.ts
1546
1585
  function parseProperties(input) {
1547
1586
  const pairs = [];
1548
- for (const { text, line } of logicalLines(input)) {
1587
+ for (const { text, line } of logicalLines(stripBom(input))) {
1549
1588
  const [rawKey, rawValue] = splitKeyValue(text);
1550
1589
  const key = unescapeProps(rawKey, line);
1551
1590
  if (key === "") continue;
1552
- pairs.push([key, unescapeProps(rawValue, line)]);
1591
+ const segments = key.split(".");
1592
+ if (tooDeep(segments.length)) {
1593
+ throw new ParseError(
1594
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1595
+ line,
1596
+ 1
1597
+ );
1598
+ }
1599
+ pairs.push([segments, unescapeProps(rawValue, line)]);
1553
1600
  }
1554
1601
  return nestPairs(pairs);
1555
1602
  }
1556
1603
  function nestPairs(pairs) {
1557
1604
  const root = /* @__PURE__ */ Object.create(null);
1558
- const sorted = [...pairs].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1559
- for (const [key, value] of sorted) {
1560
- setNested(root, key.split("."), value);
1605
+ const sorted = [...pairs].sort(([a], [b]) => compareSegments(a, b));
1606
+ for (const [segments, value] of sorted) {
1607
+ setNested(root, segments, value);
1561
1608
  }
1562
1609
  return root;
1563
1610
  }
1611
+ function compareSegments(a, b) {
1612
+ const n = Math.min(a.length, b.length);
1613
+ for (let i = 0; i < n; i++) {
1614
+ const x = a[i];
1615
+ const y = b[i];
1616
+ if (x !== y) return x < y ? -1 : 1;
1617
+ }
1618
+ return a.length - b.length;
1619
+ }
1564
1620
  function logicalLines(input) {
1565
1621
  const natural = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
1566
1622
  const out = [];
@@ -1647,19 +1703,16 @@ function unescapeProps(s, line) {
1647
1703
  }
1648
1704
  return out;
1649
1705
  }
1650
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1651
1706
  function setNested(obj, segments, value) {
1652
1707
  let current = obj;
1653
1708
  for (let i = 0; i < segments.length - 1; i++) {
1654
1709
  const seg = segments[i];
1655
- if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
1656
1710
  if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
1657
1711
  current[seg] = /* @__PURE__ */ Object.create(null);
1658
1712
  }
1659
1713
  current = current[seg];
1660
1714
  }
1661
1715
  const last = segments[segments.length - 1];
1662
- if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1663
1716
  if (typeof current[last] === "object" && current[last] !== null) return;
1664
1717
  current[last] = value;
1665
1718
  }
@@ -1681,7 +1734,22 @@ function recordToHoconValue(obj) {
1681
1734
 
1682
1735
  // src/internal/resolver/include-loader.ts
1683
1736
  var import_meta = {};
1684
- var _require = (0, import_node_module.createRequire)(import_meta.url);
1737
+ var _require;
1738
+ function getRequire() {
1739
+ if (_require === void 0) {
1740
+ _require = pickRequire(
1741
+ typeof require === "function" ? require : void 0,
1742
+ import_meta.url
1743
+ );
1744
+ }
1745
+ return _require;
1746
+ }
1747
+ function pickRequire(candidate, metaUrl) {
1748
+ if (typeof candidate?.resolve === "function") {
1749
+ return candidate;
1750
+ }
1751
+ return (0, import_node_module.createRequire)(metaUrl ?? (0, import_node_url.pathToFileURL)(nodePath.join(process.cwd(), "noop.js")));
1752
+ }
1685
1753
  function isPnpPath(p) {
1686
1754
  return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1687
1755
  }
@@ -1737,7 +1805,7 @@ function defaultPackageResolver(identifier, file, includingFile, baseDir, resolv
1737
1805
  const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
1738
1806
  let resolved;
1739
1807
  try {
1740
- resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
1808
+ resolved = getRequire().resolve(`${identifier}/${file}`, { paths: from });
1741
1809
  } catch {
1742
1810
  throw new PackageLookupError(
1743
1811
  `include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
@@ -2791,10 +2859,10 @@ var Config = class _Config {
2791
2859
  }
2792
2860
  if (v.kind === "object") {
2793
2861
  const converted = numericObjectToArray(v);
2794
- if (converted !== null) return converted.map(hoconToJs);
2862
+ if (converted !== null) return converted.map((item) => hoconToJs(item));
2795
2863
  }
2796
2864
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
2797
- return v.items.map(hoconToJs);
2865
+ return v.items.map((item) => hoconToJs(item));
2798
2866
  }
2799
2867
  has(path2) {
2800
2868
  return this.lookupNode(path2) !== void 0;
@@ -2922,8 +2990,52 @@ var Config = class _Config {
2922
2990
  originDescription: this._originDescription
2923
2991
  });
2924
2992
  }
2925
- toObject() {
2926
- return hoconToJs(this.root);
2993
+ /**
2994
+ * The whole config as plain JS data: objects, arrays, strings, numbers,
2995
+ * booleans and `null`.
2996
+ *
2997
+ * Keys are reproduced exactly as the config carries them, `__proto__`,
2998
+ * `constructor` and `prototype` included: each is defined as an **own data
2999
+ * property**, so the returned object's prototype is always
3000
+ * `Object.prototype`, and this call writes nothing to the global one.
3001
+ *
3002
+ * ### Passing the result to other code
3003
+ *
3004
+ * A config file — especially a `.properties` file or an env namespace you
3005
+ * merged in — can therefore hand you an own `__proto__` key, and what happens
3006
+ * next depends entirely on how the consumer copies it:
3007
+ *
3008
+ * ```ts
3009
+ * const data = cfg.toObject()
3010
+ * { ...data } // safe — own properties are copied as data
3011
+ * structuredClone(data) // safe
3012
+ * Object.assign({}, data) // NOT safe — [[Set]] on the {} target hits
3013
+ * // Object.prototype's __proto__ setter: the key
3014
+ * // vanishes and config data becomes the result's
3015
+ * // prototype
3016
+ * deepMerge({}, data) // NOT safe — a naive recursive merge reads
3017
+ * // target['__proto__'], gets Object.prototype,
3018
+ * // and writes to it: global pollution
3019
+ * for (const k in data) … // prefer Object.entries / Object.keys
3020
+ * ```
3021
+ *
3022
+ * Those two hazards come from the **destination** object's prototype, so no
3023
+ * option here can disarm them — copy with spread or `structuredClone` instead.
3024
+ *
3025
+ * What `{ nullPrototype: true }` does give you is a result that inherits
3026
+ * nothing: every object comes back with a `null` prototype, so reading
3027
+ * `x.__proto__`, `x.constructor` or `x.toString` yields config data or
3028
+ * `undefined` rather than something from `Object.prototype`, and code that
3029
+ * merges *into* this object cannot climb out of it. Keys are all still
3030
+ * present. The trade-off is the usual one for prototype-less objects: not
3031
+ * `instanceof Object`, and no `hasOwnProperty` of their own.
3032
+ *
3033
+ * Numbers follow the JS number model, so an integer wider than 2^53 rounds
3034
+ * here even when the config holds it exactly — read those with
3035
+ * {@link Config.getString} instead.
3036
+ */
3037
+ toObject(opts) {
3038
+ return hoconToJs(this.root, opts?.nullPrototype === true);
2927
3039
  }
2928
3040
  /**
2929
3041
  * @internal Test-only: renders this resolved Config's value tree as canonical
@@ -3035,16 +3147,25 @@ function scalarToJs(raw, valueType) {
3035
3147
  return raw;
3036
3148
  }
3037
3149
  }
3038
- function hoconToJs(v) {
3150
+ function hoconToJs(v, nullPrototype = false) {
3039
3151
  switch (v.kind) {
3040
3152
  case "scalar":
3041
3153
  return scalarToJs(v.raw, v.valueType);
3042
3154
  case "array":
3043
- return v.items.map(hoconToJs);
3155
+ return v.items.map((item) => hoconToJs(item, nullPrototype));
3044
3156
  case "object": {
3045
- const obj = /* @__PURE__ */ Object.create(null);
3046
- for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
3047
- return Object.assign({}, obj);
3157
+ const entries = Array.from(v.fields, ([k, val]) => [k, hoconToJs(val, nullPrototype)]);
3158
+ if (nullPrototype) {
3159
+ const obj = /* @__PURE__ */ Object.create(null);
3160
+ for (const [k, val] of entries) Object.defineProperty(obj, k, {
3161
+ value: val,
3162
+ writable: true,
3163
+ enumerable: true,
3164
+ configurable: true
3165
+ });
3166
+ return obj;
3167
+ }
3168
+ return Object.fromEntries(entries);
3048
3169
  }
3049
3170
  }
3050
3171
  }
@@ -3169,13 +3290,16 @@ function buildResolveContext(input, opts) {
3169
3290
  return { ast, resolveOpts };
3170
3291
  }
3171
3292
  function parse(input, opts = {}) {
3172
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3173
- const value = resolve2(ast, resolveOpts);
3174
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3175
- return new Config(value, { resolved: true, originDescription: opts.originDescription });
3293
+ return parseStringWithOptions(input, opts);
3176
3294
  }
3177
3295
  var parseString = parse;
3178
3296
  function parseStringWithOptions(input, opts = {}) {
3297
+ return guardStackDepth(
3298
+ () => parseStringInner(input, opts),
3299
+ (msg) => new ParseError(msg, 1, 1, opts.originDescription)
3300
+ );
3301
+ }
3302
+ function parseStringInner(input, opts) {
3179
3303
  const resolveSubstitutions = opts.resolveSubstitutions ?? true;
3180
3304
  const { ast, resolveOpts } = buildResolveContext(input, opts);
3181
3305
  if (resolveSubstitutions) {
@@ -3206,10 +3330,12 @@ function parseFileWithOptions(filePath, opts = {}) {
3206
3330
  });
3207
3331
  }
3208
3332
  async function parseAsync(input, opts = {}) {
3209
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3210
- const value = await resolveAsync(ast, resolveOpts);
3211
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3212
- return new Config(value);
3333
+ return guardStackDepthAsync(async () => {
3334
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
3335
+ const value = await resolveAsync(ast, resolveOpts);
3336
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
3337
+ return new Config(value);
3338
+ }, (msg) => new ParseError(msg, 1, 1, opts.originDescription));
3213
3339
  }
3214
3340
  function parseFile(filePath, opts = {}) {
3215
3341
  if (typeof process === "undefined" && !opts.readFileSync) {
@@ -3270,7 +3396,10 @@ function isNull(v) {
3270
3396
 
3271
3397
  // src/value-factory.ts
3272
3398
  function fromMap(values, originDescription) {
3273
- const fields = coerceObject(values, "");
3399
+ const fields = guardStackDepth(
3400
+ () => coerceObject(values, ""),
3401
+ (msg) => depthError(`fromMap: ${msg}`)
3402
+ );
3274
3403
  const root = { kind: "object", fields };
3275
3404
  return Config._fromResolvedValue(root, { originDescription });
3276
3405
  }
@@ -3278,6 +3407,8 @@ function empty(originDescription) {
3278
3407
  const root = { kind: "object", fields: /* @__PURE__ */ new Map() };
3279
3408
  return Config._fromResolvedValue(root, { originDescription });
3280
3409
  }
3410
+ var INT64_MAX = 9223372036854775807n;
3411
+ var INT64_MIN = -9223372036854775808n;
3281
3412
  function coerceObject(obj, atPath) {
3282
3413
  const fields = /* @__PURE__ */ new Map();
3283
3414
  for (const [k, v] of Object.entries(obj)) {
@@ -3306,14 +3437,14 @@ function coerceValue(v, atPath) {
3306
3437
  atPath
3307
3438
  );
3308
3439
  }
3309
- return { kind: "scalar", raw: String(v), valueType: "number" };
3440
+ return { kind: "scalar", raw: Object.is(v, -0) ? "-0" : String(v), valueType: "number" };
3310
3441
  }
3311
3442
  case "boolean":
3312
3443
  return { kind: "scalar", raw: v ? "true" : "false", valueType: "boolean" };
3313
3444
  case "bigint": {
3314
- if (v > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER)) {
3445
+ if (v > INT64_MAX || v < INT64_MIN) {
3315
3446
  throw new ConfigError(
3316
- `fromMap: bigint ${v} exceeds Number.MAX_SAFE_INTEGER (not representable as HOCON number) at path "${atPath}"`,
3447
+ `fromMap: integer ${v} is out of int64 range (HOCON integers are int64; spec F0.5) at path "${atPath}"`,
3317
3448
  atPath
3318
3449
  );
3319
3450
  }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Config } from './config-PdhVgeLS.cjs';
2
- export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ReadonlyHoconValue, b as ResolveOptions, S as ScalarValueType, c as asArray, d as asBoolean, e as asNumber, f as asObject, g as asString, h as defaultParseOptions, i as defaultResolveOptions, j as isArray, k as isNull, l as isObject, m as isScalar, p as parse, n as parseAsync, o as parseFile, q as parseFileAsync, r as parseFileWithOptions, s as parseString, t as parseStringWithOptions } from './config-PdhVgeLS.cjs';
1
+ import { C as Config } from './config-Dlq6rF3w.cjs';
2
+ export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ReadonlyHoconValue, b as ResolveOptions, S as ScalarValueType, c as asArray, d as asBoolean, e as asNumber, f as asObject, g as asString, h as defaultParseOptions, i as defaultResolveOptions, j as isArray, k as isNull, l as isObject, m as isScalar, p as parse, n as parseAsync, o as parseFile, q as parseFileAsync, r as parseFileWithOptions, s as parseString, t as parseStringWithOptions } from './config-Dlq6rF3w.cjs';
3
3
 
4
4
  declare class ParseError extends Error {
5
5
  readonly line: number;
@@ -48,7 +48,14 @@ declare class NotResolvedError extends ConfigError {
48
48
  * null → scalar/null
49
49
  * Array<unknown> → array (elements recursively coerced)
50
50
  * Record<string,unknown> → object (values recursively coerced)
51
- * undefined / function / symbol / bigint (> MAX_SAFE_INTEGER) → ConfigError
51
+ * bigint → scalar/number, digits kept verbatim (out of int64 = error)
52
+ * undefined / function / symbol → ConfigError
53
+ *
54
+ * A `bigint` is how a caller (or a format adapter) hands over an integer that a
55
+ * JS `number` cannot hold exactly: its digits are kept verbatim in the value's
56
+ * raw text, so `getString` returns them intact, while `getNumber` / `toObject`
57
+ * apply the JS number model as they do for any other value. Integers outside
58
+ * the int64 range are rejected (spec F0.5).
52
59
  *
53
60
  * originDescription (optional) provides a source name for error messages.
54
61
  * E12 decision 13.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Config } from './config-PdhVgeLS.js';
2
- export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ReadonlyHoconValue, b as ResolveOptions, S as ScalarValueType, c as asArray, d as asBoolean, e as asNumber, f as asObject, g as asString, h as defaultParseOptions, i as defaultResolveOptions, j as isArray, k as isNull, l as isObject, m as isScalar, p as parse, n as parseAsync, o as parseFile, q as parseFileAsync, r as parseFileWithOptions, s as parseString, t as parseStringWithOptions } from './config-PdhVgeLS.js';
1
+ import { C as Config } from './config-Dlq6rF3w.js';
2
+ export { B as ByteUnit, D as DurationUnit, H as HoconValue, P as PackageResolver, a as ParseOptions, R as ReadonlyHoconValue, b as ResolveOptions, S as ScalarValueType, c as asArray, d as asBoolean, e as asNumber, f as asObject, g as asString, h as defaultParseOptions, i as defaultResolveOptions, j as isArray, k as isNull, l as isObject, m as isScalar, p as parse, n as parseAsync, o as parseFile, q as parseFileAsync, r as parseFileWithOptions, s as parseString, t as parseStringWithOptions } from './config-Dlq6rF3w.js';
3
3
 
4
4
  declare class ParseError extends Error {
5
5
  readonly line: number;
@@ -48,7 +48,14 @@ declare class NotResolvedError extends ConfigError {
48
48
  * null → scalar/null
49
49
  * Array<unknown> → array (elements recursively coerced)
50
50
  * Record<string,unknown> → object (values recursively coerced)
51
- * undefined / function / symbol / bigint (> MAX_SAFE_INTEGER) → ConfigError
51
+ * bigint → scalar/number, digits kept verbatim (out of int64 = error)
52
+ * undefined / function / symbol → ConfigError
53
+ *
54
+ * A `bigint` is how a caller (or a format adapter) hands over an integer that a
55
+ * JS `number` cannot hold exactly: its digits are kept verbatim in the value's
56
+ * raw text, so `getString` returns them intact, while `getNumber` / `toObject`
57
+ * apply the JS number model as they do for any other value. Integers outside
58
+ * the int64 range are rejected (spec F0.5).
52
59
  *
53
60
  * originDescription (optional) provides a source name for error messages.
54
61
  * E12 decision 13.