@o3co/ts.hocon 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/zod.js CHANGED
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/coerce.ts
2
9
  var DECIMAL_NUMBER_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
3
10
  function isHoconWs(cp) {
@@ -643,6 +650,7 @@ function isFileNotFoundError(e) {
643
650
  // src/internal/resolver/include-loader.ts
644
651
  import { createRequire } from "module";
645
652
  import * as nodePath from "path";
653
+ import { pathToFileURL } from "url";
646
654
 
647
655
  // src/internal/lexer/lexer.ts
648
656
  function isHoconWhitespace(ch) {
@@ -1044,12 +1052,12 @@ function isDecimalDigit(ch) {
1044
1052
  }
1045
1053
  function isUnquotedStart(ch) {
1046
1054
  if (ch === "" || isHoconWhitespace(ch)) return false;
1047
- if ('{}[],:=+#"$?!@*&^\\'.includes(ch)) return false;
1055
+ if ('{}[],:=+#"$?!@*&^`\\'.includes(ch)) return false;
1048
1056
  return true;
1049
1057
  }
1050
1058
  function isUnquotedContinue(ch, nextFn) {
1051
1059
  if (ch === "" || isHoconWhitespace(ch)) return false;
1052
- if ('{}[],:=#"$?!@*&^\\'.includes(ch)) return false;
1060
+ if ('{}[],:=#"$?!@*&^`\\'.includes(ch)) return false;
1053
1061
  if (ch === "+" && nextFn() === "=") return false;
1054
1062
  if (ch === "/" && nextFn() === "/") return false;
1055
1063
  return true;
@@ -1198,6 +1206,17 @@ var Parser = class {
1198
1206
  this.advance();
1199
1207
  const raw = t.value.startsWith(".") && segments.length > 0 && !t.precedingSpace ? t.value.slice(1) : t.value;
1200
1208
  const parts = raw.split(".");
1209
+ const leadingDotIsSeparator = spaceConcat || postDotPrefix !== "";
1210
+ for (let i = 0; i < parts.length; i++) {
1211
+ if (parts[i] !== "") continue;
1212
+ if (i === parts.length - 1) continue;
1213
+ if (i === 0 && leadingDotIsSeparator) continue;
1214
+ throw new ParseError(
1215
+ `path has ${i === 0 ? "a leading period '.'" : "two adjacent periods '.'"} \u2014 empty key segment not allowed, use quoted "" (HOCON.md path rules)`,
1216
+ t.line,
1217
+ t.col
1218
+ );
1219
+ }
1201
1220
  const filtered = parts.filter((s) => s.length > 0);
1202
1221
  if (spaceConcat) {
1203
1222
  segments[segments.length - 1] = `${segments[segments.length - 1]}${t.precedingWhitespace}`;
@@ -1474,45 +1493,135 @@ function parseTokens(tokens) {
1474
1493
  return new Parser(tokens).parse();
1475
1494
  }
1476
1495
 
1496
+ // src/internal/strip-bom.ts
1497
+ function stripBom(input) {
1498
+ return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1499
+ }
1500
+
1477
1501
  // src/internal/properties/properties.ts
1478
1502
  function parseProperties(input) {
1479
- const root = /* @__PURE__ */ Object.create(null);
1480
1503
  const pairs = [];
1481
- for (const line of input.split("\n")) {
1482
- const trimmed = line.trim();
1483
- if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
1484
- const sepIdx = findSeparator(trimmed);
1485
- if (sepIdx === -1) continue;
1486
- const key = trimmed.slice(0, sepIdx).trim();
1487
- const value = trimmed.slice(sepIdx + 1).trim();
1504
+ for (const { text, line } of logicalLines(stripBom(input))) {
1505
+ const [rawKey, rawValue] = splitKeyValue(text);
1506
+ const key = unescapeProps(rawKey, line);
1488
1507
  if (key === "") continue;
1489
- pairs.push([key, value]);
1508
+ pairs.push([key.split("."), unescapeProps(rawValue, line)]);
1490
1509
  }
1491
- pairs.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1492
- for (const [key, value] of pairs) {
1493
- setNested(root, key.split("."), value);
1510
+ return nestPairs(pairs);
1511
+ }
1512
+ function nestPairs(pairs) {
1513
+ const root = /* @__PURE__ */ Object.create(null);
1514
+ const sorted = [...pairs].sort(([a], [b]) => compareSegments(a, b));
1515
+ for (const [segments, value] of sorted) {
1516
+ setNested(root, segments, value);
1494
1517
  }
1495
1518
  return root;
1496
1519
  }
1497
- function findSeparator(line) {
1498
- for (let i = 0; i < line.length; i++) {
1499
- if (line[i] === "=" || line[i] === ":") return i;
1520
+ function compareSegments(a, b) {
1521
+ const n = Math.min(a.length, b.length);
1522
+ for (let i = 0; i < n; i++) {
1523
+ const x = a[i];
1524
+ const y = b[i];
1525
+ if (x !== y) return x < y ? -1 : 1;
1526
+ }
1527
+ return a.length - b.length;
1528
+ }
1529
+ function logicalLines(input) {
1530
+ const natural = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
1531
+ const out = [];
1532
+ for (let i = 0; i < natural.length; i++) {
1533
+ let text = trimLeadingSpace(natural[i] ?? "");
1534
+ if (text === "" || text.startsWith("#") || text.startsWith("!")) continue;
1535
+ const start = i + 1;
1536
+ while (endsWithContinuation(text)) {
1537
+ text = text.slice(0, -1);
1538
+ if (i + 1 >= natural.length) break;
1539
+ i++;
1540
+ text += trimLeadingSpace(natural[i] ?? "");
1541
+ }
1542
+ out.push({ text, line: start });
1543
+ }
1544
+ return out;
1545
+ }
1546
+ function trimLeadingSpace(s) {
1547
+ return s.replace(/^[ \t\f]+/, "");
1548
+ }
1549
+ function endsWithContinuation(line) {
1550
+ let n = 0;
1551
+ for (let i = line.length - 1; i >= 0 && line[i] === "\\"; i--) n++;
1552
+ return n % 2 === 1;
1553
+ }
1554
+ function splitKeyValue(line) {
1555
+ let key = "";
1556
+ let i = 0;
1557
+ for (; i < line.length; i++) {
1558
+ const c = line[i];
1559
+ if (c === "\\" && i + 1 < line.length) {
1560
+ key += c + line[i + 1];
1561
+ i++;
1562
+ continue;
1563
+ }
1564
+ if (c === "=" || c === ":" || isPropsSpace(c)) break;
1565
+ key += c;
1566
+ }
1567
+ while (i < line.length && isPropsSpace(line[i])) i++;
1568
+ if (i < line.length && (line[i] === "=" || line[i] === ":")) {
1569
+ i++;
1570
+ while (i < line.length && isPropsSpace(line[i])) i++;
1571
+ }
1572
+ return [key, line.slice(i)];
1573
+ }
1574
+ function isPropsSpace(c) {
1575
+ return c === " " || c === " " || c === "\f";
1576
+ }
1577
+ function unescapeProps(s, line) {
1578
+ let out = "";
1579
+ for (let i = 0; i < s.length; i++) {
1580
+ if (s[i] !== "\\") {
1581
+ out += s[i];
1582
+ continue;
1583
+ }
1584
+ i++;
1585
+ if (i >= s.length) break;
1586
+ const e = s[i];
1587
+ switch (e) {
1588
+ case "t":
1589
+ out += " ";
1590
+ break;
1591
+ case "n":
1592
+ out += "\n";
1593
+ break;
1594
+ case "r":
1595
+ out += "\r";
1596
+ break;
1597
+ case "f":
1598
+ out += "\f";
1599
+ break;
1600
+ case "u": {
1601
+ const hex = s.slice(i + 1, i + 5);
1602
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
1603
+ throw new ParseError(`malformed \\u escape in .properties: \\u${hex}`, line, i + 1);
1604
+ }
1605
+ out += String.fromCharCode(parseInt(hex, 16));
1606
+ i += 4;
1607
+ break;
1608
+ }
1609
+ default:
1610
+ out += e;
1611
+ }
1500
1612
  }
1501
- return -1;
1613
+ return out;
1502
1614
  }
1503
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1504
1615
  function setNested(obj, segments, value) {
1505
1616
  let current = obj;
1506
1617
  for (let i = 0; i < segments.length - 1; i++) {
1507
1618
  const seg = segments[i];
1508
- if (seg === void 0 || DANGEROUS_KEYS.has(seg)) return;
1509
1619
  if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
1510
1620
  current[seg] = /* @__PURE__ */ Object.create(null);
1511
1621
  }
1512
1622
  current = current[seg];
1513
1623
  }
1514
1624
  const last = segments[segments.length - 1];
1515
- if (last === void 0 || DANGEROUS_KEYS.has(last)) return;
1516
1625
  if (typeof current[last] === "object" && current[last] !== null) return;
1517
1626
  current[last] = value;
1518
1627
  }
@@ -1533,7 +1642,22 @@ function recordToHoconValue(obj) {
1533
1642
  }
1534
1643
 
1535
1644
  // src/internal/resolver/include-loader.ts
1536
- var _require = createRequire(import.meta.url);
1645
+ var _require;
1646
+ function getRequire() {
1647
+ if (_require === void 0) {
1648
+ _require = pickRequire(
1649
+ typeof __require === "function" ? __require : void 0,
1650
+ import.meta.url
1651
+ );
1652
+ }
1653
+ return _require;
1654
+ }
1655
+ function pickRequire(candidate, metaUrl) {
1656
+ if (typeof candidate?.resolve === "function") {
1657
+ return candidate;
1658
+ }
1659
+ return createRequire(metaUrl ?? pathToFileURL(nodePath.join(process.cwd(), "noop.js")));
1660
+ }
1537
1661
  function isPnpPath(p) {
1538
1662
  return p.includes(".yarn/cache") || p.includes(".pnp.cjs") || p.includes(".pnp.loader");
1539
1663
  }
@@ -1589,7 +1713,7 @@ function defaultPackageResolver(identifier, file, includingFile, baseDir, resolv
1589
1713
  const from = resolveFrom ? Array.isArray(resolveFrom) ? resolveFrom : [resolveFrom] : baseDir ? [baseDir] : includingFile ? [nodePath.dirname(includingFile)] : [process.cwd()];
1590
1714
  let resolved;
1591
1715
  try {
1592
- resolved = _require.resolve(`${identifier}/${file}`, { paths: from });
1716
+ resolved = getRequire().resolve(`${identifier}/${file}`, { paths: from });
1593
1717
  } catch {
1594
1718
  throw new PackageLookupError(
1595
1719
  `include package("${identifier}", "${file}"): module not found (starting from: ${from.join(", ")})`,
@@ -2639,10 +2763,10 @@ var Config = class _Config {
2639
2763
  }
2640
2764
  if (v.kind === "object") {
2641
2765
  const converted = numericObjectToArray(v);
2642
- if (converted !== null) return converted.map(hoconToJs);
2766
+ if (converted !== null) return converted.map((item) => hoconToJs(item));
2643
2767
  }
2644
2768
  if (v.kind !== "array") throw new ConfigError(`expected array at ${path2}`, path2);
2645
- return v.items.map(hoconToJs);
2769
+ return v.items.map((item) => hoconToJs(item));
2646
2770
  }
2647
2771
  has(path2) {
2648
2772
  return this.lookupNode(path2) !== void 0;
@@ -2770,8 +2894,52 @@ var Config = class _Config {
2770
2894
  originDescription: this._originDescription
2771
2895
  });
2772
2896
  }
2773
- toObject() {
2774
- return hoconToJs(this.root);
2897
+ /**
2898
+ * The whole config as plain JS data: objects, arrays, strings, numbers,
2899
+ * booleans and `null`.
2900
+ *
2901
+ * Keys are reproduced exactly as the config carries them, `__proto__`,
2902
+ * `constructor` and `prototype` included: each is defined as an **own data
2903
+ * property**, so the returned object's prototype is always
2904
+ * `Object.prototype`, and this call writes nothing to the global one.
2905
+ *
2906
+ * ### Passing the result to other code
2907
+ *
2908
+ * A config file — especially a `.properties` file or an env namespace you
2909
+ * merged in — can therefore hand you an own `__proto__` key, and what happens
2910
+ * next depends entirely on how the consumer copies it:
2911
+ *
2912
+ * ```ts
2913
+ * const data = cfg.toObject()
2914
+ * { ...data } // safe — own properties are copied as data
2915
+ * structuredClone(data) // safe
2916
+ * Object.assign({}, data) // NOT safe — [[Set]] on the {} target hits
2917
+ * // Object.prototype's __proto__ setter: the key
2918
+ * // vanishes and config data becomes the result's
2919
+ * // prototype
2920
+ * deepMerge({}, data) // NOT safe — a naive recursive merge reads
2921
+ * // target['__proto__'], gets Object.prototype,
2922
+ * // and writes to it: global pollution
2923
+ * for (const k in data) … // prefer Object.entries / Object.keys
2924
+ * ```
2925
+ *
2926
+ * Those two hazards come from the **destination** object's prototype, so no
2927
+ * option here can disarm them — copy with spread or `structuredClone` instead.
2928
+ *
2929
+ * What `{ nullPrototype: true }` does give you is a result that inherits
2930
+ * nothing: every object comes back with a `null` prototype, so reading
2931
+ * `x.__proto__`, `x.constructor` or `x.toString` yields config data or
2932
+ * `undefined` rather than something from `Object.prototype`, and code that
2933
+ * merges *into* this object cannot climb out of it. Keys are all still
2934
+ * present. The trade-off is the usual one for prototype-less objects: not
2935
+ * `instanceof Object`, and no `hasOwnProperty` of their own.
2936
+ *
2937
+ * Numbers follow the JS number model, so an integer wider than 2^53 rounds
2938
+ * here even when the config holds it exactly — read those with
2939
+ * {@link Config.getString} instead.
2940
+ */
2941
+ toObject(opts) {
2942
+ return hoconToJs(this.root, opts?.nullPrototype === true);
2775
2943
  }
2776
2944
  /**
2777
2945
  * @internal Test-only: renders this resolved Config's value tree as canonical
@@ -2883,16 +3051,25 @@ function scalarToJs(raw, valueType) {
2883
3051
  return raw;
2884
3052
  }
2885
3053
  }
2886
- function hoconToJs(v) {
3054
+ function hoconToJs(v, nullPrototype = false) {
2887
3055
  switch (v.kind) {
2888
3056
  case "scalar":
2889
3057
  return scalarToJs(v.raw, v.valueType);
2890
3058
  case "array":
2891
- return v.items.map(hoconToJs);
3059
+ return v.items.map((item) => hoconToJs(item, nullPrototype));
2892
3060
  case "object": {
2893
- const obj = /* @__PURE__ */ Object.create(null);
2894
- for (const [k, val] of v.fields) obj[k] = hoconToJs(val);
2895
- return Object.assign({}, obj);
3061
+ const entries = Array.from(v.fields, ([k, val]) => [k, hoconToJs(val, nullPrototype)]);
3062
+ if (nullPrototype) {
3063
+ const obj = /* @__PURE__ */ Object.create(null);
3064
+ for (const [k, val] of entries) Object.defineProperty(obj, k, {
3065
+ value: val,
3066
+ writable: true,
3067
+ enumerable: true,
3068
+ configurable: true
3069
+ });
3070
+ return obj;
3071
+ }
3072
+ return Object.fromEntries(entries);
2896
3073
  }
2897
3074
  }
2898
3075
  }
@@ -3083,7 +3260,7 @@ function coerceValue(value, schema) {
3083
3260
  const obj = value;
3084
3261
  const result = { ...obj };
3085
3262
  for (const key of Object.keys(shape)) {
3086
- if (key in obj) {
3263
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
3087
3264
  result[key] = coerceValue(obj[key], shape[key]);
3088
3265
  }
3089
3266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/ts.hocon",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Full Lightbend HOCON spec-compliant parser and config library for TypeScript",
5
5
  "keywords": [
6
6
  "hocon",
@@ -13,14 +13,74 @@
13
13
  "type": "module",
14
14
  "exports": {
15
15
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js",
18
- "require": "./dist/index.cjs"
16
+ "import": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "require": {
21
+ "types": "./dist/index.d.cts",
22
+ "default": "./dist/index.cjs"
23
+ }
19
24
  },
20
25
  "./zod": {
21
- "types": "./dist/zod.d.ts",
22
- "import": "./dist/zod.js",
23
- "require": "./dist/zod.cjs"
26
+ "import": {
27
+ "types": "./dist/zod.d.ts",
28
+ "default": "./dist/zod.js"
29
+ },
30
+ "require": {
31
+ "types": "./dist/zod.d.cts",
32
+ "default": "./dist/zod.cjs"
33
+ }
34
+ },
35
+ "./adapters/properties": {
36
+ "import": {
37
+ "types": "./dist/adapters/properties.d.ts",
38
+ "default": "./dist/adapters/properties.js"
39
+ },
40
+ "require": {
41
+ "types": "./dist/adapters/properties.d.cts",
42
+ "default": "./dist/adapters/properties.cjs"
43
+ }
44
+ },
45
+ "./adapters/env": {
46
+ "import": {
47
+ "types": "./dist/adapters/env.d.ts",
48
+ "default": "./dist/adapters/env.js"
49
+ },
50
+ "require": {
51
+ "types": "./dist/adapters/env.d.cts",
52
+ "default": "./dist/adapters/env.cjs"
53
+ }
54
+ },
55
+ "./adapters/jsonc": {
56
+ "import": {
57
+ "types": "./dist/adapters/jsonc.d.ts",
58
+ "default": "./dist/adapters/jsonc.js"
59
+ },
60
+ "require": {
61
+ "types": "./dist/adapters/jsonc.d.cts",
62
+ "default": "./dist/adapters/jsonc.cjs"
63
+ }
64
+ },
65
+ "./adapters/toml": {
66
+ "import": {
67
+ "types": "./dist/adapters/toml.d.ts",
68
+ "default": "./dist/adapters/toml.js"
69
+ },
70
+ "require": {
71
+ "types": "./dist/adapters/toml.d.cts",
72
+ "default": "./dist/adapters/toml.cjs"
73
+ }
74
+ },
75
+ "./adapters/yaml": {
76
+ "import": {
77
+ "types": "./dist/adapters/yaml.d.ts",
78
+ "default": "./dist/adapters/yaml.js"
79
+ },
80
+ "require": {
81
+ "types": "./dist/adapters/yaml.d.cts",
82
+ "default": "./dist/adapters/yaml.cjs"
83
+ }
24
84
  }
25
85
  },
26
86
  "main": "./dist/index.cjs",
@@ -29,21 +89,20 @@
29
89
  "files": [
30
90
  "dist"
31
91
  ],
32
- "scripts": {
33
- "build": "tsup",
34
- "test": "vitest run",
35
- "test:watch": "vitest",
36
- "typecheck": "tsc --noEmit",
37
- "coverage": "vitest run --coverage",
38
- "bench": "vitest bench",
39
- "docs": "typedoc"
40
- },
41
92
  "peerDependencies": {
42
- "zod": ">=4.0.0"
93
+ "zod": ">=4.0.0",
94
+ "smol-toml": ">=1.0.0",
95
+ "yaml": ">=2.0.0"
43
96
  },
44
97
  "peerDependenciesMeta": {
45
98
  "zod": {
46
99
  "optional": true
100
+ },
101
+ "smol-toml": {
102
+ "optional": true
103
+ },
104
+ "yaml": {
105
+ "optional": true
47
106
  }
48
107
  },
49
108
  "devDependencies": {
@@ -51,10 +110,12 @@
51
110
  "@types/node": "^25.9.5",
52
111
  "@vitest/coverage-v8": "4.1.10",
53
112
  "js-yaml": "^4.3.0",
113
+ "smol-toml": "^1.7.0",
54
114
  "tsup": "^8.5.1",
55
115
  "typedoc": "^0.28.20",
56
116
  "typescript": "^5.9.3",
57
117
  "vitest": "4.1.10",
118
+ "yaml": "^2.9.0",
58
119
  "zod": "^4.4.3"
59
120
  },
60
121
  "repository": {
@@ -67,5 +128,14 @@
67
128
  },
68
129
  "homepage": "https://o3co.github.io/ts.hocon/",
69
130
  "bugs": "https://github.com/o3co/ts.hocon/issues",
70
- "packageManager": "pnpm@10.30.2"
71
- }
131
+ "scripts": {
132
+ "build": "tsup",
133
+ "smoke": "tsup && node tools/smoke-entrypoints.mjs",
134
+ "test": "vitest run",
135
+ "test:watch": "vitest",
136
+ "typecheck": "tsc --noEmit",
137
+ "coverage": "vitest run --coverage",
138
+ "bench": "vitest bench",
139
+ "docs": "typedoc"
140
+ }
141
+ }