json-with-bigint 3.5.7 → 3.5.8

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.
@@ -0,0 +1,30 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ - "releases/*"
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ node-version:
17
+ - 20
18
+ - 22
19
+ - 24
20
+ - 25
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ with:
24
+ persist-credentials: false
25
+ - name: Setup node
26
+ uses: actions/setup-node@v6
27
+ with:
28
+ node-version: ${{ matrix.node-version }}
29
+ - name: Test
30
+ run: npm run test
package/README.md CHANGED
@@ -46,7 +46,7 @@ JSONParse(JSONStringify(data)).bigNumber === 9007199254740992n // true
46
46
 
47
47
  ✔️ Actively supported
48
48
 
49
- ✔️ Size: 1084 bytes (minified and gzipped)
49
+ ✔️ Size: 1180 bytes (minified and gzipped)
50
50
 
51
51
  ✔️ No dependencies. Even the dev ones
52
52
 
@@ -1,13 +1,15 @@
1
1
  // ------ Performance tests ------
2
2
 
3
+ "use strict";
4
+
3
5
  const { performance } = require("perf_hooks");
4
6
  const { imitateJSONParseWithoutContext } = require("./helpers.cjs");
5
7
  const { JSONParse, JSONStringify } = require("../json-with-bigint.cjs");
6
8
 
7
9
  const fs = require("fs").promises;
8
- const https = require("https");
10
+ const get = require("https").get;
9
11
 
10
- // JSON is located in a separate GitHub repo here (click Raw to see the URL below):
12
+ // JSON is located in a separate GitHub repo here:
11
13
  // https://github.com/Ivan-Korolenko/json-with-bigint.performance.json/blob/main/performance.json
12
14
  const JSON_URL =
13
15
  "https://raw.githubusercontent.com/Ivan-Korolenko/json-with-bigint.performance.json/refs/heads/main/performance.json";
