diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ce34731 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,45 @@ +name: CI +on: + push: + branches: + - main + - node + - node-ts + pull_request: + branches: + - main +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '16.x' + - name: Install dependencies + run: npm install + - name: Lint files + run: npm run lint + test: + name: Test + strategy: + matrix: + os: [ubuntu-latest] + node: [17.x, 16.x, 14.x, 12.x, "12.22.0"] + include: + - os: windows-latest + node: "16.x" + - os: macOS-latest + node: "16.x" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies + run: npm install + - name: Run tests + run: npm run test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8a9aa7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/node_modules +dist +coverage/ +npm-debug.log +.DS_Store +.idea +*.iml +.cache +.sublimelinterrc +.eslintcache diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..6aeb8f2 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged +npx lint-staged diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f2dd66f --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# set to true if you are developing an app. +package-lock=false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..940260d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["dbaeumer.vscode-eslint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..51328b0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.experimental.useFlatConfig": true, +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad59297 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 唯然 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..1488c5e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,15 @@ +import jsPlugin from '@eslint/js' +import esmConfig from 'eslint-plugin-n/configs/recommended-module.js' +import cjsConfig from 'eslint-plugin-n/configs/recommended-script.js' + +export default [ + jsPlugin.configs.recommended, + { + files: ['**/*.{js,mjs}'], + ...esmConfig, + }, + { + files: ['**/*.cjs'], + ...cjsConfig, + }, +] diff --git a/lib/jsonb.cjs b/lib/jsonb.cjs new file mode 100644 index 0000000..d34df7c --- /dev/null +++ b/lib/jsonb.cjs @@ -0,0 +1,71 @@ +/** + * @fileoverview JSONB parser and stringifier + * @author 唯然 + */ +'use strict' +const { parseJson } = require('@ton.js/json-parser') +const isBNSupported = typeof BigInt !== 'undefined' +const BN = isBNSupported ? BigInt : (x) => x + +function parse(s) { + function reviver(k, v, ctx) { + let isSafe = true + + const isInt = typeof v === 'number' && /^(-)?\d+$/.test(ctx.source) + // 如果是整数的话,也要检查是否超过了安全整数范围 + if (isInt) { + isSafe = Number.isSafeInteger(v) + } + + return isSafe ? v : BN(ctx.source) + } + + return parseJson(s, reviver) +} + +function stringify(data) { + if (data === undefined) return undefined + if (data === null) return 'null' + if (Number.isNaN(data)) return 'null' + if (data === Infinity) return 'null' + if (data.constructor === String) return '"' + data.replace(/"/g, '\\"') + '"' + if (data.constructor === Number) return String(data) + if (isBNSupported && data.constructor === BigInt) return String(data) // 避免bigint精度丢失 + if (data.constructor === Boolean) return data ? 'true' : 'false' + if (data.constructor === Array) + return ( + '[' + + data + .reduce((acc, v) => { + if ( + v === undefined || + Number.isNaN(v) || + v === Infinity || + v === -Infinity + ) + return [...acc, 'null'] + else return [...acc, stringify(v)] + }, []) + .join(',') + + ']' + ) + + if (data.constructor === Object) + return ( + '{' + + Object.keys(data) + .reduce((acc, k) => { + if (data[k] === undefined) return acc + else return [...acc, stringify(k) + ':' + stringify(data[k])] + }, []) + .join(',') + + '}' + ) + + return '' +} + +module.exports = { + parse, + stringify, +} diff --git a/lib/jsonb.d.ts b/lib/jsonb.d.ts new file mode 100644 index 0000000..97d52a9 --- /dev/null +++ b/lib/jsonb.d.ts @@ -0,0 +1,6 @@ +export function parse(s: string, bn?: any[]): T +export function stringify(obj: any): string +export namespace JSONB { + export { parse } + export { stringify } +} diff --git a/lib/jsonb.js b/lib/jsonb.js new file mode 100644 index 0000000..647f0cc --- /dev/null +++ b/lib/jsonb.js @@ -0,0 +1,67 @@ +/** + * @fileoverview JSONB parser and stringifier + * @author 唯然 + */ +import { parseJson } from '@ton.js/json-parser' +const isBNSupported = typeof BigInt !== 'undefined' +const BN = isBNSupported ? BigInt : (x) => x + +export function parse(s) { + function reviver(k, v, ctx) { + let isSafe = true + + const isInt = typeof v === 'number' && /^(-)?\d+$/.test(ctx.source) + // 如果是整数的话,也要检查是否超过了安全整数范围 + if (isInt) { + isSafe = Number.isSafeInteger(v) + } + + return isSafe ? v : BN(ctx.source) + } + + return parseJson(s, reviver) +} + +function stringify(data) { + if (data === undefined) return undefined + if (data === null) return 'null' + if (Number.isNaN(data)) return 'null' + if (data === Infinity || data === -Infinity) return 'null' + if (data.constructor === String) return '"' + data.replace(/"/g, '\\"') + '"' + if (data.constructor === Number) return String(data) + if (isBNSupported && data.constructor === BigInt) return String(data) // 避免bigint精度丢失 + if (data.constructor === Boolean) return data ? 'true' : 'false' + if (data.constructor === Array) + return ( + '[' + + data + .reduce((acc, v) => { + if ( + v === undefined || + Number.isNaN(v) || + v === Infinity || + v === -Infinity + ) + return [...acc, 'null'] + else return [...acc, stringify(v)] + }, []) + .join(',') + + ']' + ) + + if (data.constructor === Object) + return ( + '{' + + Object.keys(data) + .reduce((acc, k) => { + if (data[k] === undefined) return acc + else return [...acc, stringify(k) + ':' + stringify(data[k])] + }, []) + .join(',') + + '}' + ) + + throw new Error(`Unsupported type: ${data.constructor.name}`) +} + +export default { parse, stringify } diff --git a/lib/jsonb.test.js b/lib/jsonb.test.js new file mode 100644 index 0000000..922ff58 --- /dev/null +++ b/lib/jsonb.test.js @@ -0,0 +1,99 @@ +import test from 'node:test' +import { strict as assert } from 'node:assert' +import JSONB from './jsonb.js' + +test('parse jsonb: obj', () => { + const content = '{ "foo": { "bar": { "value": 12345678901234567890 } } }' + const parsed = JSONB.parse(content) + + assert.strictEqual(typeof parsed.foo.bar.value, 'bigint') + assert.strictEqual(parsed.foo.bar.value, 12345678901234567890n) +}) + +test('parse jsonb: neg', () => { + const content = '{ "value": -12345678901234567890 }' + const parsed = JSONB.parse(content) + + assert.strictEqual(typeof parsed.value, 'bigint') + assert.strictEqual(parsed.value, -12345678901234567890n) +}) + +test('parse jsonb: floating', () => { + const content = '{ "value": 1.0 }' + const parsed = JSONB.parse(content) + + assert.strictEqual(typeof parsed.value, 'number') + assert.strictEqual(parsed.value, 1.0) +}) + +test('parse jsonb: arr', () => { + const content = '[ 1, 12345678901234567890 ]' + const parsed = JSONB.parse(content) + + assert.strictEqual(typeof parsed[0], 'number') + assert.strictEqual(parsed[0], 1) + + assert.strictEqual(typeof parsed[1], 'bigint') + assert.strictEqual(parsed[1], 12345678901234567890n) +}) + +test('parse jsonb: str', () => { + const content = '{"value": "foo"}' + const parsed = JSONB.parse(content) + + assert.strictEqual(typeof parsed.value, 'string') + assert.strictEqual(parsed.value, 'foo') +}) + +test('parse jsonb: bool', () => { + const content = '{"value": true}' + const parsed = JSONB.parse(content) + + assert.strictEqual(typeof parsed.value, 'boolean') + assert.strictEqual(parsed.value, true) +}) + +test('stringify jsonb', () => { + const obj = { foo: { bar: { value: 12345678901234567890n } } } + const str = JSONB.stringify(obj) + + assert.strictEqual(str, '{"foo":{"bar":{"value":12345678901234567890}}}') +}) + +test('stringify jsonb: arr', () => { + const obj = [1, 12345678901234567890n] + const str = JSONB.stringify(obj) + + assert.strictEqual(str, '[1,12345678901234567890]') +}) + +test('stringify jsonb: bool', () => { + const obj = true + const str = JSONB.stringify(obj) + + assert.strictEqual(str, 'true') +}) + +test('stringify jsonb: number', () => { + const obj = 123 + const str = JSONB.stringify(obj) + + assert.strictEqual(str, '123') +}) + +test('stringify jsonb: neg number', () => { + const obj = -123 + const str = JSONB.stringify(obj) + + assert.strictEqual(str, '-123') +}) + +test('stringify jsonb: obj', () => { + const obj = { foo: true, bar: 1n, quz: true, baz: [-1, 2, 3n], qux: null } + const str = JSONB.stringify(obj) + + assert.strictEqual( + str, + '{"foo":true,"bar":1,"quz":true,"baz":[-1,2,3],"qux":null}', + ) +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..59d1d1c --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "json-bint", + "version": "0.0.4", + "private": false, + "description": "json with bigint supports.", + "keywords": [ + "json", + "bigint", + "bignumber" + ], + "repository": { + "type": "git", + "url": "https://github.com/weiran-zsd/jsonbn" + }, + "license": "MIT", + "author": "唯然=16" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..fbd58cb --- /dev/null +++ b/readme.md @@ -0,0 +1,43 @@ +# json-bint (json + bigint supports) + +## Why? + +json-bint:一个专为解决 JSON 不支持 BigInt 类型的问题而设计的开源 npm 包。在许多应用程序中,BigInt 类型用于表示超出传统 JavaScript 数值范围的大整数,但是标准的 JSON 解析器无法直接处理这种类型。json-bint 正是为了填补这一缺失而诞生的。 + +json-bint 提供了一种简便的方式,可以在将 BigInt 值序列化为 JSON 字符串时,确保其信息不会丢失。同时,它还能够在解析 JSON 字符串时,将包含 BigInt 值的字段正确地转换为 JavaScript 中的 BigInt 类型,以便应用程序可以无缝处理这些数据。 + +## Solution + +json-bint 提供了一种简单的解决方案,以确保 BigInt 值在 JSON 序列化和解析过程中的完整性: + +序列化(BigInt to JSON):在将包含 BigInt 值的对象序列化为 JSON 字符串时,json-bint 会将 BigInt 值转换为正确的数值,以避免信息丢失。这确保了在反序列化时能够正确还原 BigInt 值。 + +反序列化(JSON to BigInt):在解析 JSON 字符串时,json-bint 会检测到超出安全范围的整数,并将其转换回 JavaScript 中的 BigInt 类型,以确保数据的准确性。 + +## Usage + +```sh +$ npm i json-bint +``` + +```js +import JBINT from 'json-bint' + +JSON.parse('{"foo": 12345678901234567890}') // { foo: 12345678901234567000 } +JBINT.parse('{"foo": 12345678901234567890}') // { foo: 12345678901234567890n } + +JSON.stringify({ foo: 12345678901234567890n }) // Uncaught TypeError: Do not know how to serialize a BigInt +JBINT.stringify({ foo: 12345678901234567890n }) // '{"foo": 12345678901234567890}' +``` + +## Perf + +TODO. + +## Credits: + +- 唯然 + +## Lisente + +MIT diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..04bc9d8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/esm/tsconfig.json", + "compilerOptions": { + "lib": ["DOM", "ES2022"], + "target": "es2022", + "allowJs": true, + "checkJs": true, + "noEmit": true, + "declaration": true, + "moduleResolution": "nodenext", + "allowSyntheticDefaultImports": true + }, + "include": ["./lib/"], + "exclude": ["./lib/*.test.js"] +}