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.
- package/.github/workflows/ci.yml +30 -0
- package/README.md +1 -1
- package/__tests__/performance.cjs +27 -34
- package/__tests__/performance.mjs +9 -14
- package/__tests__/unit.cjs +20 -19
- package/__tests__/unit.mjs +3 -2
- package/json-with-bigint.cjs +90 -23
- package/json-with-bigint.js +84 -23
- package/json-with-bigint.min.js +1 -1
- package/package.json +2 -1
- package/scripts/build-cjs.js +58 -0
|
@@ -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
|
@@ -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
|
|
10
|
+
const get = require("https").get;
|
|
9
11
|
|
|
10
|
-
// JSON is located in a separate GitHub repo here
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
package/__tests__/unit.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// ------ Unit tests ------
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
112
|
+
deepStrictEqual(JSONParse(test1JSON), test1Obj);
|
|
111
113
|
console.log("1 test passed");
|
|
112
|
-
|
|
114
|
+
deepStrictEqual(JSONStringify(JSONParse(test1JSON)), test1JSON);
|
|
113
115
|
console.log("1 test round-trip passed");
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
deepStrictEqual(JSONParse(test2JSON), test2Obj);
|
|
116
118
|
console.log("2 test passed");
|
|
117
|
-
|
|
119
|
+
deepStrictEqual(JSONStringify(JSONParse(test2JSON)), test2TersedJSON);
|
|
118
120
|
console.log("2 test round-trip passed");
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
deepStrictEqual(JSONParse(test3JSON), test3Obj);
|
|
121
123
|
console.log("3 test passed");
|
|
122
|
-
|
|
124
|
+
deepStrictEqual(JSONStringify(JSONParse(test3JSON)), test3JSON);
|
|
123
125
|
console.log("3 test round-trip passed");
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
deepStrictEqual(JSONParse(test4JSON), test4Obj);
|
|
126
128
|
console.log("4 test passed");
|
|
127
|
-
|
|
129
|
+
deepStrictEqual(JSONStringify(JSONParse(test4JSON)), test4JSON);
|
|
128
130
|
console.log("4 test round-trip passed");
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
deepStrictEqual(JSONParse(test5JSON), test5Obj);
|
|
131
133
|
console.log("5 test passed");
|
|
132
|
-
|
|
134
|
+
deepStrictEqual(JSONStringify(JSONParse(test5JSON)), test5JSON);
|
|
133
135
|
console.log("5 test round-trip passed");
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
deepStrictEqual(JSONParse(test6JSON), test6Obj);
|
|
136
138
|
console.log("6 test passed");
|
|
137
|
-
|
|
139
|
+
deepStrictEqual(JSONStringify(JSONParse(test6JSON)), test6JSON);
|
|
138
140
|
console.log("6 test round-trip passed");
|
|
139
141
|
|
|
140
|
-
|
|
142
|
+
deepStrictEqual(JSONParse(test7JSON), test7Obj);
|
|
141
143
|
console.log("7 test passed");
|
|
142
|
-
|
|
144
|
+
deepStrictEqual(JSONStringify(JSONParse(test7JSON)), test7JSON);
|
|
143
145
|
console.log("7 test round-trip passed");
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
deepStrictEqual(JSONStringify(test8Obj, null, 2), test8JSON);
|
|
146
148
|
console.log("8 test passed");
|
|
147
|
-
|
|
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();
|
package/__tests__/unit.mjs
CHANGED
|
@@ -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();
|
package/json-with-bigint.cjs
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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" &&
|
|
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" &&
|
|
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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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 &&
|
|
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
|
|
package/json-with-bigint.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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" &&
|
|
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" &&
|
|
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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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 &&
|
|
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
|
|
package/json-with-bigint.min.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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 + " }");
|