@@ -17,25 +19,23 @@ async function fetchJSON(url, maxRetries = 3, delay = 1000) {
17
19
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
18
20
  try {
19
21
  const response = await new Promise((resolve, reject) => {
20
- https
21
- .get(url, (res) => {
22
- if (res.statusCode >= 500 && res.statusCode < 600) {
23
- reject(new Error(`Server error ${res.statusCode}: Retrying...`));
24
- } else if (res.statusCode !== 200) {
25
- reject(
26
- new Error(
27
- `Request failed with status ${res.statusCode} ${res.statusMessage}`,
28
- ),
29
- );
30
- }
31
-
32
- let data = "";
33
- res.on("data", (chunk) => {
34
- data += chunk;
35
- });
36
- res.on("end", () => resolve(data));
37
- })
38
- .on("error", reject);
22
+ get(url, (res) => {
23
+ if (res.statusCode >= 500 && res.statusCode < 600) {
24
+ reject(new Error(`Server error ${res.statusCode}: Retrying...`));
25
+ } else if (res.statusCode !== 200) {
26
+ reject(
27
+ new Error(
28
+ `Request failed with status ${res.statusCode} ${res.statusMessage}`,
29
+ ),
30
+ );
31
+ }
32
+
33
+ let data = "";
34
+ res.on("data", (chunk) => {
35
+ data += chunk;
36
+ });
37
+ res.on("end", () => resolve(data));
38
+ }).on("error", reject);
39
39
  });
40
40
 
41
41
  return JSON.parse(response);
@@ -125,17 +125,11 @@ const measureExecTime = (fn) => {
125
125
  };
126
126
 
127
127
  const runTests = (data) => {
128
- measureExecTime(() => {
129
- console.log("___________");
130
- console.log("Performance test. One-way");
131
- JSONParse(data);
132
- });
133
-
134
- measureExecTime(() => {
135
- console.log("___________");
136
- console.log("Performance test. Round-trip");
137
- JSONStringify(JSONParse(data));
138
- });
128
+ console.log("___________\nPerformance test. One-way");
129
+ measureExecTime(() => JSONParse(data));
130
+
131
+ console.log("___________\nPerformance test. Round-trip");
132
+ measureExecTime(() => JSONStringify(JSONParse(data)));
139
133
  };
140
134
 
141
135
  async function main() {
@@ -144,9 +138,8 @@ async function main() {
144
138
  console.log("------ V2 performance tests ------");
145
139
  runTests(data);
146
140
 
147
- JSON.parse = imitateJSONParseWithoutContext;
148
-
149
141
  console.log("\n------ V1 (without context.source) performance tests ------");
142
+ JSON.parse = imitateJSONParseWithoutContext;
150
143
  runTests(data);
151
144
  }
152
145
 
@@ -1,5 +1,7 @@
1
1
  // ------ Performance tests ------
2
2
 
3
+ "use strict";
4
+
3
5
  import { performance } from "perf_hooks";
4
6
  import { imitateJSONParseWithoutContext } from "./helpers.cjs";
5
7
  import { JSONParse, JSONStringify } from "../json-with-bigint.js";
@@ -7,7 +9,7 @@ import { JSONParse, JSONStringify } from "../json-with-bigint.js";
7
9
  import { promises as fs } from "fs";
8
10
  import { get } from "https";
9
11
 
10
- // JSON is located in a separate GitHub repo here (click Raw to see the URL below):
12
+ // JSON is located in a separate GitHub repo here:
11
13
  // https://github.com/Ivan-Korolenko/json-with-bigint.performance.json/blob/main/performance.json
12
14
  const JSON_URL =
13
15
  "https://raw.githubusercontent.com/Ivan-Korolenko/json-with-bigint.performance.json/refs/heads/main/performance.json";
@@ -123,17 +125,11 @@ const measureExecTime = (fn) => {
123
125
  };
124
126
 
125
127
  const runTests = (data) => {
126
- measureExecTime(() => {
127
- console.log("___________");
128
- console.log("Performance test. One-way");
129
- JSONParse(data);
130
- });
131
-
132
- measureExecTime(() => {
133
- console.log("___________");
134
- console.log("Performance test. Round-trip");
135
- JSONStringify(JSONParse(data));
136
- });
128
+ console.log("___________\nPerformance test. One-way");
129
+ measureExecTime(() => JSONParse(data));
130
+
131
+ console.log("___________\nPerformance test. Round-trip");
132
+ measureExecTime(() => JSONStringify(JSONParse(data)));
137
133
  };
138
134
 
139
135
  async function main() {
@@ -142,9 +138,8 @@ async function main() {
142
138
  console.log("------ V2 performance tests ------");
143
139
  runTests(data);
144
140
 
145
- JSON.parse = imitateJSONParseWithoutContext;
146
-
147
141
  console.log("\n------ V1 (without context.source) performance tests ------");
142
+ JSON.parse = imitateJSONParseWithoutContext;
148
143
  runTests(data);
149
144
  }
150
145
 
@@ -1,6 +1,8 @@
1
1
  // ------ Unit tests ------
2
2
 
3
- const assert = require("assert");
3
+ "use strict";
4
+
5
+ const { deepStrictEqual } = require("assert");
4
6
  const { imitateJSONParseWithoutContext } = require("./helpers.cjs");
5
7
  const { JSONStringify, JSONParse } = require("../json-with-bigint.cjs");
6
8
 
@@ -107,51 +109,50 @@ const test8Obj = { uid: BigInt("1308537228663099396") };
107
109
  const test8JSON = '{\n "uid": 1308537228663099396\n}';
108
110
 
109
111
  const runTests = () => {
110
- assert.deepStrictEqual(JSONParse(test1JSON), test1Obj);
112
+ deepStrictEqual(JSONParse(test1JSON), test1Obj);
111
113
  console.log("1 test passed");
112
- assert.deepStrictEqual(JSONStringify(JSONParse(test1JSON)), test1JSON);
114
+ deepStrictEqual(JSONStringify(JSONParse(test1JSON)), test1JSON);
113
115
  console.log("1 test round-trip passed");
114
116
 
115
- assert.deepStrictEqual(JSONParse(test2JSON), test2Obj);
117
+ deepStrictEqual(JSONParse(test2JSON), test2Obj);
116
118
  console.log("2 test passed");
117
- assert.deepStrictEqual(JSONStringify(JSONParse(test2JSON)), test2TersedJSON);
119
+ deepStrictEqual(JSONStringify(JSONParse(test2JSON)), test2TersedJSON);
118
120
  console.log("2 test round-trip passed");
119
121
 
120
- assert.deepStrictEqual(JSONParse(test3JSON), test3Obj);
122
+ deepStrictEqual(JSONParse(test3JSON), test3Obj);
121
123
  console.log("3 test passed");
122
- assert.deepStrictEqual(JSONStringify(JSONParse(test3JSON)), test3JSON);
124
+ deepStrictEqual(JSONStringify(JSONParse(test3JSON)), test3JSON);
123
125
  console.log("3 test round-trip passed");
124
126
 
125
- assert.deepStrictEqual(JSONParse(test4JSON), test4Obj);
127
+ deepStrictEqual(JSONParse(test4JSON), test4Obj);
126
128
  console.log("4 test passed");
127
- assert.deepStrictEqual(JSONStringify(JSONParse(test4JSON)), test4JSON);
129
+ deepStrictEqual(JSONStringify(JSONParse(test4JSON)), test4JSON);
128
130
  console.log("4 test round-trip passed");
129
131
 
130
- assert.deepStrictEqual(JSONParse(test5JSON), test5Obj);
132
+ deepStrictEqual(JSONParse(test5JSON), test5Obj);
131
133
  console.log("5 test passed");
132
- assert.deepStrictEqual(JSONStringify(JSONParse(test5JSON)), test5JSON);
134
+ deepStrictEqual(JSONStringify(JSONParse(test5JSON)), test5JSON);
133
135
  console.log("5 test round-trip passed");
134
136
 
135
- assert.deepStrictEqual(JSONParse(test6JSON), test6Obj);
137
+ deepStrictEqual(JSONParse(test6JSON), test6Obj);
136
138
  console.log("6 test passed");
137
- assert.deepStrictEqual(JSONStringify(JSONParse(test6JSON)), test6JSON);
139
+ deepStrictEqual(JSONStringify(JSONParse(test6JSON)), test6JSON);
138
140
  console.log("6 test round-trip passed");
139
141
 
140
- assert.deepStrictEqual(JSONParse(test7JSON), test7Obj);
142
+ deepStrictEqual(JSONParse(test7JSON), test7Obj);
141
143
  console.log("7 test passed");
142
- assert.deepStrictEqual(JSONStringify(JSONParse(test7JSON)), test7JSON);
144
+ deepStrictEqual(JSONStringify(JSONParse(test7JSON)), test7JSON);
143
145
  console.log("7 test round-trip passed");
144
146
 
145
- assert.deepStrictEqual(JSONStringify(test8Obj, null, 2), test8JSON);
147
+ deepStrictEqual(JSONStringify(test8Obj, null, 2), test8JSON);
146
148
  console.log("8 test passed");
147
- assert.deepStrictEqual(JSONParse(JSONStringify(test8Obj, null, 2)), test8Obj);
149
+ deepStrictEqual(JSONParse(JSONStringify(test8Obj, null, 2)), test8Obj);
148
150
  console.log("8 test round-trip passed");
149
151
  };
150
152
 
151
153
  console.log("------ V2 unit tests ------");
152
154
  runTests();
153
155
 
154
- JSON.parse = imitateJSONParseWithoutContext;
155
-
156
156
  console.log("\n------ V1 (without context.source) unit tests ------");
157
+ JSON.parse = imitateJSONParseWithoutContext;
157
158
  runTests();
@@ -1,5 +1,7 @@
1
1
  // ------ Unit tests ------
2
2
 
3
+ "use strict";
4
+
3
5
  import { deepStrictEqual } from "assert";
4
6
  import { imitateJSONParseWithoutContext } from "./helpers.cjs";
5
7
  import { JSONStringify, JSONParse } from "../json-with-bigint.js";
@@ -151,7 +153,6 @@ const runTests = () => {
151
153
  console.log("------ V2 unit tests ------");
152
154
  runTests();
153
155
 
154
- JSON.parse = imitateJSONParseWithoutContext;
155
-
156
156
  console.log("\n------ V1 (without context.source) unit tests ------");
157
+ JSON.parse = imitateJSONParseWithoutContext;
157
158
  runTests();
@@ -1,3 +1,9 @@
1
+ // This file is auto-generated. Do not edit this file directly.
2
+ // Instead edit the json-with-bigint.js and generate this file using:
3
+ // npm run build:cjs
4
+
5
+ "use strict";
6
+
1
7
  const intRegex = /^-?\d+$/;
2
8
  const noiseValue = /^-?\d+n+$/; // Noise - strings that match the custom format before being converted to it
3
9
  const originalStringify = JSON.stringify;
@@ -8,14 +14,26 @@ const bigIntsStringify = /([\[:])?"(-?\d+)n"($|([\\n]|\s)*(\s|[\\n])*[,\}\]])/g;
8
14
  const noiseStringify =
9
15
  /([\[:])?("-?\d+n+)n("$|"([\\n]|\s)*(\s|[\\n])*[,\}\]])/g;
10
16
 
11
- /** @typedef {(key: string, value: any, context?: { source: string }) => any} Reviver */
17
+ /**
18
+ * @typedef {(this: any, key: string | number | undefined, value: any) => any} Replacer
19
+ * @typedef {(key: string | number | undefined, value: any, context?: { source: string }) => any} Reviver
20
+ */
12
21
 
13
22
  /**
14
- * Function to serialize value to a JSON string.
15
- * Converts BigInt values to a custom format (strings with digits and "n" at the end) and then converts them to proper big integers in a JSON string.
16
- * @param {*} value - The value to convert to a JSON string.
17
- * @param {(Function|Array<string>|null)} [replacer] - A function that alters the behavior of the stringification process, or an array of strings to indicate properties to exclude.
18
- * @param {(string|number)} [space] - A string or number to specify indentation or pretty-printing.
23
+ * Converts a JavaScript value to a JSON string.
24
+ *
25
+ * Supports serialization of BigInt values using two strategies:
26
+ * 1. Custom format "123n" "123" (universal fallback)
27
+ * 2. Native JSON.rawJSON() (Node.js 22+, fastest) when available
28
+ *
29
+ * All other values are serialized exactly like native JSON.stringify().
30
+ *
31
+ * @param {*} value The value to convert to a JSON string.
32
+ * @param {Replacer | Array<string | number> | null} [replacer]
33
+ * A function that alters the behavior of the stringification process,
34
+ * or an array of strings/numbers to indicate properties to exclude.
35
+ * @param {string | number} [space]
36
+ * A string or number to specify indentation or pretty-printing.
19
37
  * @returns {string} The JSON string representation.
20
38
  */
21
39
  const JSONStringify = (value, replacer, space) => {
@@ -40,8 +58,7 @@ const JSONStringify = (value, replacer, space) => {
40
58
  const convertedToCustomJSON = originalStringify(
41
59
  value,
42
60
  (key, value) => {
43
- const isNoise =
44
- typeof value === "string" && Boolean(value.match(noiseValue));
61
+ const isNoise = typeof value === "string" && noiseValue.test(value);
45
62
 
46
63
  if (isNoise) return value.toString() + "n"; // Mark noise values with additional "n" to offset the deletion of one "n" during the processing
47
64
 
@@ -64,33 +81,71 @@ const JSONStringify = (value, replacer, space) => {
64
81
  return denoisedJSON;
65
82
  };
66
83
 
84
+ const featureCache = new Map();
85
+
67
86
  /**
68
- * Support for JSON.parse's context.source feature detection.
69
- * @type {boolean}
87
+ * Detects if the current JSON.parse implementation supports the context.source feature.
88
+ *
89
+ * Uses toString() fingerprinting to cache results and automatically detect runtime
90
+ * replacements of JSON.parse (polyfills, mocks, etc.).
91
+ *
92
+ * @returns {boolean} true if context.source is supported, false otherwise.
70
93
  */
71
- const isContextSourceSupported = () =>
72
- JSON.parse("1", (_, __, context) => !!context && context.source === "1");
94
+ const isContextSourceSupported = () => {
95
+ const parseFingerprint = JSON.parse.toString();
96
+
97
+ if (featureCache.has(parseFingerprint)) {
98
+ return featureCache.get(parseFingerprint);
99
+ }
100
+
101
+ try {
102
+ const result = JSON.parse(
103
+ "1",
104
+ (_, __, context) => !!context?.source && context.source === "1",
105
+ );
106
+ featureCache.set(parseFingerprint, result);
107
+
108
+ return result;
109
+ } catch {
110
+ featureCache.set(parseFingerprint, false);
111
+
112
+ return false;
113
+ }
114
+ };
73
115
 
74
116
  /**
75
- * Convert marked big numbers to BigInt
76
- * @type {Reviver}
117
+ * Reviver function that converts custom-format BigInt strings back to BigInt values.
118
+ * Also handles "noise" strings that accidentally match the BigInt format.
119
+ *
120
+ * @param {string | number | undefined} key The object key.
121
+ * @param {*} value The value being parsed.
122
+ * @param {object} [context] Parse context (if supported by JSON.parse).
123
+ * @param {Reviver} [userReviver] User's custom reviver function.
124
+ * @returns {any} The transformed value.
77
125
  */
78
126
  const convertMarkedBigIntsReviver = (key, value, context, userReviver) => {
79
127
  const isCustomFormatBigInt =
80
- typeof value === "string" && value.match(customFormat);
128
+ typeof value === "string" && customFormat.test(value);
81
129
  if (isCustomFormatBigInt) return BigInt(value.slice(0, -1));
82
130
 
83
- const isNoiseValue = typeof value === "string" && value.match(noiseValue);
131
+ const isNoiseValue = typeof value === "string" && noiseValue.test(value);
84
132
  if (isNoiseValue) return value.slice(0, -1);
85
133
 
86
134
  if (typeof userReviver !== "function") return value;
135
+
87
136
  return userReviver(key, value, context);
88
137
  };
89
138
 
90
139
  /**
91
- * Faster (2x) and simpler function to parse JSON.
92
- * Based on JSON.parse's context.source feature, which is not universally available now.
93
- * Does not support the legacy custom format, used in the first version of this library.
140
+ * Fast JSON.parse implementation (~2x faster than classic fallback).
141
+ * Uses JSON.parse's context.source feature to detect integers and convert
142
+ * large numbers directly to BigInt without string manipulation.
143
+ *
144
+ * Does not support legacy custom format from v1 of this library.
145
+ *
146
+ * @param {string} text JSON string to parse.
147
+ * @param {Reviver} [reviver] Transform function to apply to each value.
148
+ * @returns {any} Parsed JavaScript value.
94
149
  */
95
150
  const JSONParseV2 = (text, reviver) => {
96
151
  return JSON.parse(text, (key, value, context) => {
@@ -115,9 +170,21 @@ const stringsOrLargeNumbers =
115
170
  const noiseValueWithQuotes = /^"-?\d+n+"$/; // Noise - strings that match the custom format before being converted to it
116
171
 
117
172
  /**
118
- * Function to parse JSON.
119
- * If JSON has number values greater than Number.MAX_SAFE_INTEGER, we convert those values to a custom format, then parse them to BigInt values.
120
- * Other types of values are not affected and parsed as native JSON.parse() would parse them.
173
+ * Converts a JSON string into a JavaScript value.
174
+ *
175
+ * Supports parsing of large integers using two strategies:
176
+ * 1. Classic fallback: Marks large numbers with "123n" format, then converts to BigInt
177
+ * 2. Fast path (JSONParseV2): Uses context.source feature (~2x faster) when available
178
+ *
179
+ * All other JSON values are parsed exactly like native JSON.parse().
180
+ *
181
+ * @param {string} text A valid JSON string.
182
+ * @param {Reviver} [reviver]
183
+ * A function that transforms the results. This function is called for each member
184
+ * of the object. If a member contains nested objects, the nested objects are
185
+ * transformed before the parent object is.
186
+ * @returns {any} The parsed JavaScript value.
187
+ * @throws {SyntaxError} If text is not valid JSON.
121
188
  */
122
189
  const JSONParse = (text, reviver) => {
123
190
  if (!text) return originalParse(text, reviver);
@@ -129,7 +196,7 @@ const JSONParse = (text, reviver) => {
129
196
  stringsOrLargeNumbers,
130
197
  (text, digits, fractional, exponential) => {
131
198
  const isString = text[0] === '"';
132
- const isNoise = isString && Boolean(text.match(noiseValueWithQuotes));
199
+ const isNoise = isString && noiseValueWithQuotes.test(text);
133
200
 
134
201
  if (isNoise) return text.substring(0, text.length - 1) + 'n"'; // Mark noise values with additional "n" to offset the deletion of one "n" during the processing
135
202
 
@@ -8,14 +8,26 @@ const bigIntsStringify = /([\[:])?"(-?\d+)n"($|([\\n]|\s)*(\s|[\\n])*[,\}\]])/g;
8
8
  const noiseStringify =
9
9
  /([\[:])?("-?\d+n+)n("$|"([\\n]|\s)*(\s|[\\n])*[,\}\]])/g;
10
10
 
11
- /** @typedef {(key: string, value: any, context?: { source: string }) => any} Reviver */
11
+ /**
12
+ * @typedef {(this: any, key: string | number | undefined, value: any) => any} Replacer
13
+ * @typedef {(key: string | number | undefined, value: any, context?: { source: string }) => any} Reviver
14
+ */
12
15
 
13
16
  /**
14
- * Function to serialize value to a JSON string.
15
- * Converts BigInt values to a custom format (strings with digits and "n" at the end) and then converts them to proper big integers in a JSON string.
16
- * @param {*} value - The value to convert to a JSON string.
17
- * @param {(Function|Array<string>|null)} [replacer] - A function that alters the behavior of the stringification process, or an array of strings to indicate properties to exclude.
18
- * @param {(string|number)} [space] - A string or number to specify indentation or pretty-printing.
17
+ * Converts a JavaScript value to a JSON string.
18
+ *
19
+ * Supports serialization of BigInt values using two strategies:
20
+ * 1. Custom format "123n" "123" (universal fallback)
21
+ * 2. Native JSON.rawJSON() (Node.js 22+, fastest) when available
22
+ *
23
+ * All other values are serialized exactly like native JSON.stringify().
24
+ *
25
+ * @param {*} value The value to convert to a JSON string.
26
+ * @param {Replacer | Array<string | number> | null} [replacer]
27
+ * A function that alters the behavior of the stringification process,
28
+ * or an array of strings/numbers to indicate properties to exclude.
29
+ * @param {string | number} [space]
30
+ * A string or number to specify indentation or pretty-printing.
19
31
  * @returns {string} The JSON string representation.
20
32
  */
21
33
  const JSONStringify = (value, replacer, space) => {
@@ -40,8 +52,7 @@ const JSONStringify = (value, replacer, space) => {
40
52
  const convertedToCustomJSON = originalStringify(
41
53
  value,
42
54
  (key, value) => {
43
- const isNoise =
44
- typeof value === "string" && Boolean(value.match(noiseValue));
55
+ const isNoise = typeof value === "string" && noiseValue.test(value);
45
56
 
46
57
  if (isNoise) return value.toString() + "n"; // Mark noise values with additional "n" to offset the deletion of one "n" during the processing
47
58
 
@@ -64,33 +75,71 @@ const JSONStringify = (value, replacer, space) => {
64
75
  return denoisedJSON;
65
76
  };
66
77
 
78
+ const featureCache = new Map();
79
+
67
80
  /**
68
- * Support for JSON.parse's context.source feature detection.
69
- * @type {boolean}
81
+ * Detects if the current JSON.parse implementation supports the context.source feature.
82
+ *
83
+ * Uses toString() fingerprinting to cache results and automatically detect runtime
84
+ * replacements of JSON.parse (polyfills, mocks, etc.).
85
+ *
86
+ * @returns {boolean} true if context.source is supported, false otherwise.
70
87
  */
71
- const isContextSourceSupported = () =>
72
- JSON.parse("1", (_, __, context) => !!context && context.source === "1");
88
+ const isContextSourceSupported = () => {
89
+ const parseFingerprint = JSON.parse.toString();
90
+
91
+ if (featureCache.has(parseFingerprint)) {
92
+ return featureCache.get(parseFingerprint);
93
+ }
94
+
95
+ try {
96
+ const result = JSON.parse(
97
+ "1",
98
+ (_, __, context) => !!context?.source && context.source === "1",
99
+ );
100
+ featureCache.set(parseFingerprint, result);
101
+
102
+ return result;
103
+ } catch {
104
+ featureCache.set(parseFingerprint, false);
105
+
106
+ return false;
107
+ }
108
+ };
73
109
 
74
110
  /**
75
- * Convert marked big numbers to BigInt
76
- * @type {Reviver}
111
+ * Reviver function that converts custom-format BigInt strings back to BigInt values.
112
+ * Also handles "noise" strings that accidentally match the BigInt format.
113
+ *
114
+ * @param {string | number | undefined} key The object key.
115
+ * @param {*} value The value being parsed.
116
+ * @param {object} [context] Parse context (if supported by JSON.parse).
117
+ * @param {Reviver} [userReviver] User's custom reviver function.
118
+ * @returns {any} The transformed value.
77
119
  */
78
120
  const convertMarkedBigIntsReviver = (key, value, context, userReviver) => {
79
121
  const isCustomFormatBigInt =
80
- typeof value === "string" && value.match(customFormat);
122
+ typeof value === "string" && customFormat.test(value);
81
123
  if (isCustomFormatBigInt) return BigInt(value.slice(0, -1));
82
124
 
83
- const isNoiseValue = typeof value === "string" && value.match(noiseValue);
125
+ const isNoiseValue = typeof value === "string" && noiseValue.test(value);
84
126
  if (isNoiseValue) return value.slice(0, -1);
85
127
 
86
128
  if (typeof userReviver !== "function") return value;
129
+
87
130
  return userReviver(key, value, context);
88
131
  };
89
132
 
90
133
  /**
91
- * Faster (2x) and simpler function to parse JSON.
92
- * Based on JSON.parse's context.source feature, which is not universally available now.
93
- * Does not support the legacy custom format, used in the first version of this library.
134
+ * Fast JSON.parse implementation (~2x faster than classic fallback).
135
+ * Uses JSON.parse's context.source feature to detect integers and convert
136
+ * large numbers directly to BigInt without string manipulation.
137
+ *
138
+ * Does not support legacy custom format from v1 of this library.
139
+ *
140
+ * @param {string} text JSON string to parse.
141
+ * @param {Reviver} [reviver] Transform function to apply to each value.
142
+ * @returns {any} Parsed JavaScript value.
94
143
  */
95
144
  const JSONParseV2 = (text, reviver) => {
96
145
  return JSON.parse(text, (key, value, context) => {
@@ -115,9 +164,21 @@ const stringsOrLargeNumbers =
115
164
  const noiseValueWithQuotes = /^"-?\d+n+"$/; // Noise - strings that match the custom format before being converted to it
116
165
 
117
166
  /**
118
- * Function to parse JSON.
119
- * If JSON has number values greater than Number.MAX_SAFE_INTEGER, we convert those values to a custom format, then parse them to BigInt values.
120
- * Other types of values are not affected and parsed as native JSON.parse() would parse them.
167
+ * Converts a JSON string into a JavaScript value.
168
+ *
169
+ * Supports parsing of large integers using two strategies:
170
+ * 1. Classic fallback: Marks large numbers with "123n" format, then converts to BigInt
171
+ * 2. Fast path (JSONParseV2): Uses context.source feature (~2x faster) when available
172
+ *
173
+ * All other JSON values are parsed exactly like native JSON.parse().
174
+ *
175
+ * @param {string} text A valid JSON string.
176
+ * @param {Reviver} [reviver]
177
+ * A function that transforms the results. This function is called for each member
178
+ * of the object. If a member contains nested objects, the nested objects are
179
+ * transformed before the parent object is.
180
+ * @returns {any} The parsed JavaScript value.
181
+ * @throws {SyntaxError} If text is not valid JSON.
121
182
  */
122
183
  const JSONParse = (text, reviver) => {
123
184
  if (!text) return originalParse(text, reviver);
@@ -129,7 +190,7 @@ const JSONParse = (text, reviver) => {
129
190
  stringsOrLargeNumbers,
130
191
  (text, digits, fractional, exponential) => {
131
192
  const isString = text[0] === '"';
132
- const isNoise = isString && Boolean(text.match(noiseValueWithQuotes));
193
+ const isNoise = isString && noiseValueWithQuotes.test(text);
133
194
 
134
195
  if (isNoise) return text.substring(0, text.length - 1) + 'n"'; // Mark noise values with additional "n" to offset the deletion of one "n" during the processing
135
196
 
@@ -1 +1 @@
1
- const intRegex=/^-?\d+$/,noiseValue=/^-?\d+n+$/,originalStringify=JSON.stringify,originalParse=JSON.parse,customFormat=/^-?\d+n$/,bigIntsStringify=/([\[:])?"(-?\d+)n"($|([\\n]|\s)*(\s|[\\n])*[,\}\]])/g,noiseStringify=/([\[:])?("-?\d+n+)n("$|"([\\n]|\s)*(\s|[\\n])*[,\}\]])/g,JSONStringify=(r,n,e)=>{if("rawJSON"in JSON)return originalStringify(r,((r,e)=>"bigint"==typeof e?JSON.rawJSON(e.toString()):"function"==typeof n?n(r,e):(Array.isArray(n)&&n.includes(r),e)),e);if(!r)return originalStringify(r,n,e);const t=originalStringify(r,((r,e)=>"string"==typeof e&&Boolean(e.match(noiseValue))||"bigint"==typeof e?e.toString()+"n":"function"==typeof n?n(r,e):(Array.isArray(n)&&n.includes(r),e)),e);return t.replace(bigIntsStringify,"$1$2$3").replace(noiseStringify,"$1$2$3")},isContextSourceSupported=()=>JSON.parse("1",((r,n,e)=>!!e&&"1"===e.source)),convertMarkedBigIntsReviver=(r,n,e,t)=>{if("string"==typeof n&&n.match(customFormat))return BigInt(n.slice(0,-1));return"string"==typeof n&&n.match(noiseValue)?n.slice(0,-1):"function"!=typeof t?n:t(r,n,e)},JSONParseV2=(r,n)=>JSON.parse(r,((r,e,t)=>{const i="number"==typeof e&&(e>Number.MAX_SAFE_INTEGER||e<Number.MIN_SAFE_INTEGER),s=t&&intRegex.test(t.source);return i&&s?BigInt(t.source):"function"!=typeof n?e:n(r,e,t)})),MAX_INT=Number.MAX_SAFE_INTEGER.toString(),MAX_DIGITS=MAX_INT.length,stringsOrLargeNumbers=/"(?:\\.|[^"])*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/g,noiseValueWithQuotes=/^"-?\d+n+"$/,JSONParse=(r,n)=>{if(!r)return originalParse(r,n);if(JSON.parse("1",((r,n,e)=>!!e&&"1"===e.source)))return JSONParseV2(r,n);const e=r.replace(stringsOrLargeNumbers,((r,n,e,t)=>{const i='"'===r[0];if(i&&Boolean(r.match(noiseValueWithQuotes)))return r.substring(0,r.length-1)+'n"';const s=e||t,o=n&&(n.length<MAX_DIGITS||n.length===MAX_DIGITS&&n<=MAX_INT);return i||s||o?r:'"'+r+'n"'}));return originalParse(e,((r,e,t)=>convertMarkedBigIntsReviver(r,e,t,n)))};export{JSONStringify,JSONParse};
1
+ const intRegex=/^-?\d+$/,noiseValue=/^-?\d+n+$/,originalStringify=JSON.stringify,originalParse=JSON.parse,customFormat=/^-?\d+n$/,bigIntsStringify=/([\[:])?"(-?\d+)n"($|([\\n]|\s)*(\s|[\\n])*[,\}\]])/g,noiseStringify=/([\[:])?("-?\d+n+)n("$|"([\\n]|\s)*(\s|[\\n])*[,\}\]])/g,JSONStringify=(e,r,t)=>{if("rawJSON"in JSON)return originalStringify(e,((e,t)=>"bigint"==typeof t?JSON.rawJSON(t.toString()):"function"==typeof r?r(e,t):(Array.isArray(r)&&r.includes(e),t)),t);if(!e)return originalStringify(e,r,t);const n=originalStringify(e,((e,t)=>"string"==typeof t&&noiseValue.test(t)||"bigint"==typeof t?t.toString()+"n":"function"==typeof r?r(e,t):(Array.isArray(r)&&r.includes(e),t)),t);return n.replace(bigIntsStringify,"$1$2$3").replace(noiseStringify,"$1$2$3")},featureCache=new Map,isContextSourceSupported=()=>{const e=JSON.parse.toString();if(featureCache.has(e))return featureCache.get(e);try{const r=JSON.parse("1",((e,r,t)=>!!t?.source&&"1"===t.source));return featureCache.set(e,r),r}catch{return featureCache.set(e,!1),!1}},convertMarkedBigIntsReviver=(e,r,t,n)=>{if("string"==typeof r&&customFormat.test(r))return BigInt(r.slice(0,-1));return"string"==typeof r&&noiseValue.test(r)?r.slice(0,-1):"function"!=typeof n?r:n(e,r,t)},JSONParseV2=(e,r)=>JSON.parse(e,((e,t,n)=>{const i="number"==typeof t&&(t>Number.MAX_SAFE_INTEGER||t<Number.MIN_SAFE_INTEGER),s=n&&intRegex.test(n.source);return i&&s?BigInt(n.source):"function"!=typeof r?t:r(e,t,n)})),MAX_INT=Number.MAX_SAFE_INTEGER.toString(),MAX_DIGITS=MAX_INT.length,stringsOrLargeNumbers=/"(?:\\.|[^"])*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/g,noiseValueWithQuotes=/^"-?\d+n+"$/,JSONParse=(e,r)=>{if(!e)return originalParse(e,r);if(isContextSourceSupported())return JSONParseV2(e,r);const t=e.replace(stringsOrLargeNumbers,((e,r,t,n)=>{const i='"'===e[0];if(i&&noiseValueWithQuotes.test(e))return e.substring(0,e.length-1)+'n"';const s=t||n,o=r&&(r.length<MAX_DIGITS||r.length===MAX_DIGITS&&r<=MAX_INT);return i||s||o?e:'"'+e+'n"'}));return originalParse(t,((e,t,n)=>convertMarkedBigIntsReviver(e,t,n,r)))};export{JSONStringify,JSONParse};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-with-bigint",
3
- "version": "3.5.7",
3
+ "version": "3.5.8",
4
4
  "description": "JS library that allows you to easily serialize and deserialize data with BigInt values",
5
5
  "type": "module",
6
6
  "types": "./json-with-bigint.d.ts",
@@ -9,6 +9,7 @@
9
9
  "require": "./json-with-bigint.cjs"
10
10
  },
11
11
  "scripts": {
12
+ "build:cjs": "node ./scripts/build-cjs.js",
12
13
  "test": "npm run unit:cjs && npm run unit:esm",
13
14
  "performance:cjs": "node __tests__/performance.cjs",
14
15
  "unit:cjs": "node __tests__/unit.cjs",
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+
3
+ const esmSource = fs.readFileSync("./json-with-bigint.js", "utf-8");
4
+ const prefix = `// This file is auto-generated. Do not edit this file directly.
5
+ // Instead edit the json-with-bigint.js and regenerate this file using:
6
+ // npm run build:cjs
7
+
8
+ "use strict";
9
+
10
+ `;
11
+
12
+ const exportedNames = new Set();
13
+
14
+ // Find export { name1, name2 }
15
+ const namedExportsMatch = esmSource.match(/export\s*\{([^}]+?)\}/s);
16
+ if (namedExportsMatch) {
17
+ namedExportsMatch[1]
18
+ .split(",")
19
+ .map((name) => name.trim())
20
+ .filter(Boolean)
21
+ .forEach((name) => exportedNames.add(name));
22
+ }
23
+
24
+ // Find export foo;
25
+ const singleExports = esmSource.match(/export\s+(\w+)\s*;/g);
26
+ if (singleExports) {
27
+ singleExports.forEach((match) => {
28
+ const name = match.match(/export\s+(\w+)/)?.[1];
29
+ if (name) exportedNames.add(name);
30
+ });
31
+ }
32
+
33
+ // Find export default foo;
34
+ const defaultExport = esmSource.match(/export\s+default\s+(\w+)/);
35
+ if (defaultExport) {
36
+ exportedNames.add(defaultExport[1]);
37
+ }
38
+
39
+ let cjsSource = esmSource
40
+ // Remove export { ... }
41
+ .replace(/^\s*export\s*\{[^}]+\}\s*;?\s*$/gm, "")
42
+ // Remove export name;
43
+ .replace(/^\s*export\s+\w+\s*;?\s*$/gm, "")
44
+ // Remove export default name;
45
+ .replace(/^\s*export\s+default\s+\w+\s*;?\s*$/gm, "")
46
+ // Remove export default (...)
47
+ .replace(/^\s*export\s+default\s+.+?$/gm, "")
48
+ .trim();
49
+
50
+ const exportsList = Array.from(exportedNames).join(", ");
51
+
52
+ const moduleExports =
53
+ exportedNames.size > 0 ? `module.exports = { ${exportsList} };` : "";
54
+
55
+ cjsSource = prefix + cjsSource + `\n\n${moduleExports}\n`;
56
+
57
+ fs.writeFileSync("./json-with-bigint.cjs", cjsSource, "utf8");
58
+ console.log("✅ CJS generated: module.exports = { " + exportsList + " }");