├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benchmarks ├── README.md ├── benchmark-utils.js ├── fixture │ ├── .gitignore │ ├── big.json │ ├── medium.json │ └── small.json ├── gen-fixture.js ├── package-lock.json ├── package.json ├── parse-chunked.js ├── run-test.js ├── stringify-chunked-conformance.js ├── stringify-chunked.js ├── stringify-info.js └── tmp │ ├── .gitignore │ └── .npmignore ├── dist ├── .gitignore ├── .npmignore ├── package.json └── test │ ├── json-ext.js │ └── json-ext.min.js ├── index.d.ts ├── package-lock.json ├── package.json ├── scripts ├── bundle.js ├── deno-adapt-test.js └── transpile.cjs ├── src ├── index.js ├── parse-chunked.js ├── parse-chunked.test.js ├── stringify-cases.js ├── stringify-chunked.js ├── stringify-chunked.test.js ├── stringify-info.js ├── stringify-info.test.js ├── utils.js ├── web-streams.js └── web-streams.test.js ├── test-e2e ├── commonjs.cjs └── esm.js └── test-fixture ├── stringify-medium.json └── stringify-small.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2022, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "no-duplicate-case": 2, 13 | "no-undef": 2, 14 | "no-unused-vars": [ 15 | 2, 16 | { 17 | "vars": "all", 18 | "args": "after-used" 19 | } 20 | ], 21 | "no-empty": [ 22 | 2, 23 | { 24 | "allowEmptyCatch": true 25 | } 26 | ], 27 | "no-implicit-coercion": [ 28 | 2, 29 | { 30 | "boolean": true, 31 | "string": true, 32 | "number": true 33 | } 34 | ], 35 | "no-with": 2, 36 | "brace-style": 2, 37 | "no-mixed-spaces-and-tabs": 2, 38 | "no-multiple-empty-lines": 2, 39 | "no-multi-str": 2, 40 | "dot-location": [ 41 | 2, 42 | "property" 43 | ], 44 | "operator-linebreak": [ 45 | 2, 46 | "after", 47 | { 48 | "overrides": { 49 | "?": "before", 50 | ":": "before" 51 | } 52 | } 53 | ], 54 | "key-spacing": [ 55 | 2, 56 | { 57 | "beforeColon": false, 58 | "afterColon": true 59 | } 60 | ], 61 | "space-unary-ops": [ 62 | 2, 63 | { 64 | "words": true, 65 | "nonwords": false 66 | } 67 | ], 68 | "no-spaced-func": 2, 69 | "space-before-function-paren": [ 70 | 2, 71 | { 72 | "anonymous": "ignore", 73 | "named": "never" 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | 2, 78 | "never" 79 | ], 80 | "space-in-parens": [ 81 | 2, 82 | "never" 83 | ], 84 | "comma-dangle": [ 85 | 2, 86 | "never" 87 | ], 88 | "no-trailing-spaces": 2, 89 | "yoda": [ 90 | 2, 91 | "never" 92 | ], 93 | "camelcase": [ 94 | 2, 95 | { 96 | "properties": "never" 97 | } 98 | ], 99 | "comma-style": [ 100 | 2, 101 | "last" 102 | ], 103 | "curly": [ 104 | 2, 105 | "all" 106 | ], 107 | "dot-notation": 2, 108 | "eol-last": 2, 109 | "one-var": [ 110 | 2, 111 | "never" 112 | ], 113 | "wrap-iife": 2, 114 | "space-infix-ops": 2, 115 | "keyword-spacing": [ 116 | 2, 117 | { 118 | "overrides": { 119 | "else": { 120 | "before": true 121 | }, 122 | "while": { 123 | "before": true 124 | }, 125 | "catch": { 126 | "before": true 127 | }, 128 | "finally": { 129 | "before": true 130 | } 131 | } 132 | } 133 | ], 134 | "spaced-comment": [ 135 | 2, 136 | "always" 137 | ], 138 | "space-before-blocks": [ 139 | 2, 140 | "always" 141 | ], 142 | "semi": [ 143 | 2, 144 | "always" 145 | ], 146 | "indent": [ 147 | 2, 148 | 4, 149 | { 150 | "SwitchCase": 1 151 | } 152 | ], 153 | "linebreak-style": [ 154 | 2, 155 | "unix" 156 | ], 157 | "quotes": [ 158 | 2, 159 | "single", 160 | { 161 | "avoidEscape": true 162 | } 163 | ] 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | bin/* text eol=lf 3 | *.js text eol=lf 4 | *.json text eol=lf 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | PRIMARY_NODEJS_VERSION: 20 9 | REPORTER: "min" 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 21 | cache: "npm" 22 | - run: npm ci 23 | - run: npm run lint 24 | 25 | coverage: 26 | name: Collect coverage 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | node_version: 31 | - 14.17 32 | - 18.0 33 | - 20 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Setup node ${{ matrix.node_version }} 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: ${{ matrix.node_version }} 40 | cache: 'npm' 41 | - run: npm i # old versions of npm doesn't support lockfile v3 42 | if: ${{ matrix.node_version < '15' }} 43 | - run: npm ci 44 | - run: npm run coverage 45 | - name: Coveralls Parallel 46 | uses: coverallsapp/github-action@1.1.3 47 | with: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | flag-name: node-${{ matrix.node_version }} 50 | parallel: true 51 | 52 | send-to-coveralls: 53 | name: Send coverage to Coveralls 54 | needs: coverage 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Send coverage to Coveralls 58 | uses: coverallsapp/github-action@v2 59 | with: 60 | github-token: ${{ secrets.GITHUB_TOKEN }} 61 | parallel-finished: true 62 | 63 | test-bundle: 64 | name: Test bundle 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 69 | uses: actions/setup-node@v4 70 | with: 71 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 72 | cache: "npm" 73 | - run: npm ci 74 | - run: npm run bundle-and-test 75 | 76 | e2e-tests: 77 | name: E2E tests 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v4 81 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 82 | uses: actions/setup-node@v4 83 | with: 84 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 85 | cache: "npm" 86 | - run: npm ci 87 | - run: npm run transpile 88 | - run: npm run bundle 89 | - run: npm run test:e2e 90 | 91 | unit-tests: 92 | name: Unit tests 93 | runs-on: ubuntu-latest 94 | strategy: 95 | matrix: 96 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 97 | node_version: 98 | - 14.17 99 | - 16 100 | - 18.0 101 | - 18 102 | - 20 103 | - 22 104 | 105 | steps: 106 | - uses: actions/checkout@v4 107 | - name: Setup node ${{ matrix.node_version }} 108 | uses: actions/setup-node@v4 109 | with: 110 | node-version: ${{ matrix.node_version }} 111 | cache: "npm" 112 | - run: npm i # old versions of npm doesn't support lockfile v3 113 | if: ${{ matrix.node_version < '15' }} 114 | - run: npm ci 115 | - run: npm run transpile 116 | - run: npm run test 117 | - run: npm run test:cjs 118 | 119 | test-bun: 120 | name: Bun test 121 | runs-on: ubuntu-latest 122 | strategy: 123 | matrix: 124 | bun_version: 125 | - 1.1.15 126 | 127 | steps: 128 | - uses: actions/checkout@v4 129 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 130 | uses: actions/setup-node@v4 131 | with: 132 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 133 | cache: "npm" 134 | - name: Setup Bun ${{ matrix.bun_version }} 135 | uses: oven-sh/setup-bun@v1 136 | with: 137 | bun-version: ${{ matrix.bun_version }} 138 | - run: npm ci 139 | - run: npm run transpile 140 | - run: bun test 141 | 142 | test-deno: 143 | name: Deno test 144 | runs-on: ubuntu-latest 145 | strategy: 146 | matrix: 147 | deno_version: 148 | - 1.44 149 | 150 | steps: 151 | - uses: actions/checkout@v4 152 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }} 153 | uses: actions/setup-node@v4 154 | with: 155 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }} 156 | cache: "npm" 157 | - name: Setup Deno ${{ matrix.deno_version }} 158 | uses: denoland/setup-deno@v1 159 | with: 160 | deno-version: ${{ matrix.deno_version }} 161 | - run: npm ci 162 | - run: deno task test:deno 163 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /**/node_modules/ 3 | /coverage/ 4 | /.nyc_output/ 5 | /.vscode/ 6 | /cjs/ 7 | /deno-tests/ 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.3 (2024-10-24) 2 | 3 | - Fixed an issue with `types` in the `exports` of `package.json` that introduced in version `0.6.2` 4 | 5 | ## 0.6.2 (2024-10-18) 6 | 7 | - Added `spaceBytes` field to `stringifyInfo()` result, which indicates the number of bytes used for white spaces. This allows for estimating size of `JSON.stringify()` result with and without formatting (when `space` option is used) in a single pass instead of two 8 | - Fixed `stringifyInfo()` to correctly accept the `space` parameter from options, i.e. `stringifyInfo(data, { space: 2 })` 9 | 10 | ## 0.6.1 (2024-08-06) 11 | 12 | - Enhanced the performance of `stringifyChunked()` by 1.5-3x 13 | - Enhanced the performance of `stringifyInfo()` by 1.5-5x 14 | - Fixed `parseFromWebStream()` to ensure that the lock on the reader is properly released 15 | 16 | ## 0.6.0 (2024-07-02) 17 | 18 | - Added `stringifyChunked()` as a generator function (as a replacer for `stringifyStream()`) 19 | - Added `createStringifyWebStream()` function 20 | - Added `parseFromWebStream()` function 21 | - Changed `parseChunked()` to accept an iterable or async iterable that iterates over string, Buffer, or TypedArray elements 22 | - Removed `stringifyStream()`, use `Readable.from(stringifyChunked())` instead 23 | - Fixed conformance `stringifyChunked()` with `JSON.stringify()` when replacer a list of keys and a key refer to an entry in a prototype chain 24 | - `stringifyInfo()`: 25 | - Aligned API with `stringifyChunked` by accepting `options` as the second parameter. Now supports: 26 | - `stringifyInfo(value, replacer?, space?)` 27 | - `stringifyInfo(value, options?)` 28 | - Renamed `minLength` field into `bytes` in functions result 29 | - Removed the `async` option 30 | - The function result no longer contains the `async` and `duplicate` fields 31 | - Fixed conformance with `JSON.stringify()` when replacer a list of keys and a key refer to an entry in a prototype chain 32 | - Discontinued exposing the `version` attribute 33 | - Converted to Dual Package, i.e. ESM and CommonJS support 34 | 35 | ## 0.5.7 (2022-03-09) 36 | 37 | - Fixed adding entire `package.json` content to a bundle when target is a browser 38 | 39 | ## 0.5.6 (2021-11-30) 40 | 41 | - Fixed `stringifyStream()` hang when last element in a stream takes a long time to process (#9, @kbrownlees) 42 | 43 | ## 0.5.5 (2021-09-14) 44 | 45 | - Added missed TypeScript typings file into the npm package 46 | 47 | ## 0.5.4 (2021-09-14) 48 | 49 | - Added TypeScript typings (#7, @lexich) 50 | 51 | ## 0.5.3 (2021-05-13) 52 | 53 | - Fixed `stringifyStream()` and `stringifyInfo()` to work properly when replacer is an allowlist 54 | - `parseChunked()` 55 | - Fixed wrong parse error when chunks are splitted on a whitespace inside an object or array (#6, @alexei-vedder) 56 | - Fixed corner cases when wrong placed or missed comma doesn't cause to parsing failure 57 | 58 | ## 0.5.2 (2020-12-26) 59 | 60 | - Fixed `RangeError: Maximum call stack size exceeded` in `parseChunked()` on very long arrays (corner case) 61 | 62 | ## 0.5.1 (2020-12-18) 63 | 64 | - Fixed `parseChunked()` crash when input has trailing whitespaces (#4, @smelukov) 65 | 66 | ## 0.5.0 (2020-12-05) 67 | 68 | - Added support for Node.js 10 69 | 70 | ## 0.4.0 (2020-12-04) 71 | 72 | - Added `parseChunked()` method 73 | - Fixed `stringifyInfo()` to not throw when meet unknown value type 74 | 75 | ## 0.3.2 (2020-10-26) 76 | 77 | - Added missed file for build purposes 78 | 79 | ## 0.3.1 (2020-10-26) 80 | 81 | - Changed build setup to allow building by any bundler that supports `browser` property in `package.json` 82 | - Exposed version 83 | 84 | ## 0.3.0 (2020-09-28) 85 | 86 | - Renamed `info()` method into `stringifyInfo()` 87 | - Fixed lib's distribution setup 88 | 89 | ## 0.2.0 (2020-09-28) 90 | 91 | - Added `dist` version to package (`dist/json-ext.js` and `dist/json-ext.min.js`) 92 | 93 | ## 0.1.1 (2020-09-08) 94 | 95 | - Fixed main entry point 96 | 97 | ## 0.1.0 (2020-09-08) 98 | 99 | - Initial release 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Roman Dvornov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-ext 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@discoveryjs/json-ext.svg)](https://www.npmjs.com/package/@discoveryjs/json-ext) 4 | [![Build Status](https://github.com/discoveryjs/json-ext/actions/workflows/ci.yml/badge.svg)](https://github.com/discoveryjs/json-ext/actions/workflows/ci.yml) 5 | [![Coverage Status](https://coveralls.io/repos/github/discoveryjs/json-ext/badge.svg?branch=master)](https://coveralls.io/github/discoveryjs/json-ext) 6 | [![NPM Downloads](https://img.shields.io/npm/dm/@discoveryjs/json-ext.svg)](https://www.npmjs.com/package/@discoveryjs/json-ext) 7 | 8 | A set of utilities designed to extend JSON's capabilities, especially for handling large JSON data (over 100MB) efficiently: 9 | 10 | - [parseChunked()](#parsechunked) – Parses JSON incrementally; similar to [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), but processing JSON data in chunks. 11 | - [stringifyChunked()](#stringifychunked) – Converts JavaScript objects to JSON incrementally; similar to [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify), but returns a generator that yields JSON strings in parts. 12 | - [stringifyInfo()](#stringifyinfo) – Estimates the size of the `JSON.stringify()` result and identifies circular references without generating the JSON. 13 | - [parseFromWebStream()](#parsefromwebstream) – A helper function to parse JSON chunks directly from a Web Stream. 14 | - [createStringifyWebStream()](#createstringifywebstream) – A helper function to generate JSON data as a Web Stream. 15 | 16 | ### Key Features 17 | 18 | - Optimized to handle large JSON data with minimal resource usage (see [benchmarks](./benchmarks/README.md)) 19 | - Works seamlessly with browsers, Node.js, Deno, and Bun 20 | - Supports both Node.js and Web streams 21 | - Available in both ESM and CommonJS 22 | - TypeScript typings included 23 | - No external dependencies 24 | - Compact size: 9.4Kb (minified), 3.8Kb (min+gzip) 25 | 26 | ### Why json-ext? 27 | 28 | - **Handles large JSON files**: Overcomes the limitations of V8 for strings larger than ~500MB, enabling the processing of huge JSON data. 29 | - **Prevents main thread blocking**: Distributes parsing and stringifying over time, ensuring the main thread remains responsive during heavy JSON operations. 30 | - **Reduces memory usage**: Traditional `JSON.parse()` and `JSON.stringify()` require loading entire data into memory, leading to high memory consumption and increased garbage collection pressure. `parseChunked()` and `stringifyChunked()` process data incrementally, optimizing memory usage. 31 | - **Size estimation**: `stringifyInfo()` allows estimating the size of resulting JSON before generating it, enabling better decision-making for JSON generation strategies. 32 | 33 | ## Install 34 | 35 | ```bash 36 | npm install @discoveryjs/json-ext 37 | ``` 38 | 39 | ## API 40 | 41 | ### parseChunked() 42 | 43 | Functions like [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), iterating over chunks to reconstruct the result object, and returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 44 | 45 | > Note: `reviver` parameter is not supported yet. 46 | 47 | ```ts 48 | function parseChunked(input: Iterable | AsyncIterable): Promise; 49 | function parseChunked(input: () => (Iterable | AsyncIterable)): Promise; 50 | 51 | type Chunk = string | Buffer | Uint8Array; 52 | ``` 53 | 54 | [Benchmark](https://github.com/discoveryjs/json-ext/tree/master/benchmarks#parse-chunked) 55 | 56 | Usage: 57 | 58 | ```js 59 | import { parseChunked } from '@discoveryjs/json-ext'; 60 | 61 | const data = await parseChunked(chunkEmitter); 62 | ``` 63 | 64 | Parameter `chunkEmitter` can be an iterable or async iterable that iterates over chunks, or a function returning such a value. A chunk can be a `string`, `Uint8Array`, or Node.js `Buffer`. 65 | 66 | Examples: 67 | 68 | - Generator: 69 | ```js 70 | parseChunked(function*() { 71 | yield '{ "hello":'; 72 | yield Buffer.from(' "wor'); // Node.js only 73 | yield new TextEncoder().encode('ld" }'); // returns Uint8Array 74 | }); 75 | ``` 76 | - Async generator: 77 | ```js 78 | parseChunked(async function*() { 79 | for await (const chunk of someAsyncSource) { 80 | yield chunk; 81 | } 82 | }); 83 | ``` 84 | - Array: 85 | ```js 86 | parseChunked(['{ "hello":', ' "world"}']) 87 | ``` 88 | - Function returning iterable: 89 | ```js 90 | parseChunked(() => ['{ "hello":', ' "world"}']) 91 | ``` 92 | - Node.js [`Readable`](https://nodejs.org/dist/latest-v14.x/docs/api/stream.html#stream_readable_streams) stream: 93 | ```js 94 | import fs from 'node:fs'; 95 | 96 | parseChunked(fs.createReadStream('path/to/file.json')) 97 | ``` 98 | - Web stream (e.g., using [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)): 99 | > Note: Iterability for Web streams was added later in the Web platform, not all environments support it. Consider using `parseFromWebStream()` for broader compatibility. 100 | ```js 101 | const response = await fetch('https://example.com/data.json'); 102 | const data = await parseChunked(response.body); // body is ReadableStream 103 | ``` 104 | 105 | ### stringifyChunked() 106 | 107 | Functions like [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify), but returns a generator yielding strings instead of a single string. 108 | 109 | > Note: Returns `"null"` when `JSON.stringify()` returns `undefined` (since a chunk cannot be `undefined`). 110 | 111 | ```ts 112 | function stringifyChunked(value: any, replacer?: Replacer, space?: Space): Generator; 113 | function stringifyChunked(value: any, options: StringifyOptions): Generator; 114 | 115 | type Replacer = 116 | | ((this: any, key: string, value: any) => any) 117 | | (string | number)[] 118 | | null; 119 | type Space = string | number | null; 120 | type StringifyOptions = { 121 | replacer?: Replacer; 122 | space?: Space; 123 | highWaterMark?: number; 124 | }; 125 | ``` 126 | 127 | [Benchmark](https://github.com/discoveryjs/json-ext/tree/master/benchmarks#stream-stringifying) 128 | 129 | Usage: 130 | 131 | - Getting an array of chunks: 132 | ```js 133 | const chunks = [...stringifyChunked(data)]; 134 | ``` 135 | - Iterating over chunks: 136 | ```js 137 | for (const chunk of stringifyChunked(data)) { 138 | console.log(chunk); 139 | } 140 | ``` 141 | - Specifying the minimum size of a chunk with `highWaterMark` option: 142 | ```js 143 | const data = [1, "hello world", 42]; 144 | 145 | console.log([...stringifyChunked(data)]); // default 16kB 146 | // ['[1,"hello world",42]'] 147 | 148 | console.log([...stringifyChunked(data, { highWaterMark: 16 })]); 149 | // ['[1,"hello world"', ',42]'] 150 | 151 | console.log([...stringifyChunked(data, { highWaterMark: 1 })]); 152 | // ['[1', ',"hello world"', ',42', ']'] 153 | ``` 154 | - Streaming into a stream with a `Promise` (modern Node.js): 155 | ```js 156 | import { pipeline } from 'node:stream/promises'; 157 | import fs from 'node:fs'; 158 | 159 | await pipeline( 160 | stringifyChunked(data), 161 | fs.createWriteStream('path/to/file.json') 162 | ); 163 | ``` 164 | - Wrapping into a `Promise` streaming into a stream (legacy Node.js): 165 | ```js 166 | import { Readable } from 'node:stream'; 167 | 168 | new Promise((resolve, reject) => { 169 | Readable.from(stringifyChunked(data)) 170 | .on('error', reject) 171 | .pipe(stream) 172 | .on('error', reject) 173 | .on('finish', resolve); 174 | }); 175 | ``` 176 | - Writing into a file synchronously: 177 | > Note: Slower than `JSON.stringify()` but uses much less heap space and has no limitation on string length 178 | ```js 179 | import fs from 'node:fs'; 180 | 181 | const fd = fs.openSync('output.json', 'w'); 182 | 183 | for (const chunk of stringifyChunked(data)) { 184 | fs.writeFileSync(fd, chunk); 185 | } 186 | 187 | fs.closeSync(fd); 188 | ``` 189 | - Using with fetch (JSON streaming): 190 | > Note: This feature has limited support in browsers, see [Streaming requests with the fetch API](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests) 191 | 192 | > Note: `ReadableStream.from()` has limited [support in browsers](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static), use [`createStringifyWebStream()`](#createstringifywebstream) instead. 193 | ```js 194 | fetch('http://example.com', { 195 | method: 'POST', 196 | duplex: 'half', 197 | body: ReadableStream.from(stringifyChunked(data)) 198 | }); 199 | ``` 200 | - Wrapping into `ReadableStream`: 201 | > Note: Use `ReadableStream.from()` or [`createStringifyWebStream()`](#createstringifywebstream) when no extra logic is needed 202 | ```js 203 | new ReadableStream({ 204 | start() { 205 | this.generator = stringifyChunked(data); 206 | }, 207 | pull(controller) { 208 | const { value, done } = this.generator.next(); 209 | 210 | if (done) { 211 | controller.close(); 212 | } else { 213 | controller.enqueue(value); 214 | } 215 | }, 216 | cancel() { 217 | this.generator = null; 218 | } 219 | }); 220 | ``` 221 | 222 | ### stringifyInfo() 223 | 224 | ```ts 225 | export function stringifyInfo(value: any, replacer?: Replacer, space?: Space): StringifyInfoResult; 226 | export function stringifyInfo(value: any, options?: StringifyInfoOptions): StringifyInfoResult; 227 | 228 | type StringifyInfoOptions = { 229 | replacer?: Replacer; 230 | space?: Space; 231 | continueOnCircular?: boolean; 232 | } 233 | type StringifyInfoResult = { 234 | bytes: number; // size of JSON in bytes 235 | spaceBytes: number; // size of white spaces in bytes (when space option used) 236 | circular: object[]; // list of circular references 237 | }; 238 | ``` 239 | 240 | Functions like [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify), but returns an object with the expected overall size of the stringify operation and a list of circular references. 241 | 242 | Example: 243 | 244 | ```js 245 | import { stringifyInfo } from '@discoveryjs/json-ext'; 246 | 247 | console.log(stringifyInfo({ test: true }, null, 4)); 248 | // { 249 | // bytes: 20, // Buffer.byteLength('{\n "test": true\n}') 250 | // spaceBytes: 7, 251 | // circular: [] 252 | // } 253 | ``` 254 | 255 | #### Options 256 | 257 | ##### continueOnCircular 258 | 259 | Type: `Boolean` 260 | Default: `false` 261 | 262 | Determines whether to continue collecting info for a value when a circular reference is found. Setting this option to `true` allows finding all circular references. 263 | 264 | ### parseFromWebStream() 265 | 266 | A helper function to consume JSON from a Web Stream. You can use `parseChunked(stream)` instead, but `@@asyncIterator` on `ReadableStream` has limited support in browsers (see [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) compatibility table). 267 | 268 | ```js 269 | import { parseFromWebStream } from '@discoveryjs/json-ext'; 270 | 271 | const data = await parseFromWebStream(readableStream); 272 | // equivalent to (when ReadableStream[@@asyncIterator] is supported): 273 | // await parseChunked(readableStream); 274 | ``` 275 | 276 | ### createStringifyWebStream() 277 | 278 | A helper function to convert `stringifyChunked()` into a `ReadableStream` (Web Stream). You can use `ReadableStream.from()` instead, but this method has limited support in browsers (see [ReadableStream.from()](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static) compatibility table). 279 | 280 | ```js 281 | import { createStringifyWebStream } from '@discoveryjs/json-ext'; 282 | 283 | createStringifyWebStream({ test: true }); 284 | // equivalent to (when ReadableStream.from() is supported): 285 | // ReadableStream.from(stringifyChunked({ test: true })) 286 | ``` 287 | 288 | ## License 289 | 290 | MIT 291 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks for JSON utils libraries 2 | 3 | 4 | 5 | - [Parse chunked](#parse-chunked) 6 | - [Stringify chunked](#stringify-chunked) 7 | - [Stringify info](#stringify-info) 8 | 9 | 10 | 11 | ## Parse chunked 12 | 13 | Benchmark: `parse-chunked.js` 14 | 15 | How to run: 16 | 17 | ``` 18 | node benchmarks/parse-chunked [fixture] 19 | ``` 20 | 21 | Where `[fixture]` is number of fixture: 22 | 23 | * `0` – fixture/small.json (~2MB) 24 | * `1` – fixture/medium.json (~13.7MB) 25 | * `2` – fixture/big.json (~100MB) 26 | * `3` – fixture/500mb.json (500MB, auto-generated from big.json x 5 + padding strings) 27 | * `4` – fixture/1gb.json (1gb, auto-generated from big.json x 10 + padding strings) 28 | 29 | ### Time 30 | 31 | 32 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 33 | | -------- | -------: | ----------: | ---------: | ----: | --: | 34 | | JSON.parse() | 18ms | 53ms | 592ms | 3325ms | CRASH | 35 | | @discoveryjs/json-ext parseChunked(fs.createReadStream()) | 41ms | 89ms | 753ms | 3709ms | 7342ms | 36 | | @discoveryjs/json-ext parseChunked(fs.readFileSync()) | 37ms | 83ms | 761ms | 3776ms | 8128ms | 37 | | @discoveryjs/json-ext parseFromWebStream() | 44ms | 92ms | 756ms | 3738ms | 7439ms | 38 | | bfj | 756ms | 3042ms | 55518ms | CRASH | ERR_RUN_TOO_LONG | 39 | 40 | 41 | ### CPU usage 42 | 43 | 44 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 45 | | -------- | -------: | ----------: | ---------: | ----: | --: | 46 | | JSON.parse() | 17ms | 59ms | 863ms | 3813ms | CRASH | 47 | | @discoveryjs/json-ext parseChunked(fs.createReadStream()) | 52ms | 112ms | 1033ms | 4892ms | 9052ms | 48 | | @discoveryjs/json-ext parseChunked(fs.readFileSync()) | 47ms | 110ms | 996ms | 4560ms | 10294ms | 49 | | @discoveryjs/json-ext parseFromWebStream() | 59ms | 114ms | 1067ms | 4949ms | 9015ms | 50 | | bfj | 924ms | 3241ms | 57905ms | CRASH | ERR_RUN_TOO_LONG | 51 | 52 | 53 | ### Max memory usage 54 | 55 | 56 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 57 | | -------- | -------: | ----------: | ---------: | ----: | --: | 58 | | JSON.parse() | 6.50MB | 19.17MB | 113.74MB | 1.57GB | CRASH | 59 | | @discoveryjs/json-ext parseChunked(fs.createReadStream()) | 11.41MB | 47.26MB | 146.38MB | 618.23MB | 1.23GB | 60 | | @discoveryjs/json-ext parseChunked(fs.readFileSync()) | 10.83MB | 48.93MB | 222.70MB | 1.12GB | 2.15GB | 61 | | @discoveryjs/json-ext parseFromWebStream() | 12.04MB | 47.58MB | 146.34MB | 617.86MB | 1.24GB | 62 | | bfj | 63.93MB | 123.42MB | 2.32GB | CRASH | ERR_RUN_TOO_LONG | 63 | 64 | 65 | ### Output for fixtures 66 | 67 |
68 |
> node benchmarks/parse-chunked    # use benchmarks/fixture/small.json (~2MB)
69 | 70 | 71 | ``` 72 | Benchmark: parseChunked() (parse chunked JSON) 73 | Node version: 20.14.0 74 | Fixture: fixture/small.json 2.08MB / chunk size 524kB 75 | 76 | # JSON.parse() 77 | time: 18 ms 78 | cpu: 17 ms 79 | mem impact: rss +7.59MB | heapTotal +5.77MB | heapUsed +2.00MB | external +56 80 | max: rss +11.39MB | heapTotal +10.19MB | heapUsed +6.50MB | external +56 81 | 82 | # @discoveryjs/json-ext parseChunked(fs.createReadStream()) 83 | time: 41 ms 84 | cpu: 52 ms 85 | mem impact: rss +5.46MB | heapTotal +6.82MB | heapUsed +2.36MB | external +56 86 | max: rss +10.58MB | heapTotal +11.01MB | heapUsed +8.80MB | external +2.60MB 87 | 88 | # @discoveryjs/json-ext parseChunked(fs.readFileSync()) 89 | time: 37 ms 90 | cpu: 47 ms 91 | mem impact: rss +5.85MB | heapTotal +6.29MB | heapUsed +2.29MB | external +56 92 | max: rss +10.86MB | heapTotal +10.75MB | heapUsed +8.75MB | external +2.08MB 93 | 94 | # @discoveryjs/json-ext parseFromWebStream() 95 | time: 44 ms 96 | cpu: 59 ms 97 | mem impact: rss +7.27MB | heapTotal +7.34MB | heapUsed +2.70MB | external +160kB 98 | max: rss +12.44MB | heapTotal +11.53MB | heapUsed +9.28MB | external +2.76MB 99 | 100 | # bfj 101 | time: 756 ms 102 | cpu: 924 ms 103 | mem impact: rss +76.89MB | heapTotal +35.13MB | heapUsed +5.04MB | external +63 104 | max: rss +87.65MB | heapTotal +81.15MB | heapUsed +62.38MB | external +1.55MB 105 | ``` 106 | 107 |
108 | 109 |
110 |
> node benchmarks/parse-chunked 1  # use benchmarks/fixture/medium.json (~13.7MB)
111 | 112 | 113 | ``` 114 | Benchmark: parseChunked() (parse chunked JSON) 115 | Node version: 20.14.0 116 | Fixture: fixture/medium.json 13.69MB / chunk size 524kB 117 | 118 | # JSON.parse() 119 | time: 53 ms 120 | cpu: 59 ms 121 | mem impact: rss +61.62MB | heapTotal +49.00MB | heapUsed +18.96MB | external +56 122 | max: rss +88.82MB | heapTotal +48.74MB | heapUsed +19.17MB | external +56 123 | 124 | # @discoveryjs/json-ext parseChunked(fs.createReadStream()) 125 | time: 89 ms 126 | cpu: 112 ms 127 | mem impact: rss +40.68MB | heapTotal +49.53MB | heapUsed +19.49MB | external +56 128 | max: rss +56.57MB | heapTotal +58.82MB | heapUsed +39.33MB | external +7.93MB 129 | 130 | # @discoveryjs/json-ext parseChunked(fs.readFileSync()) 131 | time: 83 ms 132 | cpu: 110 ms 133 | mem impact: rss +39.27MB | heapTotal +48.48MB | heapUsed +19.19MB | external +56 134 | max: rss +53.59MB | heapTotal +58.20MB | heapUsed +35.24MB | external +13.69MB 135 | 136 | # @discoveryjs/json-ext parseFromWebStream() 137 | time: 92 ms 138 | cpu: 114 ms 139 | mem impact: rss +42.29MB | heapTotal +50.05MB | heapUsed +19.61MB | external +160kB 140 | max: rss +58.57MB | heapTotal +58.56MB | heapUsed +39.49MB | external +8.09MB 141 | 142 | # bfj 143 | time: 3042 ms 144 | cpu: 3241 ms 145 | mem impact: rss +142.38MB | heapTotal +97.80MB | heapUsed +20.87MB | external +63 146 | max: rss +146.15MB | heapTotal +135.76MB | heapUsed +118.84MB | external +4.59MB 147 | ``` 148 | 149 |
150 | 151 | 152 |
153 |
> node benchmarks/parse-chunked 2  # use benchmarks/fixture/big.json (~100MB)
154 | 155 | 156 | ``` 157 | Benchmark: parseChunked() (parse chunked JSON) 158 | Node version: 20.14.0 159 | Fixture: fixture/big.json 99.95MB / chunk size 524kB 160 | 161 | # JSON.parse() 162 | time: 592 ms 163 | cpu: 863 ms 164 | mem impact: rss +267.88MB | heapTotal +144.79MB | heapUsed +113.57MB | external +56 165 | max: rss +466.63MB | heapTotal +145.05MB | heapUsed +113.74MB | external +56 166 | 167 | # @discoveryjs/json-ext parseChunked(fs.createReadStream()) 168 | time: 753 ms 169 | cpu: 1033 ms 170 | mem impact: rss +165.46MB | heapTotal +146.11MB | heapUsed +114.15MB | external +56 171 | max: rss +181.78MB | heapTotal +155.53MB | heapUsed +136.42MB | external +9.96MB 172 | 173 | # @discoveryjs/json-ext parseChunked(fs.readFileSync()) 174 | time: 761 ms 175 | cpu: 996 ms 176 | mem impact: rss +238.55MB | heapTotal +146.37MB | heapUsed +113.98MB | external +56 177 | max: rss +244.20MB | heapTotal +146.06MB | heapUsed +122.75MB | external +99.95MB 178 | 179 | # @discoveryjs/json-ext parseFromWebStream() 180 | time: 756 ms 181 | cpu: 1067 ms 182 | mem impact: rss +158.25MB | heapTotal +147.16MB | heapUsed +114.43MB | external +160kB 183 | max: rss +175.23MB | heapTotal +163.68MB | heapUsed +136.24MB | external +10.11MB 184 | 185 | # bfj 186 | time: 55518 ms 187 | cpu: 57905 ms 188 | mem impact: rss +2.37GB | heapTotal +2.28GB | heapUsed +1.76GB | external +63 189 | max: rss +2.22GB | heapTotal +2.36GB | heapUsed +2.30GB | external +17.76MB 190 | ``` 191 | 192 |
193 | 194 |
195 |
> node benchmarks/parse-chunked 3  # use benchmarks/fixture/500mb.json
196 | 197 | 198 | ``` 199 | Benchmark: parseChunked() (parse chunked JSON) 200 | Node version: 20.14.0 201 | Fixture: fixture/500mb.json 500MB / chunk size 524kB 202 | 203 | # JSON.parse() 204 | time: 3325 ms 205 | cpu: 3813 ms 206 | mem impact: rss +612.47MB | heapTotal +608.58MB | heapUsed +568.88MB | external +56 207 | max: rss +1.42GB | heapTotal +1.60GB | heapUsed +1.57GB | external +56 208 | 209 | # @discoveryjs/json-ext parseChunked(fs.createReadStream()) 210 | time: 3709 ms 211 | cpu: 4892 ms 212 | mem impact: rss +639.35MB | heapTotal +610.34MB | heapUsed +570.17MB | external +56 213 | max: rss +671.11MB | heapTotal +635.09MB | heapUsed +607.74MB | external +10.49MB 214 | 215 | # @discoveryjs/json-ext parseChunked(fs.readFileSync()) 216 | time: 3776 ms 217 | cpu: 4560 ms 218 | mem impact: rss +604.73MB | heapTotal +609.81MB | heapUsed +569.98MB | external +56 219 | max: rss +1.15GB | heapTotal +646.38MB | heapUsed +617.93MB | external +500.00MB 220 | 221 | # @discoveryjs/json-ext parseFromWebStream() 222 | time: 3738 ms 223 | cpu: 4949 ms 224 | mem impact: rss +637.76MB | heapTotal +610.34MB | heapUsed +570.55MB | external +160kB 225 | max: rss +669.96MB | heapTotal +634.04MB | heapUsed +606.18MB | external +11.68MB 226 | 227 | # bfj 228 | 229 | <--- Last few GCs ---> 230 | 231 | [65418:0x130008000] 161105 ms: Mark-Compact 4042.4 (4128.6) -> 4026.9 (4129.1) MB, 4035.96 / 0.00 ms (average mu = 0.130, current mu = 0.015) allocation failure; scavenge might not succeed 232 | [65418:0x130008000] 164489 ms: Mark-Compact 4042.8 (4129.1) -> 4027.2 (4129.4) MB, 3372.04 / 0.00 ms (average mu = 0.074, current mu = 0.004) allocation failure; scavenge might not succeed 233 | 234 | 235 | <--- JS stacktrace ---> 236 | 237 | FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 238 | ----- Native stack trace ----- 239 | 240 | 1: 0x1008fcb44 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/local/bin/node] 241 | 2: 0x100a843ec v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/local/bin/node] 242 | 3: 0x100c58ac0 v8::internal::Heap::GarbageCollectionReasonToString(v8::internal::GarbageCollectionReason) [/usr/local/bin/node] 243 | 4: 0x100c5c974 v8::internal::Heap::CollectGarbageShared(v8::internal::LocalHeap*, v8::internal::GarbageCollectionReason) [/usr/local/bin/node] 244 | 5: 0x100c593d8 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::internal::GarbageCollectionReason, char const*) [/usr/local/bin/node] 245 | 6: 0x100c57160 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/bin/node] 246 | 7: 0x100c4ddb4 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/bin/node] 247 | 8: 0x100c4e614 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/bin/node] 248 | 9: 0x100c33684 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/usr/local/bin/node] 249 | 10: 0x10101b394 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/usr/local/bin/node] 250 | 11: 0x101378c44 Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit [/usr/local/bin/node] 251 | 12: 0x10671cfe0 252 | 13: 0x106738eb4 253 | 14: 0x10675d248 254 | 15: 0x106721e1c 255 | 16: 0x106721088 256 | 17: 0x106722e6c 257 | 18: 0x10671de50 258 | 19: 0x10672f3dc 259 | 20: 0x10674d9d8 260 | 21: 0x1067561c8 261 | 22: 0x1012ee50c Builtins_JSEntryTrampoline [/usr/local/bin/node] 262 | 23: 0x1012ee1f4 Builtins_JSEntry [/usr/local/bin/node] 263 | 24: 0x100bc5f68 v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/usr/local/bin/node] 264 | 25: 0x100bc53b4 v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle, v8::internal::Handle, int, v8::internal::Handle*) [/usr/local/bin/node] 265 | 26: 0x100a9fca4 v8::Function::Call(v8::Local, v8::Local, int, v8::Local*) [/usr/local/bin/node] 266 | 27: 0x100828fa0 node::InternalMakeCallback(node::Environment*, v8::Local, v8::Local, v8::Local, int, v8::Local*, node::async_context) [/usr/local/bin/node] 267 | 28: 0x1008292b8 node::MakeCallback(v8::Isolate*, v8::Local, v8::Local, int, v8::Local*, node::async_context) [/usr/local/bin/node] 268 | 29: 0x10089e464 node::Environment::CheckImmediate(uv_check_s*) [/usr/local/bin/node] 269 | 30: 0x1012d64e4 uv__run_check [/usr/local/bin/node] 270 | 31: 0x1012d0204 uv_run [/usr/local/bin/node] 271 | 32: 0x1008296f0 node::SpinEventLoopInternal(node::Environment*) [/usr/local/bin/node] 272 | 33: 0x10093c7c0 node::NodeMainInstance::Run(node::ExitCode*, node::Environment*) [/usr/local/bin/node] 273 | 34: 0x10093c4d4 node::NodeMainInstance::Run() [/usr/local/bin/node] 274 | 35: 0x1008c47ac node::Start(int, char**) [/usr/local/bin/node] 275 | 36: 0x18dede0e0 start [/usr/lib/dyld] 276 | ``` 277 | 278 |
279 | 280 |
281 |
> node benchmarks/parse-chunked 4  # use benchmarks/fixture/1gb.json
282 | 283 | 284 | ``` 285 | Benchmark: parseChunked() (parse chunked JSON) 286 | Node version: 20.14.0 287 | Fixture: fixture/1gb.json 1000MB / chunk size 524kB 288 | 289 | # JSON.parse() 290 | FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal 291 | ----- Native stack trace ----- 292 | 293 | 1: 0x100824a20 node::OnFatalError(char const*, char const*) [/usr/local/bin/node] 294 | 2: 0x1009ae24c v8::api_internal::ToLocalEmpty() [/usr/local/bin/node] 295 | 3: 0x100831cf4 node::fs::ReadFileUtf8(v8::FunctionCallbackInfo const&) [/usr/local/bin/node] 296 | 4: 0x100a19f68 v8::internal::MaybeHandle v8::internal::(anonymous namespace)::HandleApiCallHelper(v8::internal::Isolate*, v8::internal::Handle, v8::internal::Handle, v8::internal::Handle, unsigned long*, int) [/usr/local/bin/node] 297 | 5: 0x100a19660 v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [/usr/local/bin/node] 298 | 6: 0x1012a0b24 Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit [/usr/local/bin/node] 299 | 7: 0x106636a98 300 | 8: 0x1012183e4 Builtins_InterpreterEntryTrampoline [/usr/local/bin/node] 301 | 9: 0x1012183e4 Builtins_InterpreterEntryTrampoline [/usr/local/bin/node] 302 | 10: 0x10124f210 Builtins_AsyncFunctionAwaitResolveClosure [/usr/local/bin/node] 303 | 11: 0x1012fcfb8 Builtins_PromiseFulfillReactionJob [/usr/local/bin/node] 304 | 12: 0x10123eb94 Builtins_RunMicrotasks [/usr/local/bin/node] 305 | 13: 0x1012163f4 Builtins_JSRunMicrotasksEntry [/usr/local/bin/node] 306 | 14: 0x100aedf40 v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/usr/local/bin/node] 307 | 15: 0x100aee42c v8::internal::(anonymous namespace)::InvokeWithTryCatch(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/usr/local/bin/node] 308 | 16: 0x100aee608 v8::internal::Execution::TryRunMicrotasks(v8::internal::Isolate*, v8::internal::MicrotaskQueue*) [/usr/local/bin/node] 309 | 17: 0x100b157d4 v8::internal::MicrotaskQueue::RunMicrotasks(v8::internal::Isolate*) [/usr/local/bin/node] 310 | 18: 0x100b15f70 v8::internal::MicrotaskQueue::PerformCheckpoint(v8::Isolate*) [/usr/local/bin/node] 311 | 19: 0x100750c4c node::InternalCallbackScope::Close() [/usr/local/bin/node] 312 | 20: 0x1007507bc node::InternalCallbackScope::~InternalCallbackScope() [/usr/local/bin/node] 313 | 21: 0x1007c7838 node::Environment::RunTimers(uv_timer_s*) [/usr/local/bin/node] 314 | 22: 0x1011f49d4 uv__run_timers [/usr/local/bin/node] 315 | 23: 0x1011f8234 uv_run [/usr/local/bin/node] 316 | 24: 0x1007516f0 node::SpinEventLoopInternal(node::Environment*) [/usr/local/bin/node] 317 | 25: 0x1008647c0 node::NodeMainInstance::Run(node::ExitCode*, node::Environment*) [/usr/local/bin/node] 318 | 26: 0x1008644d4 node::NodeMainInstance::Run() [/usr/local/bin/node] 319 | 27: 0x1007ec7ac node::Start(int, char**) [/usr/local/bin/node] 320 | 28: 0x18dede0e0 start [/usr/lib/dyld] 321 | 322 | ----- JavaScript stack trace ----- 323 | 324 | 1: readFileSync (node:fs:448:20) 325 | 2: JSON.parse() (file:///Users/romandvornov/Developer/json-ext/benchmarks/parse-chunked.js:38:23) 326 | 3: benchmark (file:///Users/romandvornov/Developer/json-ext/benchmarks/benchmark-utils.js:65:28) 327 | 328 | 329 | # @discoveryjs/json-ext parseChunked(fs.createReadStream()) 330 | time: 7342 ms 331 | cpu: 9052 ms 332 | mem impact: rss +1.21GB | heapTotal +1.19GB | heapUsed +1.14GB | external +56 333 | max: rss +918.83MB | heapTotal +1.26GB | heapUsed +1.22GB | external +11.53MB 334 | 335 | # @discoveryjs/json-ext parseChunked(fs.readFileSync()) 336 | time: 8128 ms 337 | cpu: 10294 ms 338 | mem impact: rss +1.18GB | heapTotal +1.19GB | heapUsed +1.14GB | external +56 339 | max: rss +1.59GB | heapTotal +1.20GB | heapUsed +1.15GB | external +1.00GB 340 | 341 | # @discoveryjs/json-ext parseFromWebStream() 342 | time: 7439 ms 343 | cpu: 9015 ms 344 | mem impact: rss +1.23GB | heapTotal +1.19GB | heapUsed +1.14GB | external +160kB 345 | max: rss +1.32GB | heapTotal +1.27GB | heapUsed +1.23GB | external +11.68MB 346 | 347 | # bfj 348 | Error: Run takes too long time 349 | at sizeLessThan (file://~/json-ext/benchmarks/parse-chunked.js:67:19) 350 | at bfj (file://~/json-ext/benchmarks/parse-chunked.js:54:18) 351 | at benchmark (file://~/json-ext/benchmarks/benchmark-utils.js:65:28) 352 | at async file://~/json-ext/benchmarks/run-test.js:7:17 353 | ``` 354 | 355 |
356 | 357 | ## Stringify chunked 358 | 359 | Benchmark: `stringify-chunked.js` 360 | 361 | How to run: 362 | 363 | ``` 364 | node benchmarks/stringify-chunked [fixture] 365 | ``` 366 | 367 | Where `[fixture]` is number of fixture: 368 | 369 | * `0` – fixture/small.json (~2MB) 370 | * `1` – fixture/medium.json (~13.7MB) 371 | * `2` – fixture/big.json (~100MB) 372 | * `3` – fixture/500mb.json (500MB, auto-generated from big.json x 5 + padding strings) 373 | * `4` – fixture/1gb.json (1gb, auto-generated from big.json x 10 + padding strings) 374 | 375 | ### Time 376 | 377 | 378 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 379 | | -------- | -------: | ----------: | ---------: | ----: | --: | 380 | | JSON.stringify() | 10ms | 42ms | 447ms | 2957ms | ERR_STRING_TOO_LONG | 381 | | @discoveryjs/json-ext stringifyChunked() | 20ms | 40ms | 645ms | 3476ms | 7536ms | 382 | | @discoveryjs/json-ext createStringifyWebStream() | 31ms | 46ms | 648ms | 3577ms | 7689ms | 383 | | @discoveryjs/json-ext v0.6.0 stringifyChunked() | 27ms | 40ms | 933ms | 5530ms | 11433ms | 384 | | @discoveryjs/json-ext v0.5.7 stringifyStream() | 37ms | 70ms | 1050ms | 5842ms | 12793ms | 385 | | json-stream-stringify | 33ms | 50ms | 1092ms | 5596ms | 11983ms | 386 | | bfj | 554ms | 2172ms | 75006ms | ERR_RUN_TOO_LONG | ERR_RUN_TOO_LONG | 387 | 388 | 389 | ### CPU usage 390 | 391 | 392 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 393 | | -------- | -------: | ----------: | ---------: | ----: | --: | 394 | | JSON.stringify() | 7ms | 32ms | 427ms | 1995ms | ERR_STRING_TOO_LONG | 395 | | @discoveryjs/json-ext stringifyChunked() | 37ms | 65ms | 698ms | 3595ms | 7690ms | 396 | | @discoveryjs/json-ext createStringifyWebStream() | 46ms | 73ms | 723ms | 3678ms | 7829ms | 397 | | @discoveryjs/json-ext v0.6.0 stringifyChunked() | 45ms | 63ms | 972ms | 5405ms | 11280ms | 398 | | @discoveryjs/json-ext v0.5.7 stringifyStream() | 52ms | 89ms | 1084ms | 5820ms | 12551ms | 399 | | json-stream-stringify | 58ms | 84ms | 1122ms | 5683ms | 12034ms | 400 | | bfj | 399ms | 850ms | 32648ms | ERR_RUN_TOO_LONG | ERR_RUN_TOO_LONG | 401 | 402 | 403 | ### Max memory usage 404 | 405 | 406 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 407 | | -------- | -------: | ----------: | ---------: | ----: | --: | 408 | | JSON.stringify() | 4.26MB | 27.46MB | 210.13MB | 1GB | ERR_STRING_TOO_LONG | 409 | | @discoveryjs/json-ext stringifyChunked() | 671kB | 9.74MB | 56.73MB | 249.85MB | 500.65MB | 410 | | @discoveryjs/json-ext createStringifyWebStream() | 1.78MB | 12.09MB | 52.64MB | 262.89MB | 504.76MB | 411 | | @discoveryjs/json-ext v0.6.0 stringifyChunked() | 6.88MB | 12.04MB | 73.69MB | 300.91MB | 596.83MB | 412 | | @discoveryjs/json-ext v0.5.7 stringifyStream() | 7.75MB | 18.37MB | 64.11MB | 301.09MB | 592.95MB | 413 | | json-stream-stringify | 7.93MB | 14.18MB | 8.17MB | 8.60MB | 14.89MB | 414 | | bfj | 17.55MB | 17.91MB | 38.92MB | ERR_RUN_TOO_LONG | ERR_RUN_TOO_LONG | 415 | 416 | 417 | ### Output for fixtures 418 | 419 |
420 |
> node benchmarks/stringify-chunked    # use benchmarks/fixture/small.json (~2MB)
421 | 422 | 423 | ``` 424 | Benchmark: stringifyChunked() (JSON.stringify() as a stream of chunks) 425 | Node version: 22.5.1 426 | Fixture: fixture/small.json 2.08MB 427 | 428 | # JSON.stringify() 429 | Result: 2077407 430 | time: 10 ms 431 | cpu: 7 ms 432 | mem impact: rss +7.67MB | heapTotal 0 | heapUsed +48kB | external +56 433 | max: rss +11.58MB | heapTotal +4.16MB | heapUsed +4.26MB | external +56 434 | 435 | # @discoveryjs/json-ext stringifyChunked() 436 | Result: 2077407 437 | time: 20 ms 438 | cpu: 37 ms 439 | mem impact: rss +3.24MB | heapTotal 0 | heapUsed +220kB | external +56 440 | max: rss +3.18MB | heapTotal +262kB | heapUsed +671kB | external +56 441 | 442 | # @discoveryjs/json-ext createStringifyWebStream() 443 | Result: 2077407 444 | time: 31 ms 445 | cpu: 46 ms 446 | mem impact: rss +5.78MB | heapTotal +8.91MB | heapUsed +641kB | external +160kB 447 | max: rss +5.69MB | heapTotal +9.18MB | heapUsed +1.62MB | external +160kB 448 | 449 | # @discoveryjs/json-ext v0.6.0 stringifyChunked() 450 | Result: 2077407 451 | time: 27 ms 452 | cpu: 45 ms 453 | mem impact: rss +5.31MB | heapTotal +8.65MB | heapUsed +223kB | external +56 454 | max: rss +5.19MB | heapTotal +8.65MB | heapUsed +6.88MB | external +56 455 | 456 | # @discoveryjs/json-ext v0.5.7 stringifyStream() 457 | Result: 2077471 458 | time: 37 ms 459 | cpu: 52 ms 460 | mem impact: rss +10.91MB | heapTotal +8.65MB | heapUsed +275kB | external +56 461 | max: rss +11.03MB | heapTotal +8.95MB | heapUsed +7.12MB | external +635kB 462 | 463 | # json-stream-stringify 464 | Result: 2077407 465 | time: 33 ms 466 | cpu: 58 ms 467 | mem impact: rss +6.96MB | heapTotal +8.65MB | heapUsed +352kB | external +56 468 | max: rss +6.78MB | heapTotal +8.91MB | heapUsed +7.93MB | external +56 469 | 470 | # bfj 471 | Result: 2077407 472 | time: 554 ms 473 | cpu: 399 ms 474 | mem impact: rss +36.32MB | heapTotal +26.74MB | heapUsed +1.15MB | external +3kB 475 | max: rss +36.32MB | heapTotal +29.36MB | heapUsed +17.54MB | external +3kB 476 | ``` 477 | 478 |
479 | 480 |
481 |
> node benchmarks/stringify-chunked 1  # use benchmarks/fixture/medium.json (~13.7MB)
482 | 483 | 484 | ``` 485 | Benchmark: stringifyChunked() (JSON.stringify() as a stream of chunks) 486 | Node version: 22.5.1 487 | Fixture: fixture/medium.json 13.69MB 488 | 489 | # JSON.stringify() 490 | Result: 13693862 491 | time: 42 ms 492 | cpu: 32 ms 493 | mem impact: rss +57.31MB | heapTotal 0 | heapUsed +50kB | external +56 494 | max: rss +84.41MB | heapTotal +27.39MB | heapUsed +27.46MB | external +56 495 | 496 | # @discoveryjs/json-ext stringifyChunked() 497 | Result: 13693862 498 | time: 40 ms 499 | cpu: 65 ms 500 | mem impact: rss +10.67MB | heapTotal 0 | heapUsed +97kB | external +56 501 | max: rss +10.65MB | heapTotal 0 | heapUsed +9.74MB | external +56 502 | 503 | # @discoveryjs/json-ext createStringifyWebStream() 504 | Result: 13693862 505 | time: 46 ms 506 | cpu: 73 ms 507 | mem impact: rss +12.34MB | heapTotal +524kB | heapUsed +553kB | external +160kB 508 | max: rss +12.16MB | heapTotal +786kB | heapUsed +11.93MB | external +160kB 509 | 510 | # @discoveryjs/json-ext v0.6.0 stringifyChunked() 511 | Result: 13693862 512 | time: 40 ms 513 | cpu: 63 ms 514 | mem impact: rss +9.47MB | heapTotal 0 | heapUsed +99kB | external +56 515 | max: rss +9.37MB | heapTotal +262kB | heapUsed +12.04MB | external +56 516 | 517 | # @discoveryjs/json-ext v0.5.7 stringifyStream() 518 | Result: 13693865 519 | time: 70 ms 520 | cpu: 89 ms 521 | mem impact: rss +13.32MB | heapTotal +262kB | heapUsed +183kB | external +56 522 | max: rss +13.14MB | heapTotal +262kB | heapUsed +15.16MB | external +3.20MB 523 | 524 | # json-stream-stringify 525 | Result: 13693862 526 | time: 50 ms 527 | cpu: 84 ms 528 | mem impact: rss +9.72MB | heapTotal +524kB | heapUsed +192kB | external +56 529 | max: rss +9.63MB | heapTotal +786kB | heapUsed +14.18MB | external +56 530 | 531 | # bfj 532 | Result: 13693862 533 | time: 2172 ms 534 | cpu: 850 ms 535 | mem impact: rss +18.22MB | heapTotal +1.84MB | heapUsed +1.07MB | external +3kB 536 | max: rss +18.12MB | heapTotal +2.36MB | heapUsed +17.90MB | external +3kB 537 | ``` 538 | 539 |
540 | 541 | 542 |
543 |
> node benchmarks/stringify-chunked 2  # use benchmarks/fixture/big.json (~100MB)
544 | 545 | 546 | ``` 547 | Benchmark: stringifyChunked() (JSON.stringify() as a stream of chunks) 548 | Node version: 22.5.1 549 | Fixture: fixture/big.json 99.95MB 550 | 551 | # JSON.stringify() 552 | Result: 99947225 553 | time: 447 ms 554 | cpu: 427 ms 555 | mem impact: rss +236.54MB | heapTotal 0 | heapUsed +48kB | external +56 556 | max: rss +435.98MB | heapTotal +199.90MB | heapUsed +210.13MB | external +56 557 | 558 | # @discoveryjs/json-ext stringifyChunked() 559 | Result: 99947225 560 | time: 645 ms 561 | cpu: 698 ms 562 | mem impact: rss +57.05MB | heapTotal +262kB | heapUsed +106kB | external +56 563 | max: rss +56.74MB | heapTotal +47.45MB | heapUsed +56.73MB | external +56 564 | 565 | # @discoveryjs/json-ext createStringifyWebStream() 566 | Result: 99947225 567 | time: 648 ms 568 | cpu: 723 ms 569 | mem impact: rss +57.52MB | heapTotal +524kB | heapUsed +581kB | external +160kB 570 | max: rss +57.18MB | heapTotal +48.76MB | heapUsed +52.48MB | external +160kB 571 | 572 | # @discoveryjs/json-ext v0.6.0 stringifyChunked() 573 | Result: 99947225 574 | time: 933 ms 575 | cpu: 972 ms 576 | mem impact: rss +66.98MB | heapTotal +262kB | heapUsed +108kB | external +56 577 | max: rss +66.73MB | heapTotal +58.46MB | heapUsed +73.69MB | external +56 578 | 579 | # @discoveryjs/json-ext v0.5.7 stringifyStream() 580 | Result: 99947225 581 | time: 1050 ms 582 | cpu: 1084 ms 583 | mem impact: rss +65.36MB | heapTotal 0 | heapUsed +159kB | external +56 584 | max: rss +64.82MB | heapTotal +57.93MB | heapUsed +63.79MB | external +326kB 585 | 586 | # json-stream-stringify 587 | Result: 99947225 588 | time: 1092 ms 589 | cpu: 1122 ms 590 | mem impact: rss +8.80MB | heapTotal +262kB | heapUsed +152kB | external +56 591 | max: rss +8.49MB | heapTotal +262kB | heapUsed +8.17MB | external +56 592 | ``` 593 | 594 |
595 | 596 |
597 |
> node benchmarks/stringify-chunked 3  # use benchmarks/fixture/500mb.json
598 | 599 | 600 | ``` 601 | Benchmark: stringifyChunked() (JSON.stringify() as a stream of chunks) 602 | Node version: 22.5.1 603 | Fixture: fixture/500mb.json 500MB 604 | 605 | # JSON.stringify() 606 | Result: 500000000 607 | time: 2957 ms 608 | cpu: 1995 ms 609 | mem impact: rss -34.59MB | heapTotal 0 | heapUsed +47kB | external +56 610 | max: rss +957.30MB | heapTotal +1.00GB | heapUsed +1.00GB | external +56 611 | 612 | # @discoveryjs/json-ext stringifyChunked() 613 | Result: 500000000 614 | time: 3476 ms 615 | cpu: 3595 ms 616 | mem impact: rss +251.46MB | heapTotal 0 | heapUsed +104kB | external +56 617 | max: rss +250.58MB | heapTotal +245.10MB | heapUsed +249.85MB | external +56 618 | 619 | # @discoveryjs/json-ext createStringifyWebStream() 620 | Result: 500000000 621 | time: 3577 ms 622 | cpu: 3678 ms 623 | mem impact: rss +255.30MB | heapTotal +262kB | heapUsed +577kB | external +160kB 624 | max: rss +254.64MB | heapTotal +246.68MB | heapUsed +262.73MB | external +160kB 625 | 626 | # @discoveryjs/json-ext v0.6.0 stringifyChunked() 627 | Result: 500000000 628 | time: 5530 ms 629 | cpu: 5405 ms 630 | mem impact: rss +186.20MB | heapTotal 0 | heapUsed +106kB | external +56 631 | max: rss +225.20MB | heapTotal +295.96MB | heapUsed +300.90MB | external +56 632 | 633 | # @discoveryjs/json-ext v0.5.7 stringifyStream() 634 | Result: 500000000 635 | time: 5842 ms 636 | cpu: 5820 ms 637 | mem impact: rss +186.86MB | heapTotal +262kB | heapUsed +190kB | external +56 638 | max: rss +44.40MB | heapTotal +293.86MB | heapUsed +300.65MB | external +444kB 639 | 640 | # json-stream-stringify 641 | Result: 500000000 642 | time: 5596 ms 643 | cpu: 5683 ms 644 | mem impact: rss +9.29MB | heapTotal +262kB | heapUsed +168kB | external +56 645 | max: rss +9.11MB | heapTotal +262kB | heapUsed +8.60MB | external +56 646 | ``` 647 | 648 |
649 | 650 |
651 |
> node benchmarks/stringify-chunked 4  # use benchmarks/fixture/1gb.json
652 | 653 | 654 | ``` 655 | Benchmark: stringifyChunked() (JSON.stringify() as a stream of chunks) 656 | Node version: 22.5.1 657 | Fixture: fixture/1gb.json 1000MB 658 | 659 | # JSON.stringify() 660 | RangeError: Invalid string length 661 | at JSON.stringify () 662 | at JSON.stringify() (../json-ext/benchmarks/stringify-chunked.js:58:15) 663 | at tests. (../json-ext/benchmarks/stringify-chunked.js:86:35) 664 | at benchmark (../json-ext/benchmarks/benchmark-utils.js:65:28) 665 | at async ../json-ext/benchmarks/run-test.js:7:17 666 | 667 | # @discoveryjs/json-ext stringifyChunked() 668 | Result: 1000000000 669 | time: 7536 ms 670 | cpu: 7690 ms 671 | mem impact: rss +502.27MB | heapTotal 0 | heapUsed +104kB | external +56 672 | max: rss +501.27MB | heapTotal +495.45MB | heapUsed +500.65MB | external +56 673 | 674 | # @discoveryjs/json-ext createStringifyWebStream() 675 | Result: 1000000000 676 | time: 7689 ms 677 | cpu: 7829 ms 678 | mem impact: rss +119.05MB | heapTotal +524kB | heapUsed +585kB | external +160kB 679 | max: rss +81.26MB | heapTotal +497.03MB | heapUsed +504.60MB | external +160kB 680 | 681 | # @discoveryjs/json-ext v0.6.0 stringifyChunked() 682 | Result: 1000000000 683 | time: 11433 ms 684 | cpu: 11280 ms 685 | mem impact: rss +301.89MB | heapTotal 0 | heapUsed +106kB | external +56 686 | max: rss +9.72MB | heapTotal +588.25MB | heapUsed +596.83MB | external +56 687 | 688 | # @discoveryjs/json-ext v0.5.7 stringifyStream() 689 | Result: 1000000000 690 | time: 12793 ms 691 | cpu: 12551 ms 692 | mem impact: rss +374.57MB | heapTotal -262kB | heapUsed +193kB | external +56 693 | max: rss +62.93MB | heapTotal +595.59MB | heapUsed +592.64MB | external +313kB 694 | 695 | # json-stream-stringify 696 | Result: 1000000000 697 | time: 11983 ms 698 | cpu: 12034 ms 699 | mem impact: rss -12.12MB | heapTotal +262kB | heapUsed +175kB | external +56 700 | max: rss 0 | heapTotal +934kB | heapUsed +14.89MB | external +56 701 | ``` 702 | 703 |
704 | 705 | ## Stringify Info 706 | 707 | Benchmark: `strigify-info.js` 708 | 709 | How to run: 710 | 711 | ``` 712 | node benchmarks/stringify-info [fixture] 713 | ``` 714 | 715 | Where `[fixture]` is number of fixture: 716 | 717 | * `0` – fixture/small.json (~2MB) 718 | * `1` – fixture/medium.json (~13.7MB) 719 | * `2` – fixture/big.json (~100MB) 720 | * `3` – fixture/500mb.json (500MB, auto-generated from big.json x 5 + padding strings) 721 | * `4` – fixture/1gb.json (1gb, auto-generated from big.json x 10 + padding strings) 722 | 723 | ### Time 724 | 725 | 726 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 727 | | -------- | -------: | ----------: | ---------: | ----: | --: | 728 | | JSON.stringify() | 13ms | 54ms | 518ms | 2726ms | ERR_STRING_TOO_LONG | 729 | | @discoveryjs/json-ext stringifyInfo() | 18ms | 34ms | 280ms | 1429ms | 3052ms | 730 | | @discoveryjs/json-ext v0.6.0 stringifyInfo() | 22ms | 49ms | 562ms | 31342ms | 177746ms | 731 | | @discoveryjs/json-ext v0.5.7 stringifyInfo() | 22ms | 48ms | 613ms | 3605ms | 8637ms | 732 | 733 | 734 | ### CPU usage 735 | 736 | 737 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 738 | | -------- | -------: | ----------: | ---------: | ----: | --: | 739 | | JSON.stringify() | 11ms | 47ms | 450ms | 1980ms | ERR_STRING_TOO_LONG | 740 | | @discoveryjs/json-ext stringifyInfo() | 28ms | 49ms | 329ms | 1602ms | 3339ms | 741 | | @discoveryjs/json-ext v0.6.0 stringifyInfo() | 38ms | 61ms | 595ms | 31128ms | 175554ms | 742 | | @discoveryjs/json-ext v0.5.7 stringifyInfo() | 39ms | 60ms | 643ms | 3681ms | 8431ms | 743 | 744 | 745 | ### Max memory usage 746 | 747 | 748 | | Solution | S (~2MB) | M (~13.7MB) | L (~100MB) | 500MB | 1GB | 749 | | -------- | -------: | ----------: | ---------: | ----: | --: | 750 | | JSON.stringify() | 4.30MB | 27.51MB | 210.20MB | 1GB | ERR_STRING_TOO_LONG | 751 | | @discoveryjs/json-ext stringifyInfo() | 1.66MB | 13.06MB | 26.41MB | 64.84MB | 121.28MB | 752 | | @discoveryjs/json-ext v0.6.0 stringifyInfo() | 1.53MB | 1.13MB | 115.32MB | 480.38MB | 968.82MB | 753 | | @discoveryjs/json-ext v0.5.7 stringifyInfo() | 1.42MB | 1MB | 103.87MB | 682.86MB | 1.37GB | 754 | 755 | 756 | ### Output for fixtures 757 | 758 |
759 |
> node benchmarks/stringify-info    # use benchmarks/fixture/small.json (~2MB)
760 | 761 | 762 | ``` 763 | Benchmark: stringifyInfo() (size of JSON.stringify()) 764 | Node version: 22.5.1 765 | Fixture: fixture/small.json 2.08MB 766 | 767 | # JSON.stringify() 768 | Result: 2077471 769 | time: 13 ms 770 | cpu: 11 ms 771 | mem impact: rss +8.03MB | heapTotal +262kB | heapUsed +77kB | external +1kB 772 | max: rss +12.11MB | heapTotal +4.16MB | heapUsed +4.30MB | external +1kB 773 | 774 | # @discoveryjs/json-ext stringifyInfo() 775 | Result: 2077471 776 | time: 18 ms 777 | cpu: 28 ms 778 | mem impact: rss +3.78MB | heapTotal +524kB | heapUsed +237kB | external +1kB 779 | max: rss +3.62MB | heapTotal +262kB | heapUsed +1.66MB | external +1kB 780 | 781 | # @discoveryjs/json-ext v0.6.0 stringifyInfo() 782 | Result: 2077471 783 | time: 22 ms 784 | cpu: 38 ms 785 | mem impact: rss +2.57MB | heapTotal +262kB | heapUsed +227kB | external +1kB 786 | max: rss +2.87MB | heapTotal +688kB | heapUsed +1.52MB | external +1kB 787 | 788 | # @discoveryjs/json-ext v0.5.7 stringifyInfo() 789 | Result: 2077471 790 | time: 22 ms 791 | cpu: 39 ms 792 | mem impact: rss +2.08MB | heapTotal +524kB | heapUsed +232kB | external +1kB 793 | max: rss +2.08MB | heapTotal +508kB | heapUsed +1.42MB | external +1kB 794 | ``` 795 | 796 |
797 | 798 |
799 |
> node benchmarks/stringify-info 1  # use benchmarks/fixture/medium.json (~13.7MB)
800 | 801 | 802 | ``` 803 | Benchmark: stringifyInfo() (size of JSON.stringify()) 804 | Node version: 22.5.1 805 | Fixture: fixture/medium.json 13.69MB 806 | 807 | # JSON.stringify() 808 | Result: 13693865 809 | time: 54 ms 810 | cpu: 47 ms 811 | mem impact: rss +57.46MB | heapTotal 0 | heapUsed +79kB | external +1kB 812 | max: rss +84.79MB | heapTotal +27.39MB | heapUsed +27.51MB | external +1kB 813 | 814 | # @discoveryjs/json-ext stringifyInfo() 815 | Result: 13693865 816 | time: 34 ms 817 | cpu: 49 ms 818 | mem impact: rss +2.59MB | heapTotal +262kB | heapUsed +107kB | external +1kB 819 | max: rss +1.87MB | heapTotal +262kB | heapUsed +13.06MB | external +1kB 820 | 821 | # @discoveryjs/json-ext v0.6.0 stringifyInfo() 822 | Result: 13693865 823 | time: 49 ms 824 | cpu: 61 ms 825 | mem impact: rss +1.11MB | heapTotal +262kB | heapUsed +103kB | external +1kB 826 | max: rss +1.47MB | heapTotal +688kB | heapUsed +1.13MB | external +1kB 827 | 828 | # @discoveryjs/json-ext v0.5.7 stringifyInfo() 829 | Result: 13693865 830 | time: 48 ms 831 | cpu: 60 ms 832 | mem impact: rss +786kB | heapTotal +262kB | heapUsed +108kB | external +1kB 833 | max: rss +983kB | heapTotal +508kB | heapUsed +1.00MB | external +1kB 834 | ``` 835 | 836 |
837 | 838 |
839 |
> node benchmarks/stringify-info 2  # use benchmarks/fixture/big.json (~100MB)
840 | 841 | 842 | ``` 843 | Benchmark: stringifyInfo() (size of JSON.stringify()) 844 | Node version: 22.5.1 845 | Fixture: fixture/big.json 99.95MB 846 | 847 | # JSON.stringify() 848 | Result: 99947225 849 | time: 518 ms 850 | cpu: 450 ms 851 | mem impact: rss +170.98MB | heapTotal 0 | heapUsed +75kB | external +1kB 852 | max: rss +369.48MB | heapTotal +199.90MB | heapUsed +210.20MB | external +1kB 853 | 854 | # @discoveryjs/json-ext stringifyInfo() 855 | Result: 99947225 856 | time: 280 ms 857 | cpu: 329 ms 858 | mem impact: rss +7.91MB | heapTotal +262kB | heapUsed +108kB | external +1kB 859 | max: rss +22.09MB | heapTotal +14.81MB | heapUsed +26.41MB | external +1kB 860 | 861 | # @discoveryjs/json-ext v0.6.0 stringifyInfo() 862 | Result: 99947225 863 | time: 562 ms 864 | cpu: 595 ms 865 | mem impact: rss +50.66MB | heapTotal 0 | heapUsed +100kB | external +1kB 866 | max: rss +113.18MB | heapTotal +106.76MB | heapUsed +115.32MB | external +1kB 867 | 868 | # @discoveryjs/json-ext v0.5.7 stringifyInfo() 869 | Result: 99947225 870 | time: 613 ms 871 | cpu: 643 ms 872 | mem impact: rss +48.63MB | heapTotal +262kB | heapUsed +106kB | external +1kB 873 | max: rss +99.70MB | heapTotal +94.16MB | heapUsed +103.87MB | external +1kB 874 | ``` 875 | 876 |
877 | 878 |
879 |
> node benchmarks/stringify-info 3  # use benchmarks/fixture/500mb.json
880 | 881 | 882 | ``` 883 | Benchmark: stringifyInfo() (size of JSON.stringify()) 884 | Node version: 22.5.1 885 | Fixture: fixture/500mb.json 500MB 886 | 887 | # JSON.stringify() 888 | Result: 500000000 889 | time: 2726 ms 890 | cpu: 1980 ms 891 | mem impact: rss -6.47MB | heapTotal +262kB | heapUsed +75kB | external +1kB 892 | max: rss +925.91MB | heapTotal +1.00GB | heapUsed +1.00GB | external +1kB 893 | 894 | # @discoveryjs/json-ext stringifyInfo() 895 | Result: 500000000 896 | time: 1429 ms 897 | cpu: 1602 ms 898 | mem impact: rss +8.01MB | heapTotal 0 | heapUsed +107kB | external +1kB 899 | max: rss +66.16MB | heapTotal +58.88MB | heapUsed +64.84MB | external +1kB 900 | 901 | # @discoveryjs/json-ext v0.6.0 stringifyInfo() 902 | Result: 500000000 903 | time: 31342 ms 904 | cpu: 31128 ms 905 | mem impact: rss +221.92MB | heapTotal 0 | heapUsed +100kB | external +1kB 906 | max: rss +481.59MB | heapTotal +475.35MB | heapUsed +480.37MB | external +1kB 907 | 908 | # @discoveryjs/json-ext v0.5.7 stringifyInfo() 909 | Result: 500000000 910 | time: 3605 ms 911 | cpu: 3681 ms 912 | mem impact: rss +225.82MB | heapTotal +262kB | heapUsed +105kB | external +1kB 913 | max: rss +687.59MB | heapTotal +681.67MB | heapUsed +682.86MB | external +1kB 914 | ``` 915 | 916 |
917 | 918 |
919 |
> node benchmarks/stringify-info 4  # use benchmarks/fixture/1gb.json
920 | 921 | 922 | ``` 923 | Benchmark: stringifyInfo() (size of JSON.stringify()) 924 | Node version: 22.5.1 925 | Fixture: fixture/1gb.json 0 926 | 927 | # JSON.stringify() 928 | RangeError: Invalid string length 929 | at JSON.stringify () 930 | at JSON.stringify() (../json-ext/benchmarks/stringify-info.js:45:32) 931 | at tests. (../json-ext/benchmarks/stringify-info.js:65:21) 932 | at benchmark (../json-ext/benchmarks/benchmark-utils.js:65:28) 933 | at async ../json-ext/benchmarks/run-test.js:7:17 934 | 935 | # @discoveryjs/json-ext stringifyInfo() 936 | Result: 1000000000 937 | time: 3052 ms 938 | cpu: 3339 ms 939 | mem impact: rss +6.90MB | heapTotal 0 | heapUsed +106kB | external +1kB 940 | max: rss +124.19MB | heapTotal +117.62MB | heapUsed +121.28MB | external +1kB 941 | 942 | # @discoveryjs/json-ext v0.6.0 stringifyInfo() 943 | Result: 1000000000 944 | time: 177746 ms 945 | cpu: 175554 ms 946 | mem impact: rss +15.73MB | heapTotal 0 | heapUsed +100kB | external +1kB 947 | max: rss 0 | heapTotal +966.36MB | heapUsed +968.82MB | external +1kB 948 | 949 | # @discoveryjs/json-ext v0.5.7 stringifyInfo() 950 | Result: 1000000000 951 | time: 8637 ms 952 | cpu: 8431 ms 953 | mem impact: rss +29.08MB | heapTotal +262kB | heapUsed +105kB | external +1kB 954 | max: rss +968.85MB | heapTotal +1.37GB | heapUsed +1.37GB | external +1kB 955 | ``` 956 | 957 |
958 | -------------------------------------------------------------------------------- /benchmarks/benchmark-utils.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import url from 'node:url'; 4 | import { fork } from 'node:child_process'; 5 | import { createRequire } from 'node:module'; 6 | import chalk from 'chalk'; 7 | 8 | const ANSI_REGEXP = /([\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><])/g; 9 | const require = createRequire(import.meta.url); 10 | const __main = require.resolve(process.argv[1]); 11 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 12 | 13 | export function isMain(meta) { 14 | const modulePath = url.fileURLToPath(meta.url); 15 | 16 | return modulePath === __main; 17 | } 18 | 19 | export function runBenchmark(name, argv = process.argv.slice(2)) { 20 | return new Promise((resolve, reject) => { 21 | const child = fork(__dirname + '/run-test.js', [ 22 | __main, // require.main.filename 23 | name, 24 | ...argv 25 | ], { 26 | stdio: ['inherit', 'pipe', 'pipe', 'ipc'], 27 | execArgv: ['--expose-gc'], 28 | env: { 29 | ...process.env, 30 | FORCE_COLOR: chalk.supportsColor ? chalk.supportsColor.level : 0 31 | } 32 | }) 33 | .on('message', resolve) 34 | .on('error', reject) 35 | .on('close', code => code ? reject(new Error('Exit code ' + code)) : resolve()); 36 | 37 | child.stdout.pipe(process.stdout); 38 | child.stderr.pipe(process.stderr); 39 | }); 40 | } 41 | 42 | function sanitizeErrorOutput(error) { 43 | const home = path.join(__dirname, '../..'); 44 | const rx = new RegExp('(?:file://)?' + home.replace(/\[\]\(\)\{\}\.\+\*\?/g, '\\$1'), 'g'); 45 | const text = String(error.stack || error); 46 | 47 | return home ? text.replace(rx, '..') : text; 48 | } 49 | 50 | export async function benchmark(name, fn, beforeFn, output = true) { 51 | const data = typeof beforeFn === 'function' ? await beforeFn() : undefined; 52 | 53 | await collectGarbage(); 54 | 55 | const mem = traceMem(10); 56 | const startCpu = process.cpuUsage(); 57 | const startTime = Date.now(); 58 | 59 | try { 60 | if (output) { 61 | console.log('#', chalk.cyan(name)); 62 | } 63 | 64 | // run test and catch a result 65 | let result = await fn(data); 66 | 67 | // compute metrics 68 | const time = Date.now() - startTime; 69 | const cpu = parseInt(process.cpuUsage(startCpu).user / 1000); 70 | const currentMem = mem.stop(); 71 | const maxMem = memDelta(mem.base, mem.max); 72 | 73 | if (output) { 74 | console.log('time:', time, 'ms'); 75 | console.log('cpu:', cpu, 'ms'); 76 | } 77 | 78 | await collectGarbage(); 79 | 80 | if (output) { 81 | console.log('mem impact: ', String(memDelta(currentMem.base))); 82 | console.log(' max: ', String(maxMem)); 83 | console.log(); 84 | } 85 | 86 | // release mem 87 | // eslint-disable-next-line no-unused-vars 88 | result = null; 89 | await collectGarbage(); 90 | 91 | // fs.writeFileSync(outputPath('mem-' + name), JSON.stringify(mem.series())); 92 | 93 | return { 94 | name, 95 | time, 96 | cpu, 97 | rss: maxMem.delta.rss, 98 | heapTotal: maxMem.delta.heapTotal, 99 | heapUsed: maxMem.delta.heapUsed, 100 | external: maxMem.delta.external, 101 | arrayBuffers: maxMem.delta.arrayBuffers 102 | }; 103 | } catch (e) { 104 | mem.stop(); 105 | 106 | if (output) { 107 | console.error(sanitizeErrorOutput(e)); 108 | console.error(); 109 | } 110 | 111 | let code = e.message === 'Invalid string length' ? 'ERR_STRING_TOO_LONG' : e.code || false; 112 | 113 | return { 114 | name, 115 | error: e.name + ': ' + e.message, 116 | code 117 | }; 118 | } 119 | } 120 | 121 | function stripAnsi(str) { 122 | return str.replace(ANSI_REGEXP, ''); 123 | } 124 | 125 | export function prettySize(size, options) { 126 | const unit = ['', 'kB', 'MB', 'GB']; 127 | const { signed, pad, preserveZero } = options || {}; 128 | 129 | while (Math.abs(size) > 1000) { 130 | size /= 1000; 131 | unit.shift(); 132 | } 133 | 134 | return ( 135 | (signed && size > 0 ? '+' : '') + 136 | size.toFixed(unit.length > 2 ? 0 : 2).replace(/\.0+$/, preserveZero ? '$&' : '') + 137 | unit[0] 138 | ).padStart(pad || 0); 139 | } 140 | 141 | export function memDelta(_base, cur, skip = ['arrayBuffers']) { 142 | const current = cur || process.memoryUsage(); 143 | const delta = {}; 144 | const base = { ..._base }; 145 | 146 | for (const [k, v] of Object.entries(current)) { 147 | base[k] = base[k] || 0; 148 | delta[k] = v - base[k]; 149 | } 150 | 151 | return { 152 | base, 153 | current, 154 | delta, 155 | toString() { 156 | const res = []; 157 | 158 | for (const [k, v] of Object.entries(delta)) { 159 | if (skip.includes(k)) { 160 | continue; 161 | } 162 | 163 | const rel = _base && k in _base; 164 | res.push(`${k} ${(rel && v > 0 ? chalk.yellow : chalk.green)(prettySize(v, { signed: rel, pad: 9, preserveZero: true }))}`); 165 | } 166 | 167 | return res.join(' | ') || 'No changes'; 168 | } 169 | }; 170 | } 171 | 172 | export async function timeout(ms) { 173 | await new Promise(resolve => setTimeout(resolve, ms)); 174 | } 175 | 176 | export function traceMem(resolutionMs, sample = false) { 177 | const base = process.memoryUsage(); 178 | const max = { ...base }; 179 | const startTime = Date.now(); 180 | const samples = []; 181 | const takeSample = () => { 182 | const mem = process.memoryUsage(); 183 | 184 | if (sample) { 185 | samples.push({ 186 | time: Date.now() - startTime, 187 | mem 188 | }); 189 | } 190 | 191 | for (let key in base) { 192 | if (max[key] < mem[key]) { 193 | max[key] = mem[key]; 194 | } 195 | } 196 | }; 197 | const timer = setInterval( 198 | takeSample, 199 | isFinite(resolutionMs) && parseInt(resolutionMs) > 0 ? parseInt(resolutionMs) : 16 200 | ); 201 | 202 | return { 203 | base, 204 | max, 205 | get current() { 206 | return memDelta(base); 207 | }, 208 | series(abs) { 209 | const keys = Object.keys(base); 210 | const series = {}; 211 | 212 | for (const key of keys) { 213 | series[key] = { 214 | name: key, 215 | data: new Array(samples.length) 216 | }; 217 | } 218 | 219 | for (let i = 0; i < samples.length; i++) { 220 | const sample = samples[i]; 221 | 222 | for (const key of keys) { 223 | series[key].data[i] = abs 224 | ? sample.mem[key] || 0 225 | : sample.mem[key] ? sample.mem[key] - base[key] : 0; 226 | } 227 | } 228 | 229 | return { 230 | time: samples.map(s => s.time), 231 | series: Object.values(series) 232 | }; 233 | }, 234 | stop() { 235 | clearInterval(timer); 236 | takeSample(); 237 | return memDelta(base); 238 | } 239 | }; 240 | } 241 | 242 | let exposeGcShowed = false; 243 | export async function collectGarbage() { 244 | if (typeof global.gc === 'function') { 245 | global.gc(); 246 | 247 | // double sure 248 | await timeout(100); 249 | global.gc(); 250 | } else if (!exposeGcShowed) { 251 | exposeGcShowed = true; 252 | console.warn(chalk.magenta('Looks like script is forcing GC to collect garbage, but corresponding API is not enabled')); 253 | console.warn(chalk.magenta('Run node with --expose-gc flag to enable API and get precise measurements')); 254 | } 255 | } 256 | 257 | function captureStdio(stream, buffer) { 258 | const oldWrite = stream.write; 259 | 260 | stream.write = (chunk, encoding, fd) => { 261 | buffer.push(chunk); 262 | return oldWrite.call(stream, chunk, encoding, fd); 263 | }; 264 | 265 | return () => stream.write = oldWrite; 266 | } 267 | 268 | export function captureOutput(callback) { 269 | let buffer = []; 270 | const cancelCapture = () => captures.forEach(fn => fn()); 271 | const captures = [ 272 | captureStdio(process.stdout, buffer), 273 | captureStdio(process.stderr, buffer) 274 | ]; 275 | 276 | process.once('exit', () => { 277 | cancelCapture(); 278 | callback(buffer.join('')); 279 | buffer = null; 280 | }); 281 | 282 | return cancelCapture; 283 | } 284 | 285 | export function replaceInReadme(start, end, replace) { 286 | const filename = path.join(__dirname, '/README.md'); 287 | const content = fs.readFileSync(filename, 'utf8'); 288 | const mstart = content.match(start); 289 | 290 | if (!mstart) { 291 | throw new Error('No start offset found'); 292 | } 293 | 294 | const startOffset = mstart.index + mstart[0].length; 295 | const endRegExp = new RegExp(end, (end.flags || '').replace('g', '') + 'g'); 296 | endRegExp.lastIndex = startOffset; 297 | const mend = endRegExp.exec(content); 298 | 299 | if (!mend) { 300 | throw new Error('No end offset found'); 301 | } 302 | 303 | const endOffset = mend.index; 304 | 305 | fs.writeFileSync(filename, 306 | content.slice(0, startOffset) + 307 | (typeof replace === 'function' ? replace(content.slice(startOffset, endOffset)) : replace) + 308 | content.slice(endOffset), 'utf8'); 309 | } 310 | 311 | export function outputToReadme(benchmarkName, fixtureIndex) { 312 | captureOutput(output => replaceInReadme( 313 | new RegExp(``), 314 | new RegExp(``), 315 | '\n\n```\n' + stripAnsi(output || '').trim() + '\n```\n' 316 | )); 317 | } 318 | 319 | export function updateReadmeTable(benchmarkName, fixtureIndex, fixtures, results) { 320 | for (const type of ['time', 'cpu', 'memory']) { 321 | replaceInReadme( 322 | new RegExp(``), 323 | new RegExp(``), 324 | content => { 325 | const lines = content.trim().split(/\n/); 326 | const current = Object.create(null); 327 | const newValues = Object.fromEntries(results.filter(Boolean).map(item => 328 | [item.name, item.error 329 | ? item.code || 'ERROR' 330 | : type === 'memory' 331 | ? prettySize(item.heapUsed + item.external) 332 | : item[type] + 'ms' 333 | ] 334 | )); 335 | 336 | for (const line of lines.slice(2)) { 337 | const cells = line.trim().replace(/^\|\s*|\s*\|$/g, '').split(/\s*\|\s*/); 338 | current[cells[0]] = cells.slice(1); 339 | } 340 | 341 | for (const [k, v] of Object.entries(newValues)) { 342 | if (k in current === false) { 343 | current[k] = []; 344 | } 345 | current[k][fixtureIndex] = v; 346 | } 347 | 348 | // normalize 349 | for (const array of Object.values(current)) { 350 | for (let i = 0; i < fixtures.length; i++) { 351 | if (!array[i]) { 352 | array[i] = '–'; 353 | } 354 | } 355 | } 356 | 357 | return '\n' + [ 358 | ...lines.slice(0, 2), 359 | ...Object.entries(current).map(([k, v]) => '| ' + [k, ...v].join(' | ') + ' |') 360 | ].join('\n') + '\n'; 361 | } 362 | ); 363 | } 364 | } 365 | 366 | export function getSelfPackageJson() { 367 | return JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'))); 368 | } 369 | -------------------------------------------------------------------------------- /benchmarks/fixture/.gitignore: -------------------------------------------------------------------------------- 1 | *mb.json 2 | *gb.json 3 | -------------------------------------------------------------------------------- /benchmarks/gen-fixture.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import url from 'node:url'; 4 | import { finished } from 'node:stream/promises'; 5 | import { isMain } from './benchmark-utils.js'; 6 | 7 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 8 | const pattern = __dirname + '/fixture/big.json'; 9 | 10 | export async function genFixture(times, stream) { 11 | const size = fs.statSync(pattern).size; 12 | const padString = 'x'.repeat((1e8 - size) - 2 /* , */ - 2 /* "" */); 13 | 14 | console.error( 15 | 'Generate', 16 | times < 10 ? `${times}00mb` : `${(times / 10).toFixed(1).replace(/\.0$/, '')}gb`, 17 | 'fixture' 18 | ); 19 | 20 | if (typeof stream === 'string') { 21 | stream = fs.createWriteStream(stream); 22 | } 23 | 24 | stream.write('['); 25 | 26 | for (let i = 0; i < times * 2; i++) { 27 | if (i > 0) { 28 | stream.write(','); 29 | } 30 | 31 | if (i % 2) { 32 | stream.write('"' + (i === 1 ? padString.slice(1) : padString) + '"'); 33 | } else { 34 | for await (let chunk of fs.createReadStream(pattern)) { 35 | stream.write(chunk); 36 | } 37 | } 38 | } 39 | 40 | await finished( 41 | stream.end(']') 42 | ); 43 | 44 | return stream.bytesWritten; 45 | }; 46 | 47 | if (isMain(import.meta)) { 48 | const times = Math.max(parseInt(process.argv[2] || 5) || 5, 1); 49 | genFixture(times, process.stdout); 50 | } 51 | -------------------------------------------------------------------------------- /benchmarks/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "benchmarks", 9 | "dependencies": { 10 | "bfj": "^8.0.0", 11 | "chalk": "^4.1.0", 12 | "json-ext-0.5.7": "npm:@discoveryjs/json-ext@0.5.7", 13 | "json-ext-0.6.0": "npm:@discoveryjs/json-ext@0.6.0", 14 | "json-stream-stringify": "^3.1.4" 15 | } 16 | }, 17 | "node_modules/@types/color-name": { 18 | "version": "1.1.1", 19 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 20 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" 21 | }, 22 | "node_modules/ansi-styles": { 23 | "version": "4.2.1", 24 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 25 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 26 | "dependencies": { 27 | "@types/color-name": "^1.1.1", 28 | "color-convert": "^2.0.1" 29 | }, 30 | "engines": { 31 | "node": ">=8" 32 | }, 33 | "funding": { 34 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 35 | } 36 | }, 37 | "node_modules/bfj": { 38 | "version": "8.0.0", 39 | "resolved": "https://registry.npmjs.org/bfj/-/bfj-8.0.0.tgz", 40 | "integrity": "sha512-6KJe4gFrZ4lhmvWcUIj37yFAs36mi2FZXuTkw6udZ/QsX/znFypW4SatqcLA5K5T4BAWgJZD73UFEJJQxuJjoA==", 41 | "dependencies": { 42 | "bluebird": "^3.7.2", 43 | "check-types": "^11.2.3", 44 | "hoopy": "^0.1.4", 45 | "jsonpath": "^1.1.1", 46 | "tryer": "^1.0.1" 47 | }, 48 | "engines": { 49 | "node": ">= 18.0.0" 50 | } 51 | }, 52 | "node_modules/bluebird": { 53 | "version": "3.7.2", 54 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 55 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 56 | }, 57 | "node_modules/chalk": { 58 | "version": "4.1.0", 59 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 60 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 61 | "dependencies": { 62 | "ansi-styles": "^4.1.0", 63 | "supports-color": "^7.1.0" 64 | }, 65 | "engines": { 66 | "node": ">=10" 67 | }, 68 | "funding": { 69 | "url": "https://github.com/chalk/chalk?sponsor=1" 70 | } 71 | }, 72 | "node_modules/check-types": { 73 | "version": "11.2.3", 74 | "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", 75 | "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" 76 | }, 77 | "node_modules/color-convert": { 78 | "version": "2.0.1", 79 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 80 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 81 | "dependencies": { 82 | "color-name": "~1.1.4" 83 | }, 84 | "engines": { 85 | "node": ">=7.0.0" 86 | } 87 | }, 88 | "node_modules/color-name": { 89 | "version": "1.1.4", 90 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 91 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 92 | }, 93 | "node_modules/deep-is": { 94 | "version": "0.1.4", 95 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 96 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" 97 | }, 98 | "node_modules/escodegen": { 99 | "version": "1.14.3", 100 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", 101 | "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", 102 | "dependencies": { 103 | "esprima": "^4.0.1", 104 | "estraverse": "^4.2.0", 105 | "esutils": "^2.0.2", 106 | "optionator": "^0.8.1" 107 | }, 108 | "bin": { 109 | "escodegen": "bin/escodegen.js", 110 | "esgenerate": "bin/esgenerate.js" 111 | }, 112 | "engines": { 113 | "node": ">=4.0" 114 | }, 115 | "optionalDependencies": { 116 | "source-map": "~0.6.1" 117 | } 118 | }, 119 | "node_modules/escodegen/node_modules/esprima": { 120 | "version": "4.0.1", 121 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 122 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 123 | "bin": { 124 | "esparse": "bin/esparse.js", 125 | "esvalidate": "bin/esvalidate.js" 126 | }, 127 | "engines": { 128 | "node": ">=4" 129 | } 130 | }, 131 | "node_modules/esprima": { 132 | "version": "1.2.2", 133 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", 134 | "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", 135 | "bin": { 136 | "esparse": "bin/esparse.js", 137 | "esvalidate": "bin/esvalidate.js" 138 | }, 139 | "engines": { 140 | "node": ">=0.4.0" 141 | } 142 | }, 143 | "node_modules/estraverse": { 144 | "version": "4.3.0", 145 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 146 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 147 | "engines": { 148 | "node": ">=4.0" 149 | } 150 | }, 151 | "node_modules/esutils": { 152 | "version": "2.0.3", 153 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 154 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 155 | "engines": { 156 | "node": ">=0.10.0" 157 | } 158 | }, 159 | "node_modules/fast-levenshtein": { 160 | "version": "2.0.6", 161 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 162 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" 163 | }, 164 | "node_modules/has-flag": { 165 | "version": "4.0.0", 166 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 167 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 168 | "engines": { 169 | "node": ">=8" 170 | } 171 | }, 172 | "node_modules/hoopy": { 173 | "version": "0.1.4", 174 | "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", 175 | "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", 176 | "engines": { 177 | "node": ">= 6.0.0" 178 | } 179 | }, 180 | "node_modules/json-ext-0.5.7": { 181 | "name": "@discoveryjs/json-ext", 182 | "version": "0.5.7", 183 | "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", 184 | "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", 185 | "license": "MIT", 186 | "engines": { 187 | "node": ">=10.0.0" 188 | } 189 | }, 190 | "node_modules/json-ext-0.6.0": { 191 | "name": "@discoveryjs/json-ext", 192 | "version": "0.6.0", 193 | "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.0.tgz", 194 | "integrity": "sha512-ggk8A6Y4RxpOPaCER/yl1sYtcZ1JfFBdHR6it/e2IolmV3VXSaFov2hqmWUdh9dXgpMprSg3xUvkGJDfR5sc/w==", 195 | "license": "MIT", 196 | "engines": { 197 | "node": ">=14.17.0" 198 | } 199 | }, 200 | "node_modules/json-stream-stringify": { 201 | "version": "3.1.4", 202 | "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.4.tgz", 203 | "integrity": "sha512-oGoz05ft577LolnXFQHD2CjnXDxXVA5b8lHwfEZgRXQUZeCMo6sObQQRq+NXuHQ3oTeMZHHmmPY2rjVwyqR62A==", 204 | "engines": { 205 | "node": ">=7.10.1" 206 | } 207 | }, 208 | "node_modules/jsonpath": { 209 | "version": "1.1.1", 210 | "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", 211 | "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", 212 | "dependencies": { 213 | "esprima": "1.2.2", 214 | "static-eval": "2.0.2", 215 | "underscore": "1.12.1" 216 | } 217 | }, 218 | "node_modules/levn": { 219 | "version": "0.3.0", 220 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 221 | "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", 222 | "dependencies": { 223 | "prelude-ls": "~1.1.2", 224 | "type-check": "~0.3.2" 225 | }, 226 | "engines": { 227 | "node": ">= 0.8.0" 228 | } 229 | }, 230 | "node_modules/optionator": { 231 | "version": "0.8.3", 232 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 233 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 234 | "dependencies": { 235 | "deep-is": "~0.1.3", 236 | "fast-levenshtein": "~2.0.6", 237 | "levn": "~0.3.0", 238 | "prelude-ls": "~1.1.2", 239 | "type-check": "~0.3.2", 240 | "word-wrap": "~1.2.3" 241 | }, 242 | "engines": { 243 | "node": ">= 0.8.0" 244 | } 245 | }, 246 | "node_modules/prelude-ls": { 247 | "version": "1.1.2", 248 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 249 | "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", 250 | "engines": { 251 | "node": ">= 0.8.0" 252 | } 253 | }, 254 | "node_modules/source-map": { 255 | "version": "0.6.1", 256 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 257 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 258 | "optional": true, 259 | "engines": { 260 | "node": ">=0.10.0" 261 | } 262 | }, 263 | "node_modules/static-eval": { 264 | "version": "2.0.2", 265 | "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", 266 | "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", 267 | "dependencies": { 268 | "escodegen": "^1.8.1" 269 | } 270 | }, 271 | "node_modules/supports-color": { 272 | "version": "7.1.0", 273 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 274 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 275 | "dependencies": { 276 | "has-flag": "^4.0.0" 277 | }, 278 | "engines": { 279 | "node": ">=8" 280 | } 281 | }, 282 | "node_modules/tryer": { 283 | "version": "1.0.1", 284 | "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", 285 | "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" 286 | }, 287 | "node_modules/type-check": { 288 | "version": "0.3.2", 289 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 290 | "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", 291 | "dependencies": { 292 | "prelude-ls": "~1.1.2" 293 | }, 294 | "engines": { 295 | "node": ">= 0.8.0" 296 | } 297 | }, 298 | "node_modules/underscore": { 299 | "version": "1.12.1", 300 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", 301 | "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" 302 | }, 303 | "node_modules/word-wrap": { 304 | "version": "1.2.5", 305 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 306 | "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 307 | "engines": { 308 | "node": ">=0.10.0" 309 | } 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchmarks", 3 | "type": "module", 4 | "scripts": { 5 | "test": "echo \"Error: no test specified\" && exit 1" 6 | }, 7 | "dependencies": { 8 | "bfj": "^8.0.0", 9 | "chalk": "^4.1.0", 10 | "json-stream-stringify": "^3.1.4", 11 | "json-ext-0.5.7": "npm:@discoveryjs/json-ext@0.5.7", 12 | "json-ext-0.6.0": "npm:@discoveryjs/json-ext@0.6.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /benchmarks/parse-chunked.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import url from 'node:url'; 4 | import chalk from 'chalk'; 5 | import bfj from 'bfj'; 6 | import { parseChunked, parseFromWebStream } from '../src/index.js'; 7 | import { runBenchmark, prettySize, outputToReadme, updateReadmeTable, getSelfPackageJson, isMain } from './benchmark-utils.js'; 8 | 9 | const benchmarkName = 'parse-chunked'; 10 | const fixtures = [ 11 | './fixture/small.json', 12 | './fixture/medium.json', 13 | './fixture/big.json', 14 | './fixture/500mb.json', // 3 | auto-generate from big.json 15 | './fixture/1gb.json' // 4 | auto-generate from big.json 16 | ]; 17 | const selfPackageJson = getSelfPackageJson(); 18 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 19 | const fixtureIndex = process.argv[2] || 0; 20 | const filename = fixtureIndex in fixtures ? path.join(__dirname, fixtures[fixtureIndex]) : false; 21 | let filesize = fs.existsSync(filename) ? fs.statSync(filename).size : 0; 22 | 23 | if (!filename) { 24 | console.error('Fixture is not selected!'); 25 | console.error(); 26 | console.error('Run script:', chalk.green(`node ${path.relative(process.cwd(), process.argv[1])} [fixture]`)); 27 | console.error(); 28 | console.error(`where ${chalk.yellow('[fixture]')} is a number:`); 29 | fixtures.forEach((fixture, idx) => 30 | console.log(idx, fixture) 31 | ); 32 | process.exit(); 33 | } 34 | 35 | const chunkSize = 512 * 1024; // chunk size for generator 36 | export const tests = { 37 | 'JSON.parse()': () => 38 | JSON.parse(fs.readFileSync(filename)), 39 | 40 | [selfPackageJson.name + ' parseChunked(fs.createReadStream())']: () => 41 | parseChunked(fs.createReadStream(filename, { highWaterMark: chunkSize })), 42 | 43 | [selfPackageJson.name + ' parseChunked(fs.readFileSync())']: () => 44 | parseChunked(function*() { 45 | let json = fs.readFileSync(filename); 46 | for (let i = 0; i < json.length; i += chunkSize) { 47 | yield json.subarray(i, i + chunkSize); 48 | } 49 | }), 50 | 51 | [selfPackageJson.name + ' parseFromWebStream()']: () => 52 | parseFromWebStream(ReadableStream.from(fs.createReadStream(filename, { highWaterMark: chunkSize }))), 53 | 54 | 'bfj': () => sizeLessThan(500 * 1024 * 1024) && 55 | bfj.parse(fs.createReadStream(filename)) 56 | }; 57 | 58 | if (isMain(import.meta)) { 59 | run(); 60 | } 61 | 62 | function sizeLessThan(limit) { 63 | if (filesize < limit) { 64 | return true; 65 | } 66 | 67 | const error = new Error('Run takes too long time'); 68 | error.code = 'ERR_RUN_TOO_LONG'; 69 | throw error; 70 | } 71 | 72 | // 73 | // Run benchmarks 74 | // 75 | async function run() { 76 | if (!fs.existsSync(filename)) { 77 | // auto-generate fixture 78 | let [, num, unit] = filename.match(/(\d+)([a-z]+).json/); 79 | const times = unit === 'mb' ? Math.round(num / 100) : num * 10; 80 | const { genFixture } = await import('./gen-fixture.js'); 81 | 82 | filesize = await genFixture(times, filename); 83 | } 84 | 85 | if (process.env.README) { 86 | outputToReadme(benchmarkName, fixtureIndex); 87 | } 88 | 89 | console.log('Benchmark:', chalk.green('parseChunked()'), '(parse chunked JSON)'); 90 | console.log('Node version:', chalk.green(process.versions.node)); 91 | console.log('Fixture:', 92 | chalk.green(path.relative(process.cwd(), filename)), 93 | chalk.yellow(prettySize(fs.statSync(filename).size)), 94 | '/ chunk size', 95 | chalk.yellow(prettySize(chunkSize)) 96 | ); 97 | console.log(); 98 | 99 | const results = []; 100 | for (const name of Object.keys(tests)) { 101 | results.push(await runBenchmark(name) || { name, error: true, code: 'CRASH' }); 102 | } 103 | 104 | if (process.env.README) { 105 | updateReadmeTable(benchmarkName, fixtureIndex, fixtures, results); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /benchmarks/run-test.js: -------------------------------------------------------------------------------- 1 | import { benchmark } from './benchmark-utils.js'; 2 | const [,, testModule, test, ...rest] = process.argv; 3 | 4 | process.argv = [process.argv[0], testModule, ...rest]; 5 | 6 | import(testModule).then(async ({ tests }) => { 7 | const res = await benchmark(test, tests[test], tests.__getData); 8 | 9 | if (typeof process.send === 'function') { 10 | process.send(res); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /benchmarks/stringify-chunked-conformance.js: -------------------------------------------------------------------------------- 1 | import { inspect } from 'node:util'; 2 | import { Readable } from 'node:stream'; 3 | import chalk from 'chalk'; 4 | import bfj from 'bfj'; 5 | import { JsonStreamStringify } from 'json-stream-stringify'; 6 | import * as jsonExt from '../src/index.js'; 7 | import { tests, fixture, spaces, allUtf8LengthDiffChars, replacerTests } from '../src/stringify-cases.js'; 8 | import { getSelfPackageJson } from './benchmark-utils.js'; 9 | 10 | const selfPackageJson = getSelfPackageJson(); 11 | 12 | function escape(s) { 13 | s = s === allUtf8LengthDiffChars 14 | ? `All UTF8 length diff chars ${s[0]}..${s[s.length - 1]}` 15 | : inspect(s, { depth: null }); 16 | 17 | return s.replace(/[\u0000-\u001f\u0100-\uffff]/g, m => '\\u' + m.charCodeAt().toString(16).padStart(4, '0')); 18 | } 19 | 20 | function stringify(createStream, value, replacer, space) { 21 | const chunks = []; 22 | let uncaughtExceptionFn; 23 | 24 | return new Promise((resolve, reject) => { 25 | process.on('uncaughtException', uncaughtExceptionFn = (e) => reject(e)); 26 | createStream(value, replacer, space) 27 | .on('error', reject) 28 | .on('data', chunk => chunks.push(chunk)) 29 | .on('end', () => resolve(chunks.join(''))); 30 | }) 31 | .finally(() => process.off('uncaughtException', uncaughtExceptionFn)); 32 | } 33 | 34 | async function test(createStream, value, replacer, space) { 35 | try { 36 | const expected = JSON.stringify(value, replacer, space); 37 | const actual = await stringify(createStream, value, replacer, space); 38 | 39 | return { value, actual, expected, success: actual === expected }; 40 | } catch (error) { 41 | return { value, error, success: false }; 42 | } 43 | } 44 | 45 | const streamFactories = { 46 | [selfPackageJson.name + '/strigifyChunked']: (value, replacer, space) => Readable.from(jsonExt.stringifyChunked(value, replacer, space)), 47 | 'bfj': (value, replacer, space) => bfj.streamify(value, { space }), 48 | 'json-stream-stringify': (value, replacer, space) => new JsonStreamStringify(value, replacer, space) 49 | }; 50 | 51 | async function run() { 52 | const testCount = tests.length + spaces.length + replacerTests.length; 53 | 54 | for (const [name, createStream] of Object.entries(streamFactories)) { 55 | let failures = 0; 56 | 57 | for (const value of tests) { 58 | const { success, error, actual } = await test(createStream, value); 59 | 60 | if (!success) { 61 | failures++; 62 | if (process.env.VERBOSE) { 63 | if (error) { 64 | console.error(name, 'EXCEPTION', value); 65 | console.error(' error:', error); 66 | console.error(); 67 | } else { 68 | console.error(name, 'FAILED', value); 69 | console.error(' result:', actual === '' ? '' : actual[0] === '"' ? escape(actual) : actual); 70 | console.error(); 71 | } 72 | } 73 | } 74 | } 75 | 76 | for (const space of spaces) { 77 | const { success } = await test(createStream, fixture, undefined, space); 78 | 79 | if (!success) { 80 | failures++; 81 | if (process.env.VERBOSE) { 82 | console.error(name, 'SPACE FAILED', JSON.stringify(space)); 83 | console.error(); 84 | } 85 | } 86 | } 87 | 88 | for (const [input, replacer] of replacerTests) { 89 | const { success, actual, expected } = await test(createStream, input, replacer); 90 | 91 | if (!success) { 92 | failures++; 93 | if (process.env.VERBOSE) { 94 | console.error(name, 'REPLACER FAILED', expected); 95 | console.error(' result:', actual === '' ? '' : actual[0] === '"' ? escape(actual) : actual); 96 | console.error(); 97 | } 98 | } 99 | } 100 | 101 | console.log(chalk.cyan(name), 102 | failures === 0 103 | ? chalk.green('PASSED') 104 | : chalk.white.bgRed('FAILED') + ` ${failures}/${testCount}` 105 | ); 106 | } 107 | } 108 | 109 | run(); 110 | -------------------------------------------------------------------------------- /benchmarks/stringify-chunked.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import url from 'node:url'; 4 | import chalk from 'chalk'; 5 | import bfj from 'bfj'; 6 | import { JsonStreamStringify } from 'json-stream-stringify'; 7 | import * as jsonExt057 from 'json-ext-0.5.7'; 8 | import * as jsonExt060 from 'json-ext-0.6.0'; 9 | import * as jsonExt from '../src/index.js'; 10 | import { 11 | runBenchmark, 12 | prettySize, 13 | outputToReadme, 14 | updateReadmeTable, 15 | getSelfPackageJson, 16 | isMain 17 | } from './benchmark-utils.js'; 18 | 19 | const selfPackageJson = getSelfPackageJson(); 20 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 21 | const benchmarkName = 'stringify-chunked'; 22 | // const outputPath = name => __dirname + '/tmp/stringify-chunked-' + name.replace(/[@\/]/g, '-').replace(/\s*\(.+$/, '') + '.json'; 23 | const fixtures = [ 24 | 'fixture/small.json', // ~2,1MB 25 | 'fixture/medium.json', // ~13,7MB 26 | 'fixture/big.json', // ~100Mb 27 | './fixture/500mb.json', // 3 | auto-generate from big.json 28 | './fixture/1gb.json' // 4 | auto-generate from big.json 29 | ]; 30 | const fixtureIndex = process.argv[2] || 0; 31 | const filename = fixtureIndex in fixtures ? path.join(__dirname, fixtures[fixtureIndex]) : false; 32 | let filesize = fs.existsSync(filename) ? fs.statSync(filename).size : 0; 33 | 34 | if (!filename) { 35 | console.error('Fixture is not selected!'); 36 | console.error(); 37 | console.error('Run script:', chalk.green(`node ${path.relative(process.cwd(), process.argv[1])} [fixture]`)); 38 | console.error(); 39 | console.error(`where ${chalk.yellow('[fixture]')} is a number:`); 40 | fixtures.forEach((fixture, idx) => 41 | console.log(idx, fixture) 42 | ); 43 | process.exit(); 44 | } 45 | 46 | function sizeLessThan(limit) { 47 | if (filesize < limit) { 48 | return true; 49 | } 50 | 51 | const error = new Error('Run takes too long time'); 52 | error.code = 'ERR_RUN_TOO_LONG'; 53 | throw error; 54 | } 55 | 56 | export const tests = { 57 | 'JSON.stringify()': data => 58 | [JSON.stringify(data)], 59 | 60 | [selfPackageJson.name + ' stringifyChunked()']: data => 61 | jsonExt.stringifyChunked(data), 62 | 63 | [selfPackageJson.name + ' createStringifyWebStream()']: data => 64 | jsonExt.createStringifyWebStream(data), 65 | 66 | [selfPackageJson.name + ' v0.6.0 stringifyChunked()']: data => 67 | jsonExt060.stringifyChunked(data), 68 | 69 | [selfPackageJson.name + ' v0.5.7 stringifyStream()']: data => 70 | jsonExt057.default.stringifyStream(data), 71 | 72 | 'json-stream-stringify': data => 73 | new JsonStreamStringify(data), 74 | 75 | 'bfj': data => sizeLessThan(100 * 1024 * 1024) && 76 | bfj.streamify(data) 77 | }; 78 | 79 | Object.defineProperty(tests, '__getData', { 80 | value: () => jsonExt.parseChunked(fs.createReadStream(filename)) 81 | }); 82 | 83 | for (const [name, init] of Object.entries(tests)) { 84 | tests[name] = async (data) => { 85 | let len = 0; 86 | for await (const chunk of init(data)) { 87 | len += Buffer.byteLength(chunk); 88 | } 89 | console.log('Result:', len); 90 | }; 91 | } 92 | 93 | if (isMain(import.meta)) { 94 | run(); 95 | } 96 | 97 | // 98 | // Run benchmarks 99 | // 100 | async function run() { 101 | if (!fs.existsSync(filename)) { 102 | // auto-generate fixture 103 | let [, num, unit] = filename.match(/(\d+)([a-z]+).json/); 104 | const times = unit === 'mb' ? num / 100 : num * 10; 105 | const { genFixture } = await import('./gen-fixture.js'); 106 | 107 | filesize = await genFixture(times, filename); 108 | } 109 | 110 | if (process.env.README) { 111 | outputToReadme(benchmarkName, fixtureIndex); 112 | } 113 | 114 | console.log('Benchmark:', chalk.green('stringifyChunked()'), '(JSON.stringify() as a stream of chunks)'); 115 | console.log('Node version:', chalk.green(process.versions.node)); 116 | console.log( 117 | 'Fixture:', 118 | chalk.green(path.relative(process.cwd(), filename)), 119 | chalk.yellow(prettySize(filesize)) 120 | ); 121 | console.log(''); 122 | 123 | const results = []; 124 | for (const name of Object.keys(tests)) { 125 | results.push(await runBenchmark(name) || { name, error: true, code: 'CRASH' }); 126 | } 127 | 128 | if (process.env.README) { 129 | updateReadmeTable(benchmarkName, fixtureIndex, fixtures, results); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /benchmarks/stringify-info.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import url from 'node:url'; 4 | import chalk from 'chalk'; 5 | import * as jsonExt057 from 'json-ext-0.5.7'; 6 | import * as jsonExt060 from 'json-ext-0.6.0'; 7 | import * as jsonExt from '../src/index.js'; 8 | import { 9 | runBenchmark, 10 | prettySize, 11 | outputToReadme, 12 | updateReadmeTable, 13 | getSelfPackageJson, 14 | isMain 15 | } from './benchmark-utils.js'; 16 | 17 | const selfPackageJson = getSelfPackageJson(); 18 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 19 | const benchmarkName = 'stringify-info'; 20 | const fixtures = [ 21 | 'fixture/small.json', // ~2,1MB 22 | 'fixture/medium.json', // ~13,7MB 23 | 'fixture/big.json', // ~100Mb 24 | './fixture/500mb.json', // 3 | auto-generate from big.json 25 | './fixture/1gb.json' // 4 | auto-generate from big.json 26 | ]; 27 | const fixtureIndex = process.argv[2] || 0; 28 | const filename = fixtureIndex in fixtures ? path.join(__dirname, fixtures[fixtureIndex]) : false; 29 | let filesize = fs.existsSync(filename) ? fs.statSync(filename).size : 0; 30 | 31 | if (!filename) { 32 | console.error('Fixture is not selected!'); 33 | console.error(); 34 | console.error('Run script:', chalk.green(`node ${path.relative(process.cwd(), process.argv[1])} [fixture]`)); 35 | console.error(); 36 | console.error(`where ${chalk.yellow('[fixture]')} is a number:`); 37 | fixtures.forEach((fixture, idx) => 38 | console.log(idx, fixture) 39 | ); 40 | process.exit(); 41 | } 42 | 43 | export const tests = { 44 | 'JSON.stringify()': data => 45 | Buffer.byteLength(JSON.stringify(data)), 46 | 47 | [selfPackageJson.name + ' stringifyInfo()']: data => 48 | jsonExt.stringifyInfo(data).bytes, 49 | 50 | [selfPackageJson.name + ' v0.6.0 stringifyInfo()']: data => 51 | jsonExt060.stringifyInfo(data).bytes, 52 | 53 | [selfPackageJson.name + ' v0.5.7 stringifyInfo()']: data => 54 | jsonExt057.default.stringifyInfo(data).minLength 55 | }; 56 | 57 | Object.defineProperty(tests, '__getData', { 58 | value: async () => { 59 | return await jsonExt.parseChunked(fs.createReadStream(filename)); 60 | } 61 | }); 62 | 63 | for (const [name, init] of Object.entries(tests)) { 64 | tests[name] = async (data) => { 65 | const len = init(data); 66 | console.log('Result:', len); 67 | }; 68 | } 69 | 70 | if (isMain(import.meta)) { 71 | run(); 72 | } 73 | 74 | // 75 | // Run benchmarks 76 | // 77 | async function run() { 78 | if (!fs.existsSync(filename)) { 79 | // auto-generate fixture 80 | let [, num, unit] = filename.match(/(\d+)([a-z]+).json/); 81 | const times = unit === 'mb' ? num / 100 : num * 10; 82 | const { genFixture } = await import('./gen-fixture.js'); 83 | 84 | filesize = await genFixture(times, filename); 85 | } 86 | 87 | if (process.env.README) { 88 | outputToReadme(benchmarkName, fixtureIndex); 89 | } 90 | 91 | console.log('Benchmark:', chalk.green('stringifyInfo()'), '(size of JSON.stringify())'); 92 | console.log('Node version:', chalk.green(process.versions.node)); 93 | console.log( 94 | 'Fixture:', 95 | chalk.green(path.relative(process.cwd(), filename)), 96 | chalk.yellow(prettySize(filesize)) 97 | ); 98 | console.log(''); 99 | 100 | const results = []; 101 | for (const name of Object.keys(tests)) { 102 | results.push(await runBenchmark(name) || { name, error: true, code: 'CRASH' }); 103 | } 104 | 105 | if (process.env.README) { 106 | updateReadmeTable(benchmarkName, fixtureIndex, fixtures, results); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /benchmarks/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !.npmignore 4 | -------------------------------------------------------------------------------- /benchmarks/tmp/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.js.map 3 | !.gitignore 4 | !.npmignore 5 | !test/json-ext.js 6 | !test/json-ext.min.js 7 | -------------------------------------------------------------------------------- /dist/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !json-ext.js 3 | !json-ext.min.js 4 | !json-ext.min.js.map 5 | !package.json 6 | -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /dist/test/json-ext.js: -------------------------------------------------------------------------------- 1 | /* global jsonExt */ 2 | const fs = require('node:fs'); 3 | const assert = require('node:assert'); 4 | 5 | describe('dist/json-ext.js', () => { 6 | before(() => new Function(fs.readFileSync('dist/json-ext.js'))()); 7 | 8 | it('stringifyChunked', () => { 9 | const expected = '{"test":"ok"}'; 10 | const actual = [...jsonExt.stringifyChunked({ test: 'ok' })].join(''); 11 | 12 | assert.strictEqual(actual, expected); 13 | }); 14 | 15 | it('stringifyInfo', () => { 16 | const expected = '{"test":"ok"}'.length; 17 | const { bytes: actual } = jsonExt.stringifyInfo({ test: 'ok' }); 18 | 19 | assert.strictEqual(actual, expected); 20 | }); 21 | 22 | it('parseChunked', async () => { 23 | const expected = { test: 'ok' }; 24 | const actual = await jsonExt.parseChunked(() => ['{"test"', ':"ok"}']); 25 | 26 | assert.deepStrictEqual(actual, expected); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /dist/test/json-ext.min.js: -------------------------------------------------------------------------------- 1 | /* global jsonExt */ 2 | const fs = require('node:fs'); 3 | const assert = require('node:assert'); 4 | 5 | describe('dist/json-ext.min.js', () => { 6 | before(() => new Function(fs.readFileSync('dist/json-ext.min.js'))()); 7 | 8 | it('stringifyChunked', () => { 9 | const expected = '{"test":"ok"}'; 10 | const actual = [...jsonExt.stringifyChunked({ test: 'ok' })].join(''); 11 | 12 | assert.strictEqual(actual, expected); 13 | }); 14 | 15 | it('stringifyInfo', () => { 16 | const expected = '{"test":"ok"}'.length; 17 | const { bytes: actual } = jsonExt.stringifyInfo({ test: 'ok' }); 18 | 19 | assert.strictEqual(actual, expected); 20 | }); 21 | 22 | it('parseChunked', async () => { 23 | const expected = { test: 'ok' }; 24 | const actual = await jsonExt.parseChunked(() => ['{"test"', ':"ok"}']); 25 | 26 | assert.deepStrictEqual(actual, expected); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@discoveryjs/json-ext' { 2 | type Chunk = string | Uint8Array | Buffer; 3 | type Replacer = 4 | | ((this: any, key: string, value: any) => any) 5 | | (string | number)[] 6 | | null; 7 | type Space = string | number | null; 8 | type StringifyOptions = { 9 | replacer?: Replacer; 10 | space?: Space; 11 | highWaterMark?: number; 12 | }; 13 | type StringifyInfoOptions = { 14 | replacer?: Replacer; 15 | space?: Space; 16 | continueOnCircular?: boolean; 17 | } 18 | type StringifyInfoResult = { 19 | bytes: number; 20 | spaceBytes: number; 21 | circular: object[]; 22 | }; 23 | 24 | export function parseChunked(input: Iterable | AsyncIterable): Promise; 25 | export function parseChunked(input: () => (Iterable | AsyncIterable)): Promise; 26 | 27 | export function stringifyChunked(value: any, replacer?: Replacer, space?: Space): Generator; 28 | export function stringifyChunked(value: any, options: StringifyOptions): Generator; 29 | 30 | export function stringifyInfo(value: any, replacer?: Replacer, space?: Space): StringifyInfoResult; 31 | export function stringifyInfo(value: any, options?: StringifyInfoOptions): StringifyInfoResult; 32 | 33 | // Web streams 34 | export function parseFromWebStream(stream: ReadableStream): Promise; 35 | export function createStringifyWebStream(value: any, replacer?: Replacer, space?: Space): ReadableStream; 36 | export function createStringifyWebStream(value: any, options: StringifyOptions): ReadableStream; 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discoveryjs/json-ext", 3 | "version": "0.6.3", 4 | "description": "A set of utilities that extend the use of JSON", 5 | "keywords": [ 6 | "json", 7 | "utils", 8 | "stream", 9 | "async", 10 | "promise", 11 | "stringify", 12 | "info" 13 | ], 14 | "author": "Roman Dvornov (https://github.com/lahmatiy)", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/discoveryjs/json-ext.git" 19 | }, 20 | "engines": { 21 | "node": ">=14.17.0" 22 | }, 23 | "type": "module", 24 | "main": "./cjs/index.cjs", 25 | "module": "./src/index.js", 26 | "types": "./index.d.ts", 27 | "exports": { 28 | ".": { 29 | "types": "./index.d.ts", 30 | "require": "./cjs/index.cjs", 31 | "import": "./src/index.js" 32 | }, 33 | "./dist/*": "./dist/*", 34 | "./package.json": "./package.json" 35 | }, 36 | "scripts": { 37 | "test": "npm run test:src", 38 | "lint": "eslint src", 39 | "lint-and-test": "npm run lint && npm test", 40 | "bundle": "node scripts/bundle.js", 41 | "transpile": "node scripts/transpile.cjs", 42 | "test:all": "npm run test:src && npm run test:cjs && npm run test:dist && npm run test:e2e", 43 | "test:src": "mocha --reporter progress src/*.test.js", 44 | "test:cjs": "mocha --reporter progress cjs/*.test.cjs", 45 | "test:e2e": "mocha --reporter progress test-e2e", 46 | "test:dist": "mocha --reporter progress dist/test", 47 | "test:deno": "node scripts/deno-adapt-test.js && mocha --reporter progress deno-tests/*.test.js", 48 | "bundle-and-test": "npm run bundle && npm run test:dist", 49 | "coverage": "c8 --reporter=lcovonly npm test", 50 | "prepublishOnly": "npm run lint && npm run bundle && npm run transpile && npm run test:all" 51 | }, 52 | "devDependencies": { 53 | "c8": "^7.10.0", 54 | "chalk": "^4.1.0", 55 | "esbuild": "^0.24.0", 56 | "eslint": "^8.57.0", 57 | "mocha": "^9.2.2", 58 | "rollup": "^2.79.2" 59 | }, 60 | "files": [ 61 | "cjs", 62 | "!cjs/*{.test,-cases}.cjs", 63 | "dist", 64 | "src", 65 | "!src/*{.test,-cases}.js", 66 | "index.d.ts" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /scripts/bundle.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import esbuild from 'esbuild'; 3 | 4 | const log = async (outfile, fn) => { 5 | const start = Date.now(); 6 | try { 7 | await fn(outfile); 8 | } finally { 9 | const stat = fs.statSync(outfile); 10 | if (stat.isDirectory()) { 11 | console.log(outfile, 'in', Date.now() - start, 'ms'); 12 | } else { 13 | console.log(outfile, stat.size, 'bytes in', Date.now() - start, 'ms'); 14 | } 15 | } 16 | }; 17 | 18 | const banner = { js: `(function (global, factory) { 19 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 20 | typeof define === 'function' && define.amd ? define(factory) : 21 | (global.jsonExt = factory()); 22 | }(typeof globalThis != 'undefined' ? globalThis : typeof window != 'undefined' ? window : typeof global != 'undefined' ? global : typeof self != 'undefined' ? self : this, (function () {` }; 23 | const footer = { js: ` 24 | return exports; 25 | })));` }; 26 | 27 | async function build() { 28 | const commonOptions = { 29 | entryPoints: ['src/index.js'], 30 | target: ['es2020'], 31 | format: 'iife', 32 | globalName: 'exports', 33 | // write: false, 34 | banner, 35 | footer, 36 | bundle: true 37 | }; 38 | 39 | // bundle 40 | await log('dist/json-ext.js', (outfile) => esbuild.build({ 41 | ...commonOptions, 42 | // write: false, 43 | outfile 44 | })); 45 | 46 | // minified bundle 47 | await log('dist/json-ext.min.js', (outfile) => esbuild.build({ 48 | ...commonOptions, 49 | // write: false, 50 | outfile, 51 | sourcemap: 'linked', 52 | minify: true 53 | })); 54 | } 55 | 56 | build(); 57 | -------------------------------------------------------------------------------- /scripts/deno-adapt-test.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | fs.rmSync('deno-tests', { recursive: true, force: true }); 4 | fs.mkdirSync('deno-tests'); 5 | 6 | for (const filename of fs.readdirSync('src')) { 7 | const source = fs.readFileSync(`src/${filename}`, 'utf8') 8 | .replace(/from '(assert|buffer|fs|stream|timers|util)'/g, 'from \'node:$1\''); 9 | 10 | fs.writeFileSync(`deno-tests/${filename}`, source); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/transpile.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { rollup, watch } = require('rollup'); 4 | const chalk = require('chalk'); 5 | 6 | const external = [ 7 | 'fs', 8 | 'url', 9 | 'path', 10 | 'assert', 11 | 'stream', 12 | 'util', 13 | 'buffer', 14 | 'timers', 15 | '@discoveryjs/json-ext' 16 | ]; 17 | 18 | function resolvePath(ts = false, ext) { 19 | return { 20 | name: 'transpile-ts', 21 | resolveId(source, parent) { 22 | if (parent && !/\/(src|lib)\//.test(parent) && /\/(src|lib)\//.test(source)) { 23 | return { 24 | id: source 25 | // .replace(/\/lib\//, '/cjs/') 26 | .replace(/\/src\//, '/cjs/') 27 | .replace(/\.js$/, ext), 28 | external: true 29 | }; 30 | } 31 | if (ts && parent && source.startsWith('.')) { 32 | const resolved = path.resolve(path.dirname(parent), source); 33 | const resolvedTs = resolved.replace(/.js$/, '.ts'); 34 | 35 | return fs.existsSync(resolvedTs) ? resolvedTs : resolved; 36 | } 37 | return null; 38 | } 39 | }; 40 | } 41 | 42 | function readDir(dir) { 43 | return fs 44 | .readdirSync(dir) 45 | .filter((fn) => fn.endsWith('.js') || fn.endsWith('.ts')) 46 | .map((fn) => `${dir}/${fn}`); 47 | } 48 | 49 | async function transpile({ 50 | entryPoints, 51 | outputDir, 52 | format, 53 | watch: watchMode = false, 54 | ts = false, 55 | onSuccess 56 | }) { 57 | const outputExt = format === 'esm' ? '.js' : '.cjs'; 58 | const doneMessage = (duration) => 59 | `${ 60 | ts ? 'Compile TypeScript to JavaScript (ESM)' : 'Convert ESM to CommonJS' 61 | } into "${outputDir}" done in ${duration}ms`; 62 | 63 | const inputOptions = { 64 | external, 65 | input: entryPoints, 66 | plugins: [ 67 | resolvePath(ts, outputExt) 68 | ] 69 | }; 70 | const outputOptions = { 71 | dir: outputDir, 72 | entryFileNames: `[name]${outputExt}`, 73 | sourcemap: ts, 74 | format, 75 | exports: 'auto', 76 | preserveModules: true, 77 | interop: false, 78 | esModule: format === 'esm', 79 | generatedCode: { 80 | constBindings: true 81 | } 82 | }; 83 | 84 | if (!watchMode) { 85 | const startTime = Date.now(); 86 | const bundle = await rollup(inputOptions); 87 | await bundle.write(outputOptions); 88 | await bundle.close(); 89 | 90 | console.log(doneMessage(Date.now() - startTime)); 91 | 92 | if (typeof onSuccess === 'function') { 93 | await onSuccess(); 94 | } 95 | } else { 96 | const watcher = watch({ 97 | ...inputOptions, 98 | output: outputOptions 99 | }); 100 | 101 | watcher.on('event', ({ code, duration, error }) => { 102 | if (code === 'BUNDLE_END') { 103 | console.log(doneMessage(duration)); 104 | 105 | if (typeof onSuccess === 'function') { 106 | onSuccess(); 107 | } 108 | } else if (code === 'ERROR') { 109 | console.error(chalk.bgRed.white('ERROR!'), chalk.red(error.message)); 110 | } 111 | }); 112 | } 113 | } 114 | 115 | async function transpileAll(options) { 116 | const { watch = false } = options || {}; 117 | 118 | fs.rmSync('cjs', { recursive: true, force: true }); 119 | await transpile({ 120 | entryPoints: ['src/index.js', ...readDir('src').filter(fn => fn.includes('.test.'))], 121 | outputDir: './cjs', 122 | format: 'cjs', 123 | watch 124 | }); 125 | } 126 | 127 | module.exports = transpileAll; 128 | 129 | if (require.main === module) { 130 | transpileAll({ 131 | watch: process.argv.includes('--watch') 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { parseChunked } from './parse-chunked.js'; 2 | export { stringifyChunked } from './stringify-chunked.js'; 3 | export { stringifyInfo } from './stringify-info.js'; 4 | export { createStringifyWebStream, parseFromWebStream } from './web-streams.js'; 5 | -------------------------------------------------------------------------------- /src/parse-chunked.js: -------------------------------------------------------------------------------- 1 | import { isIterable } from './utils.js'; 2 | 3 | const STACK_OBJECT = 1; 4 | const STACK_ARRAY = 2; 5 | const decoder = new TextDecoder(); 6 | 7 | function adjustPosition(error, parser) { 8 | if (error.name === 'SyntaxError' && parser.jsonParseOffset) { 9 | error.message = error.message.replace(/at position (\d+)/, (_, pos) => 10 | 'at position ' + (Number(pos) + parser.jsonParseOffset) 11 | ); 12 | } 13 | 14 | return error; 15 | } 16 | 17 | function append(array, elements) { 18 | // Note: Avoid using array.push(...elements) since it may lead to 19 | // "RangeError: Maximum call stack size exceeded" for long arrays 20 | const initialLength = array.length; 21 | array.length += elements.length; 22 | 23 | for (let i = 0; i < elements.length; i++) { 24 | array[initialLength + i] = elements[i]; 25 | } 26 | } 27 | 28 | export async function parseChunked(chunkEmitter) { 29 | const iterable = typeof chunkEmitter === 'function' 30 | ? chunkEmitter() 31 | : chunkEmitter; 32 | 33 | if (isIterable(iterable)) { 34 | let parser = createChunkParser(); 35 | 36 | try { 37 | for await (const chunk of iterable) { 38 | if (typeof chunk !== 'string' && !ArrayBuffer.isView(chunk)) { 39 | throw new TypeError('Invalid chunk: Expected string, TypedArray or Buffer'); 40 | } 41 | 42 | parser.push(chunk); 43 | } 44 | 45 | return parser.finish(); 46 | } catch (e) { 47 | throw adjustPosition(e, parser); 48 | } 49 | } 50 | 51 | throw new TypeError( 52 | 'Invalid chunk emitter: Expected an Iterable, AsyncIterable, generator, ' + 53 | 'async generator, or a function returning an Iterable or AsyncIterable' 54 | ); 55 | }; 56 | 57 | function createChunkParser() { 58 | let value = undefined; 59 | let valueStack = null; 60 | 61 | let prevArray = null; 62 | let prevArraySlices = []; 63 | 64 | let stack = new Array(100); 65 | let lastFlushDepth = 0; 66 | let flushDepth = 0; 67 | let stateString = false; 68 | let stateStringEscape = false; 69 | let pendingByteSeq = null; 70 | let pendingChunk = null; 71 | let chunkOffset = 0; 72 | let jsonParseOffset = 0; 73 | 74 | return { 75 | push, 76 | finish, 77 | get jsonParseOffset() { 78 | return jsonParseOffset; 79 | } 80 | }; 81 | 82 | function mergeArraySlices() { 83 | if (prevArray === null) { 84 | return; 85 | } 86 | 87 | if (prevArraySlices.length !== 0) { 88 | const newArray = prevArraySlices.length === 1 89 | ? prevArray.concat(prevArraySlices[0]) 90 | : prevArray.concat(...prevArraySlices); 91 | 92 | if (valueStack.prev !== null) { 93 | valueStack.prev.value[valueStack.key] = newArray; 94 | } else { 95 | value = newArray; 96 | } 97 | 98 | valueStack.value = newArray; 99 | prevArraySlices = []; 100 | } 101 | 102 | prevArray = null; 103 | } 104 | 105 | function parseAndAppend(fragment, wrap) { 106 | // Append new entries or elements 107 | if (stack[lastFlushDepth - 1] === STACK_OBJECT) { 108 | if (wrap) { 109 | jsonParseOffset--; 110 | fragment = '{' + fragment + '}'; 111 | } 112 | 113 | Object.assign(valueStack.value, JSON.parse(fragment)); 114 | } else { 115 | if (wrap) { 116 | jsonParseOffset--; 117 | fragment = '[' + fragment + ']'; 118 | } 119 | 120 | if (prevArray === valueStack.value) { 121 | prevArraySlices.push(JSON.parse(fragment)); 122 | } else { 123 | append(valueStack.value, JSON.parse(fragment)); 124 | prevArray = valueStack.value; 125 | } 126 | } 127 | } 128 | 129 | function prepareAddition(fragment) { 130 | const { value } = valueStack; 131 | const expectComma = Array.isArray(value) 132 | ? value.length !== 0 133 | : Object.keys(value).length !== 0; 134 | 135 | if (expectComma) { 136 | // Skip a comma at the beginning of fragment, otherwise it would 137 | // fail to parse 138 | if (fragment[0] === ',') { 139 | jsonParseOffset++; 140 | return fragment.slice(1); 141 | } 142 | 143 | // When value (an object or array) is not empty and a fragment 144 | // doesn't start with a comma, a single valid fragment starting 145 | // is a closing bracket. If it's not, a prefix is adding to fail 146 | // parsing. Otherwise, the sequence of chunks can be successfully 147 | // parsed, although it should not, e.g. ["[{}", "{}]"] 148 | if (fragment[0] !== '}' && fragment[0] !== ']') { 149 | jsonParseOffset -= 3; 150 | return '[[]' + fragment; 151 | } 152 | } 153 | 154 | return fragment; 155 | } 156 | 157 | function flush(chunk, start, end) { 158 | let fragment = chunk.slice(start, end); 159 | 160 | // Save position correction for an error in JSON.parse() if any 161 | jsonParseOffset = chunkOffset + start; 162 | 163 | // Prepend pending chunk if any 164 | if (pendingChunk !== null) { 165 | fragment = pendingChunk + fragment; 166 | jsonParseOffset -= pendingChunk.length; 167 | pendingChunk = null; 168 | } 169 | 170 | if (flushDepth === lastFlushDepth) { 171 | // Depth didn't change, so it's a root value or entry/element set 172 | if (flushDepth > 0) { 173 | parseAndAppend(prepareAddition(fragment), true); 174 | } else { 175 | // That's an entire value on a top level 176 | value = JSON.parse(fragment); 177 | valueStack = { 178 | value, 179 | key: null, 180 | prev: null 181 | }; 182 | } 183 | } else if (flushDepth > lastFlushDepth) { 184 | // Add missed closing brackets/parentheses 185 | for (let i = flushDepth - 1; i >= lastFlushDepth; i--) { 186 | fragment += stack[i] === STACK_OBJECT ? '}' : ']'; 187 | } 188 | 189 | if (lastFlushDepth === 0) { 190 | // That's a root value 191 | value = JSON.parse(fragment); 192 | valueStack = { 193 | value, 194 | key: null, 195 | prev: null 196 | }; 197 | } else { 198 | parseAndAppend(prepareAddition(fragment), true); 199 | mergeArraySlices(); 200 | } 201 | 202 | // Move down to the depths to the last object/array, which is current now 203 | for (let i = lastFlushDepth || 1; i < flushDepth; i++) { 204 | let value = valueStack.value; 205 | let key = null; 206 | 207 | if (stack[i - 1] === STACK_OBJECT) { 208 | // Find last entry 209 | // eslint-disable-next-line curly 210 | for (key in value); 211 | value = value[key]; 212 | } else { 213 | // Last element 214 | key = value.length - 1; 215 | value = value[key]; 216 | } 217 | 218 | valueStack = { 219 | value, 220 | key, 221 | prev: valueStack 222 | }; 223 | } 224 | } else /* flushDepth < lastFlushDepth */ { 225 | fragment = prepareAddition(fragment); 226 | 227 | // Add missed opening brackets/parentheses 228 | for (let i = lastFlushDepth - 1; i >= flushDepth; i--) { 229 | jsonParseOffset--; 230 | fragment = (stack[i] === STACK_OBJECT ? '{' : '[') + fragment; 231 | } 232 | 233 | parseAndAppend(fragment, false); 234 | mergeArraySlices(); 235 | 236 | for (let i = lastFlushDepth - 1; i >= flushDepth; i--) { 237 | valueStack = valueStack.prev; 238 | } 239 | } 240 | 241 | lastFlushDepth = flushDepth; 242 | } 243 | 244 | function ensureChunkString(chunk) { 245 | if (typeof chunk !== 'string') { 246 | // Suppose chunk is Buffer or Uint8Array 247 | 248 | // Prepend uncompleted byte sequence if any 249 | if (pendingByteSeq !== null) { 250 | const origRawChunk = chunk; 251 | chunk = new Uint8Array(pendingByteSeq.length + origRawChunk.length); 252 | chunk.set(pendingByteSeq); 253 | chunk.set(origRawChunk, pendingByteSeq.length); 254 | pendingByteSeq = null; 255 | } 256 | 257 | // In case Buffer/Uint8Array, an input is encoded in UTF8 258 | // Seek for parts of uncompleted UTF8 symbol on the ending 259 | // This makes sense only if we expect more chunks and last char is not multi-bytes 260 | if (chunk[chunk.length - 1] > 127) { 261 | for (let seqLength = 0; seqLength < chunk.length; seqLength++) { 262 | const byte = chunk[chunk.length - 1 - seqLength]; 263 | 264 | // 10xxxxxx - 2nd, 3rd or 4th byte 265 | // 110xxxxx – first byte of 2-byte sequence 266 | // 1110xxxx - first byte of 3-byte sequence 267 | // 11110xxx - first byte of 4-byte sequence 268 | if (byte >> 6 === 3) { 269 | seqLength++; 270 | 271 | // If the sequence is really incomplete, then preserve it 272 | // for the future chunk and cut off it from the current chunk 273 | if ((seqLength !== 4 && byte >> 3 === 0b11110) || 274 | (seqLength !== 3 && byte >> 4 === 0b1110) || 275 | (seqLength !== 2 && byte >> 5 === 0b110)) { 276 | pendingByteSeq = chunk.slice(chunk.length - seqLength); // use slice to avoid tying chunk 277 | chunk = chunk.subarray(0, -seqLength); // use subarray to avoid buffer copy 278 | } 279 | 280 | break; 281 | } 282 | } 283 | } 284 | 285 | // Convert chunk to a string, since single decode per chunk 286 | // is much effective than decode multiple small substrings 287 | chunk = decoder.decode(chunk); 288 | } 289 | 290 | return chunk; 291 | } 292 | 293 | function push(chunk) { 294 | chunk = ensureChunkString(chunk); 295 | 296 | const chunkLength = chunk.length; 297 | let lastFlushPoint = 0; 298 | let flushPoint = 0; 299 | 300 | // Main scan loop 301 | scan: for (let i = 0; i < chunkLength; i++) { 302 | if (stateString) { 303 | for (; i < chunkLength; i++) { 304 | if (stateStringEscape) { 305 | stateStringEscape = false; 306 | } else { 307 | switch (chunk.charCodeAt(i)) { 308 | case 0x22: /* " */ 309 | stateString = false; 310 | continue scan; 311 | 312 | case 0x5C: /* \ */ 313 | stateStringEscape = true; 314 | } 315 | } 316 | } 317 | 318 | break; 319 | } 320 | 321 | switch (chunk.charCodeAt(i)) { 322 | case 0x22: /* " */ 323 | stateString = true; 324 | stateStringEscape = false; 325 | break; 326 | 327 | case 0x2C: /* , */ 328 | flushPoint = i; 329 | break; 330 | 331 | case 0x7B: /* { */ 332 | // Open an object 333 | flushPoint = i + 1; 334 | stack[flushDepth++] = STACK_OBJECT; 335 | break; 336 | 337 | case 0x5B: /* [ */ 338 | // Open an array 339 | flushPoint = i + 1; 340 | stack[flushDepth++] = STACK_ARRAY; 341 | break; 342 | 343 | case 0x5D: /* ] */ 344 | case 0x7D: /* } */ 345 | // Close an object or array 346 | flushPoint = i + 1; 347 | flushDepth--; 348 | 349 | if (flushDepth < lastFlushDepth) { 350 | flush(chunk, lastFlushPoint, flushPoint); 351 | lastFlushPoint = flushPoint; 352 | } 353 | 354 | break; 355 | 356 | case 0x09: /* \t */ 357 | case 0x0A: /* \n */ 358 | case 0x0D: /* \r */ 359 | case 0x20: /* space */ 360 | // Move points forward when they point to current position and it's a whitespace 361 | if (lastFlushPoint === i) { 362 | lastFlushPoint++; 363 | } 364 | 365 | if (flushPoint === i) { 366 | flushPoint++; 367 | } 368 | 369 | break; 370 | } 371 | } 372 | 373 | if (flushPoint > lastFlushPoint) { 374 | flush(chunk, lastFlushPoint, flushPoint); 375 | } 376 | 377 | // Produce pendingChunk if something left 378 | if (flushPoint < chunkLength) { 379 | if (pendingChunk !== null) { 380 | // When there is already a pending chunk then no flush happened, 381 | // appending entire chunk to pending one 382 | pendingChunk += chunk; 383 | } else { 384 | // Create a pending chunk, it will start with non-whitespace since 385 | // flushPoint was moved forward away from whitespaces on scan 386 | pendingChunk = chunk.slice(flushPoint, chunkLength); 387 | } 388 | } 389 | 390 | chunkOffset += chunkLength; 391 | } 392 | 393 | function finish() { 394 | if (pendingChunk !== null) { 395 | flush('', 0, 0); 396 | pendingChunk = null; 397 | } 398 | 399 | return value; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/parse-chunked.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Buffer } from 'buffer'; // needed for Deno 3 | import { Readable } from 'stream'; 4 | import { inspect } from 'util'; 5 | import { parseChunked } from './parse-chunked.js'; 6 | 7 | function parse(chunks) { 8 | return parseChunked(() => chunks); 9 | } 10 | 11 | function split(str, chunkLen = 1) { 12 | const chunks = []; 13 | 14 | for (let i = 0; i < str.length; i += chunkLen) { 15 | chunks.push(str.slice(i, i + chunkLen)); 16 | } 17 | 18 | return chunks; 19 | } 20 | 21 | function createReadableNodejsStream(chunks) { 22 | return new Readable({ 23 | read() { 24 | const value = chunks.shift() || null; 25 | 26 | if (value instanceof Error) { 27 | return this.destroy(value); 28 | } 29 | 30 | this.push(value); 31 | } 32 | }); 33 | } 34 | 35 | describe('parseChunked()', () => { 36 | const values = [ 37 | 1, 38 | 123, 39 | -123, 40 | 0.5, 41 | -0.5, 42 | 1 / 33, 43 | -1 / 33, 44 | true, 45 | false, 46 | null, 47 | '', 48 | 'test', 49 | 'hello world', 50 | '🤓漢字', 51 | '\b\t\n\f\r"\\\\"\\u0020', // escapes 52 | '\u0000\u0010\u001F\u009F', 53 | '\uD800\uDC00', // surrogate pair 54 | '\uDC00\uD800', // broken surrogate pair 55 | '\uD800', // leading surrogate (broken surrogate pair) 56 | '\uDC00', // trailing surrogate (broken surrogate pair) 57 | '\\\\\\"\\\\"\\"\\\\\\', 58 | {}, 59 | { a: 1 }, 60 | { a: 1, b: 2 }, 61 | { a: { b: 2 } }, 62 | { 'te\\u0020st\\"': 'te\\u0020st\\"' }, 63 | [], 64 | [1], 65 | [1, 2], 66 | [1, [2, [3]]], 67 | [{ a: 2, b: true }, false, '', 12, [1, null]], 68 | [1, { a: [true, { b: 1, c: [{ d: 2 }] }, 'hello world\n!', null, 123, [{ e: '4', f: [] }, [], 123, [1, false]]] }, 2, { g: 5 }, [42]] 69 | ]; 70 | 71 | describe('basic parsing (single chunk)', () => { 72 | for (const expected of values) { 73 | const json = JSON.stringify(expected); 74 | it(json, async () => { 75 | const actual = await parse([json]); 76 | assert.deepStrictEqual(actual, expected); 77 | }); 78 | } 79 | }); 80 | 81 | for (const len of [1, 2, 3, 4, 5, 10]) { 82 | describe(len + ' char(s) length chunks', () => { 83 | for (const expected of values) { 84 | const json = JSON.stringify(expected); 85 | 86 | if (json.length > len) { 87 | it(json, async () => assert.deepStrictEqual(await parse(split(json, len)), expected)); 88 | } 89 | } 90 | }); 91 | } 92 | 93 | for (const len of [1, 2, 3, 4, 5, 10]) { 94 | describe(len + ' char(s) length chunks with formatting', () => { 95 | for (const expected of values) { 96 | const json = JSON.stringify(expected, null, '\r\n\t '); 97 | const nofmt = JSON.stringify(expected); 98 | 99 | if (json.length > len && json !== nofmt) { 100 | it(json, async () => assert.deepStrictEqual(await parse(split(json, len)), expected)); 101 | } 102 | } 103 | }); 104 | } 105 | 106 | describe('splitting on whitespaces', () => { 107 | describe('inside an object and strings', () => { 108 | const expected = { ' \r\n\t': ' \r\n\t', a: [1, 2] }; 109 | const json = ' \r\n\t{ \r\n\t" \\r\\n\\t" \r\n\t: \r\n\t" \\r\\n\\t" \r\n\t, \r\n\t"a": \r\n\t[ \r\n\t1 \r\n\t, \r\n\t2 \r\n\t] \r\n\t} \r\n\t'; 110 | 111 | for (let len = 0; len <= json.length; len++) { 112 | it(len ? len + ' char(s) length chunks' : 'parse full', async () => 113 | assert.deepStrictEqual(await parse(len ? split(json, len) : [json]), expected) 114 | ); 115 | } 116 | }); 117 | 118 | describe('between objects and arrays', () => { 119 | const expected = [{}, {}, {}, [], [], [], {}]; 120 | const json = '[{} \r\n\t, {}, \r\n\t {} \r\n\t, [], \r\n\t [] \r\n\t, [] \r\n\t, {} \r\n\t]'; 121 | 122 | for (let len = 0; len <= json.length; len++) { 123 | it(len ? len + ' char(s) length chunks' : 'parse full', async () => 124 | assert.deepStrictEqual(await parse(len ? split(json, len) : [json]), expected) 125 | ); 126 | } 127 | }); 128 | }); 129 | 130 | describe('errors', () => { 131 | it('abs pos across chunks', () => 132 | assert.rejects( 133 | async () => await parse(['{"test":"he', 'llo",}']), 134 | /(Unexpected token \}|Expected double-quoted property name) in JSON at position 16|Property name must be a string literal/ 135 | ) 136 | ); 137 | it('abs pos across chunks #2', () => 138 | assert.rejects( 139 | async () => await parse(['[{"test":"hello"},', ',}']), 140 | /Unexpected token , in JSON at position 18|Unexpected token ','(, "\[,}" is not valid JSON)?$|/ 141 | ) 142 | ); 143 | it('abs pos across chunks #3 (whitespaces)', () => 144 | assert.rejects( 145 | async () => await parse(['[{"test" ', ' ', ' :"hello"} ', ' ', ',', ' ', ',}']), 146 | /Unexpected token , in JSON at position 24|Unexpected token ','(, "\[,}" is not valid JSON)?$/ 147 | ) 148 | ); 149 | it('should fail when starts with a comma', () => 150 | assert.rejects( 151 | async () => await parse([',{}']), 152 | /Unexpected token , in JSON at position 0|Unexpected token ','(, ",{}" is not valid JSON)?$/ 153 | ) 154 | ); 155 | it('should fail when starts with a comma #2', () => 156 | assert.rejects( 157 | async () => await parse([',', '{}']), 158 | /Unexpected token , in JSON at position 0|Unexpected token ','(, ",{}" is not valid JSON)?$/ 159 | ) 160 | ); 161 | it('should fail when no comma', () => 162 | assert.rejects( 163 | async () => await parse(['[1 ', ' 2]']), 164 | /(Unexpected number|Expected ',' or ']' after array element) in JSON at position 4|Expected ']'/ 165 | ) 166 | ); 167 | it('should fail when no comma #2', () => 168 | assert.rejects( 169 | async () => await parse(['[{}', '{}]']), 170 | /(Unexpected token {|Expected ',' or ']' after array element) in JSON at position 3|Expected ']'/ 171 | ) 172 | ); 173 | }); 174 | 175 | describe('use with buffers', () => { 176 | const input = '[1234,{"🤓\\uD800\\uDC00":"🤓\\uD800\\uDC00\\u006f\\ufffd\\uffff\\ufffd"}]'; 177 | const expected = [1234, { '🤓\uD800\uDC00': '🤓\uD800\uDC00\u006f\ufffd\uffff\ufffd' }]; 178 | const slices = [ 179 | [0, 3], // [12 180 | [3, 9], // 34,{"\ud8 181 | [9, 13], // 3e\udd13 182 | [13, 14], // \uD8 183 | [14, 16], // 00\uDC00 184 | [16, 17], // " 185 | [17, 18], // : 186 | [18, 21], // "\ud83e 187 | [21, 22], // \udd 188 | [22, 23], // 13 189 | [23, 26], // \uD800\uDC 190 | [26, 28], // 00\u00 191 | [28, 29], // 6f 192 | [29, 30], // \uff 193 | [30, 31], // fd 194 | [31, 32], // \uff 195 | [32, 33], // ff 196 | [33, 34], // ff 197 | [34, 35], // fd 198 | [35] // ... 199 | ]; 200 | 201 | it('Buffer', async () => { 202 | const buffer = Buffer.from(input); 203 | const actual = await parseChunked(() => slices.map(([...args]) => buffer.slice(...args))); 204 | 205 | assert.deepStrictEqual(actual, expected); 206 | }); 207 | 208 | it('Uint8Array', async () => { 209 | const encoded = new TextEncoder().encode(input); 210 | const actual = await parseChunked(() => slices.map(([...args]) => encoded.slice(...args))); 211 | 212 | assert.deepStrictEqual(actual, expected); 213 | }); 214 | }); 215 | 216 | describe('use with generator', () => { 217 | it('basic usage', async () => { 218 | const actual = await parseChunked(function*() { 219 | yield '[1,'; 220 | yield '2]'; 221 | }); 222 | assert.deepStrictEqual(actual, [1, 2]); 223 | }); 224 | 225 | it('promise should be resolved', async () => { 226 | const actual = await parseChunked(function*() { 227 | yield '[1,'; 228 | yield Promise.resolve('2]'); 229 | }); 230 | assert.deepStrictEqual(actual, [1, 2]); 231 | }); 232 | 233 | it('with failure in JSON', () => 234 | assert.rejects( 235 | () => parseChunked(function*() { 236 | yield '[1 '; 237 | yield '2]'; 238 | }), 239 | /(Unexpected number|Expected ',' or ']' after array element) in JSON at position 3|Expected ']'/ 240 | ) 241 | ); 242 | 243 | it('with failure in generator', () => 244 | assert.rejects( 245 | () => parseChunked(function*() { 246 | yield '[1 '; 247 | throw new Error('test error in generator'); 248 | }), 249 | /test error in generator/ 250 | ) 251 | ); 252 | }); 253 | 254 | describe('use with async generator', () => { 255 | it('basic usage', async () => { 256 | const actual = await parseChunked(async function*() { 257 | yield await Promise.resolve('[1,'); 258 | yield Promise.resolve('2,'); 259 | yield await '3,'; 260 | yield '4]'; 261 | }); 262 | assert.deepStrictEqual(actual, [1, 2, 3, 4]); 263 | }); 264 | 265 | it('with failure in JSON', () => 266 | assert.rejects( 267 | () => parseChunked(async function*() { 268 | yield await Promise.resolve('[1 '); 269 | yield '2]'; 270 | }), 271 | /(Unexpected number|Expected ',' or ']' after array element) in JSON at position 3|Expected ']'/ 272 | ) 273 | ); 274 | 275 | it('with failure in generator', () => 276 | assert.rejects( 277 | () => parseChunked(async function*() { 278 | yield '[1 '; 279 | throw new Error('test error in generator'); 280 | }), 281 | /test error in generator/ 282 | ) 283 | ); 284 | 285 | it('with reject in generator', () => 286 | assert.rejects( 287 | () => parseChunked(async function*() { 288 | yield Promise.reject('test error in generator'); 289 | }), 290 | /test error in generator/ 291 | ) 292 | ); 293 | }); 294 | 295 | describe('use with a function returns iterable object', () => { 296 | it('array', async () => { 297 | const actual = await parseChunked(() => ['[1,', '2]']); 298 | assert.deepStrictEqual(actual, [1, 2]); 299 | }); 300 | 301 | it('iterator method', async () => { 302 | const actual = await parseChunked(() => ({ 303 | *[Symbol.iterator]() { 304 | yield '[1,'; 305 | yield '2]'; 306 | } 307 | })); 308 | assert.deepStrictEqual(actual, [1, 2]); 309 | }); 310 | }); 311 | 312 | describe('should fail when passed value is not supported', () => { 313 | const badValues = [ 314 | undefined, 315 | null, 316 | 123, 317 | '[1, 2]', 318 | ['[1, 2,', 3, ']'], 319 | new Uint8Array([1, 2, 3]), 320 | () => {}, 321 | () => ({}), 322 | () => '[1, 2]', 323 | () => ['[1, 2,', 3, ']'], 324 | () => 123, 325 | () => new Uint8Array([1, 2, 3]), 326 | { on() {} }, 327 | { [Symbol.iterator]: null }, 328 | { [Symbol.asyncIterator]: null } 329 | ]; 330 | 331 | for (const value of badValues) { 332 | it(inspect(value), () => 333 | assert.rejects( 334 | () => parseChunked(value), 335 | /Invalid chunk emitter: Expected an Iterable, AsyncIterable, generator, async generator, or a function returning an Iterable or AsyncIterable|Invalid chunk: Expected string, TypedArray or Buffer/ 336 | ) 337 | ); 338 | } 339 | }); 340 | 341 | describe('use with nodejs stream', () => { 342 | it('basic usage', async () => { 343 | const actual = await parseChunked(createReadableNodejsStream(['[1,', '2]'])); 344 | assert.deepStrictEqual(actual, [1, 2]); 345 | }); 346 | 347 | it('with failure in JSON', () => 348 | assert.rejects( 349 | () => parseChunked(createReadableNodejsStream(['[1 ', '2]'])), 350 | /(Unexpected number|Expected ',' or ']' after array element) in JSON at position 3|Expected ']'/ 351 | ) 352 | ); 353 | 354 | it('with failure in stream', () => 355 | assert.rejects( 356 | () => parseChunked(createReadableNodejsStream([new Error('test error in stream')])), 357 | /test error in stream/ 358 | ) 359 | ); 360 | }); 361 | 362 | describe('should not fail on very long arrays (stack overflow)', () => { 363 | it('the same depth', async () => { 364 | const size = 150000; 365 | const actual = await parseChunked(() => ['[1', ',2'.repeat(size - 1), ']']); 366 | assert.deepStrictEqual(actual.length, size); 367 | }); 368 | it('increment depth', async () => { 369 | const size = 150000; 370 | const actual = await parseChunked(() => ['[', '2,'.repeat(size - 1) + '{"a":1', '}]']); 371 | assert.deepStrictEqual(actual.length, size); 372 | }); 373 | it('decrement depth', async () => { 374 | const size = 150000; 375 | const actual = await parseChunked(() => ['[1', ',2'.repeat(size - 1) + ']']); 376 | assert.deepStrictEqual(actual.length, size); 377 | }); 378 | }); 379 | }); 380 | -------------------------------------------------------------------------------- /src/stringify-cases.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export const date = new Date(2020, 8, 3, 15, 21, 55); 4 | export const allUtf8LengthDiffChars = Array.from({ length: 0x900 }).map((_, i) => String.fromCharCode(i)).join(''); // all chars 0x00..0x8FF 5 | export const fixture = { 6 | a: 1, 7 | b: [2, null, true, false, 'string', { o: 3 }], 8 | c: 'asd', 9 | d: { 10 | e: 4, 11 | d: true, 12 | f: false, 13 | g: 'string', 14 | h: null, 15 | i: [5, 6] 16 | } 17 | }; 18 | export const positiveNumbers = [ 19 | 0, 20 | 10, 21 | 100, 22 | 1_000, 23 | 10_000, 24 | 100_000, 25 | 1_000_000, 26 | 10_000_000, 27 | 100_000_000, 28 | 1_000_000_000, 29 | 10_000_000_000, 30 | 100_000_000_000, 31 | 1_000_000_000_000, 32 | 1.2, 33 | 1 / 10000000, // 1e-7 34 | 1 / 91233000000 // 1.0960946148871571e-11 35 | ]; 36 | export const negativeNumbers = positiveNumbers.map(n => -n); 37 | export const numbers = [...negativeNumbers, ...positiveNumbers]; 38 | export const tests = [ 39 | // scalar 40 | null, 41 | true, 42 | false, 43 | ...numbers, 44 | NaN, // null 45 | Infinity, // null 46 | -Infinity, // null 47 | 'test', 48 | '漢字', 49 | '\b\t\n\f\r"\\', // escapes 50 | ...'\b\t\n\f\r"\\', // escapes as a separate char 51 | '\x7F', // 127 - 1 byte in UTF8 52 | '\x80', // 128 - 2 bytes in UTF8 53 | '\u07FF', // 2047 - 2 bytes in UTF8 54 | '\u0800', // 2048 - 3 bytes in UTF8 55 | '\u0000\u0010\u001F\u009F', 56 | '\uD800\uDC00', // surrogate pair 57 | '\uDC00\uD800', // broken surrogate pair 58 | '\uD800', // leading surrogate (broken surrogate pair) 59 | '\uDC00', // trailing surrogate (broken surrogate pair) 60 | allUtf8LengthDiffChars, 61 | 62 | new Number(3), 63 | new String('false'), 64 | new Boolean(false), 65 | date, // date.toJSON() 66 | 67 | // object 68 | {}, 69 | { a: undefined }, // {} 70 | { a: null }, // {"a":null} 71 | { a: undefined, b: undefined }, // {} 72 | { a: undefined, b: 1 }, // {"b":1} 73 | { a: 1, b: undefined }, 74 | { a: 1, b: undefined, c: 2 }, 75 | { a: 1 }, 76 | { foo: 1, bar: 2 }, 77 | { a: 1, b: { c: 2 } }, 78 | { a: [1], b: 2 }, 79 | { a() {}, b: 'b' }, 80 | { a: 10, b: undefined, c: function() { }, d: Symbol('test') }, 81 | { foo: 1, bar: undefined, baz: { a: undefined, b: 123, c: [1, 2] } }, 82 | { foo: 1, bar: NaN, baz: Infinity, qux: -Infinity, num: new Number(3), str: new String('str'), bool: new Boolean(false) }, 83 | { foo: 1, bar: () => 123 }, 84 | 85 | // array 86 | [], 87 | [1], 88 | [1, 2], 89 | [1, 2, 3], 90 | [1, undefined, 2], 91 | [1, , 2], 92 | [1, 'a'], 93 | [undefined], 94 | [[[]],[[]]], 95 | [function a() {}], 96 | [function a() {}, undefined], 97 | [{}, [], { a: [], o: {} }], 98 | [{ a: 1 }, 'test', { b: [{ c: 3, d: 4 }]}], 99 | [{ foo: 1 }, undefined, true, new Boolean(false), 123, NaN, Infinity, -Infinity, new Number(3), 'test', new String('asd')], 100 | [10, undefined, function() { }, Symbol('')], 101 | 102 | // special cases 103 | /regex/gi, // {} 104 | new RegExp('asd'), 105 | // undefined, // JSON.stringify() returns undefined instead of 'null' 106 | // () => 123, // JSON.stringify() returns undefined instead of 'null' 107 | // Symbol('test') // JSON.stringify() returns undefined instead of 'null' 108 | 109 | fixture 110 | ]; 111 | export const spaceTests = tests 112 | .filter(t => typeof t === 'object') 113 | .concat('foo', 123, false); 114 | export const replacerTests = [ 115 | [1, () => 2], 116 | [{ a: undefined }, (k, v) => { 117 | if (k) { 118 | assert.strictEqual(k, 'a'); 119 | assert.strictEqual(v, undefined); 120 | return 1; 121 | } 122 | return v; 123 | }], 124 | [{ a: 1, b: 2 }, (k, v) => { 125 | if (k === 'a' && v === 1) { 126 | return v; 127 | } 128 | if (k === 'b' && v === 2) { 129 | return undefined; 130 | } 131 | return v; 132 | }], 133 | 134 | // replacer as an allowlist of keys 135 | [{ a: 1, b: 2 }, ['b']], 136 | [{ a: 1, b: 2, __proto__: { c: 3 } }, ['c']], 137 | [{ 1: 1, b: 2 }, [1]], 138 | 139 | // toJSON/replacer order 140 | [{ 141 | source: 'replacer', 142 | toJSON: () => ({ source: 'toJSON' }) 143 | }, (_, value) => value.source], 144 | 145 | // `this` should refer to holder 146 | [ 147 | (() => { 148 | const ar = [4, 5, { a: 7 }, { m: 2, a: 8 }]; 149 | ar.m = 6; 150 | return { 151 | a: 2, 152 | b: 3, 153 | m: 4, 154 | c: ar 155 | }; 156 | })(), 157 | function(key, value) { 158 | return typeof value === 'number' && key !== 'm' && typeof this.m === 'number' 159 | ? value * this.m 160 | : value; 161 | } 162 | ] 163 | ]; 164 | 165 | export const spaces = [undefined, 0, '', 2, ' ', '\t', '___', 20, '-'.repeat(20)]; 166 | -------------------------------------------------------------------------------- /src/stringify-chunked.js: -------------------------------------------------------------------------------- 1 | import { normalizeStringifyOptions, replaceValue } from './utils.js'; 2 | 3 | function encodeString(value) { 4 | if (/[^\x20\x21\x23-\x5B\x5D-\uD799]/.test(value)) { // [^\x20-\uD799]|[\x22\x5c] 5 | return JSON.stringify(value); 6 | } 7 | 8 | return '"' + value + '"'; 9 | } 10 | 11 | export function* stringifyChunked(value, ...args) { 12 | const { replacer, getKeys, space, ...options } = normalizeStringifyOptions(...args); 13 | const highWaterMark = Number(options.highWaterMark) || 0x4000; // 16kb by default 14 | 15 | const keyStrings = new Map(); 16 | const stack = []; 17 | const rootValue = { '': value }; 18 | let prevState = null; 19 | let state = () => printEntry('', value); 20 | let stateValue = rootValue; 21 | let stateEmpty = true; 22 | let stateKeys = ['']; 23 | let stateIndex = 0; 24 | let buffer = ''; 25 | 26 | while (true) { 27 | state(); 28 | 29 | if (buffer.length >= highWaterMark || prevState === null) { 30 | // flush buffer 31 | yield buffer; 32 | buffer = ''; 33 | 34 | if (prevState === null) { 35 | break; 36 | } 37 | } 38 | } 39 | 40 | function printObject() { 41 | if (stateIndex === 0) { 42 | stateKeys = getKeys(stateValue); 43 | buffer += '{'; 44 | } 45 | 46 | // when no keys left 47 | if (stateIndex === stateKeys.length) { 48 | buffer += space && !stateEmpty 49 | ? `\n${space.repeat(stack.length - 1)}}` 50 | : '}'; 51 | 52 | popState(); 53 | return; 54 | } 55 | 56 | const key = stateKeys[stateIndex++]; 57 | printEntry(key, stateValue[key]); 58 | } 59 | 60 | function printArray() { 61 | if (stateIndex === 0) { 62 | buffer += '['; 63 | } 64 | 65 | if (stateIndex === stateValue.length) { 66 | buffer += space && !stateEmpty 67 | ? `\n${space.repeat(stack.length - 1)}]` 68 | : ']'; 69 | 70 | popState(); 71 | return; 72 | } 73 | 74 | printEntry(stateIndex, stateValue[stateIndex++]); 75 | } 76 | 77 | function printEntryPrelude(key) { 78 | if (stateEmpty) { 79 | stateEmpty = false; 80 | } else { 81 | buffer += ','; 82 | } 83 | 84 | if (space && prevState !== null) { 85 | buffer += `\n${space.repeat(stack.length)}`; 86 | } 87 | 88 | if (state === printObject) { 89 | let keyString = keyStrings.get(key); 90 | 91 | if (keyString === undefined) { 92 | keyStrings.set(key, keyString = encodeString(key) + (space ? ': ' : ':')); 93 | } 94 | 95 | buffer += keyString; 96 | } 97 | } 98 | 99 | function printEntry(key, value) { 100 | value = replaceValue(stateValue, key, value, replacer); 101 | 102 | if (value === null || typeof value !== 'object') { 103 | // primitive 104 | if (state !== printObject || value !== undefined) { 105 | printEntryPrelude(key); 106 | pushPrimitive(value); 107 | } 108 | } else { 109 | // If the visited set does not change after adding a value, then it is already in the set 110 | if (stack.includes(value)) { 111 | throw new TypeError('Converting circular structure to JSON'); 112 | } 113 | 114 | printEntryPrelude(key); 115 | stack.push(value); 116 | 117 | pushState(); 118 | state = Array.isArray(value) ? printArray : printObject; 119 | stateValue = value; 120 | stateEmpty = true; 121 | stateIndex = 0; 122 | } 123 | } 124 | 125 | function pushPrimitive(value) { 126 | switch (typeof value) { 127 | case 'string': 128 | buffer += encodeString(value); 129 | break; 130 | 131 | case 'number': 132 | buffer += Number.isFinite(value) ? String(value) : 'null'; 133 | break; 134 | 135 | case 'boolean': 136 | buffer += value ? 'true' : 'false'; 137 | break; 138 | 139 | case 'undefined': 140 | case 'object': // typeof null === 'object' 141 | buffer += 'null'; 142 | break; 143 | 144 | default: 145 | throw new TypeError(`Do not know how to serialize a ${value.constructor?.name || typeof value}`); 146 | } 147 | } 148 | 149 | function pushState() { 150 | prevState = { 151 | keys: stateKeys, 152 | index: stateIndex, 153 | prev: prevState 154 | }; 155 | } 156 | 157 | function popState() { 158 | stack.pop(); 159 | const value = stack.length > 0 ? stack[stack.length - 1] : rootValue; 160 | 161 | // restore state 162 | state = Array.isArray(value) ? printArray : printObject; 163 | stateValue = value; 164 | stateEmpty = false; 165 | stateKeys = prevState.keys; 166 | stateIndex = prevState.index; 167 | 168 | // pop state 169 | prevState = prevState.prev; 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /src/stringify-chunked.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { inspect } from 'util'; 3 | import { stringifyChunked } from './stringify-chunked.js'; 4 | import { date, allUtf8LengthDiffChars, tests, spaceTests, spaces, replacerTests } from './stringify-cases.js'; 5 | 6 | const wellformedStringify = JSON.stringify; 7 | 8 | inspect.defaultOptions.breakLength = Infinity; 9 | 10 | function testTitleWithValue(title) { 11 | title = title === allUtf8LengthDiffChars 12 | ? `All UTF8 length diff chars ${title[0]}..${title[title.length - 1]}` 13 | : inspect(title, { depth: null }); 14 | 15 | return title.replace(/[\u0000-\u001f\u0100-\uffff]/g, m => '\\u' + m.charCodeAt().toString(16).padStart(4, '0')); 16 | } 17 | 18 | function createStringifyTestFn(input, expected, ...args) { 19 | return () => { 20 | const actual = [...stringifyChunked(input, ...args)].join(''); 21 | 22 | if (actual !== expected) { 23 | const escapedActual = JSON.stringify(actual); 24 | const escapedExpected = JSON.stringify(expected); 25 | 26 | if (actual !== escapedActual || expected !== escapedExpected) { 27 | assert.strictEqual(escapedActual.slice(1, -1), escapedExpected.slice(1, -1)); 28 | } else { 29 | assert.strictEqual(actual, expected); 30 | } 31 | } 32 | }; 33 | } 34 | 35 | describe('stringifyChunked()', () => { 36 | describe('base', () => { 37 | for (const value of tests) { 38 | const expected = wellformedStringify(value); 39 | it(`${testTitleWithValue(value)} should be ${testTitleWithValue(expected)}`, 40 | createStringifyTestFn(value, expected)); 41 | } 42 | 43 | // special cases 44 | it('Symbol("test") should be null', createStringifyTestFn(Symbol('test'), 'null')); 45 | it('undefined should be null', createStringifyTestFn(undefined, 'null')); 46 | }); 47 | 48 | describe('toJSON()', () => { 49 | const values = [ 50 | date, 51 | { toJSON: () => 123 }, 52 | { a: date, b: { a: 1, toJSON: () => 'ok' } } 53 | ]; 54 | 55 | for (const value of values) { 56 | const expected = wellformedStringify(value); 57 | it(`${testTitleWithValue(value)} should be ${testTitleWithValue(expected)}`, 58 | createStringifyTestFn(value, expected)); 59 | } 60 | }); 61 | 62 | describe('highWaterMark', () => { 63 | it('16kb by default', () => { 64 | const chunks = [...stringifyChunked([...new Uint8Array(20000)])].map(chunk => chunk.length); 65 | 66 | assert.deepStrictEqual(chunks, [16384, 16384, 7233]); 67 | }); 68 | 69 | it('custom highWaterMark', () => { 70 | const chunks = [...stringifyChunked([...new Uint8Array(100)], { 71 | highWaterMark: 20 72 | })].map(chunk => chunk.length); 73 | 74 | assert.deepStrictEqual(chunks, Array.from({ length: 10 }, () => 20).concat(1)); 75 | }); 76 | }); 77 | 78 | describe('replacer', () => { 79 | for (const [value, replacer] of replacerTests) { 80 | const expected = wellformedStringify(value, replacer); 81 | it(`${testTitleWithValue(value)} should be ${testTitleWithValue(expected)}`, 82 | createStringifyTestFn(value, expected, replacer)); 83 | } 84 | 85 | describe('should take replacer from options', () => { 86 | for (const [value, replacer] of replacerTests) { 87 | const expected = wellformedStringify(value, replacer); 88 | it(`${testTitleWithValue(value)} should be ${testTitleWithValue(expected)}`, 89 | createStringifyTestFn(value, expected, { replacer })); 90 | } 91 | }); 92 | 93 | it('walk sequence should be the same', () => { 94 | const data = { a: 1, b: 'asd', c: [1, 2, 3, { d: true, e: null }] }; 95 | const actual = []; 96 | const expected = []; 97 | const replacer = function(key, value) { 98 | currentLog.push(this, key, value); 99 | return value; 100 | }; 101 | let currentLog; 102 | 103 | currentLog = expected; 104 | const expectedJson = wellformedStringify(data, replacer); 105 | 106 | currentLog = actual; 107 | const actualJson = [...stringifyChunked(data, replacer)].join(''); 108 | 109 | assert.strictEqual(actualJson, expectedJson); 110 | assert.strictEqual(actual.length, expected.length); 111 | assert.deepStrictEqual(actual[0], expected[0]); // { '': data } 112 | 113 | for (let i = 1; i < actual.length; i++) { 114 | assert.strictEqual(actual[i], expected[i]); 115 | } 116 | }); 117 | 118 | it('various values for a replace as an allowlist', () => { 119 | // NOTE: There is no way to iterate keys in order of addition 120 | // in case of numeric keys, such keys are always going first sorted 121 | // in asceding numeric order disregarding of actual position. 122 | // Therefore, the result is not the same as for JSON.stringify() 123 | // where keys goes in order of definition, e.g. "1" key goes last. 124 | const value = { '3': 'ok', b: [2, 3, { c: 5, a: 4 }, 7, { d: 1 }], 2: 'fail', 1: 'ok', a: 1, c: 6, '': 'fail' }; 125 | const replacer = ['a', 'a', new String('b'), { toString: () => 'c' }, 1, '2', new Number(3), null, () => {}, Symbol(), false]; 126 | 127 | return createStringifyTestFn( 128 | value, 129 | JSON.stringify(value, replacer), 130 | replacer 131 | )(); 132 | }); 133 | }); 134 | 135 | describe('space option', () => { 136 | for (const space of spaces) { 137 | describe('space ' + wellformedStringify(space), () => { 138 | for (const value of spaceTests) { 139 | it(inspect(value), createStringifyTestFn(value, wellformedStringify(value, null, space), null, space)); 140 | } 141 | 142 | it('[Number, Array]', 143 | createStringifyTestFn( 144 | [ 145 | 1, 146 | [2, 3], 147 | 4, 148 | [5], 149 | 6 150 | ], 151 | wellformedStringify([1, [2, 3], 4, [5], 6], null, space), 152 | null, 153 | space 154 | ) 155 | ); 156 | }); 157 | } 158 | 159 | describe('should take spaces from options', () => { 160 | for (const value of spaceTests) { 161 | const space = 5; 162 | const expected = wellformedStringify(value, null, space); 163 | it(inspect(value), createStringifyTestFn(value, expected, { space }, 3)); 164 | } 165 | }); 166 | }); 167 | 168 | it('options', () => { 169 | const actual = [...stringifyChunked({ foo: 123, bar: 456 }, { 170 | highWaterMark: 1, 171 | replacer: ['foo'], 172 | space: 4 173 | }, 2)]; // should ignore third argument when options passed 174 | 175 | assert.deepStrictEqual(actual, [ 176 | '{\n "foo": 123', 177 | '\n}' 178 | ]); 179 | }); 180 | 181 | describe('circular structure', () => { 182 | it('{ a: $ } should emit error when object refers to ancestor object', () => { 183 | const circularRef = {}; 184 | circularRef.a = circularRef; 185 | 186 | assert.throws( 187 | createStringifyTestFn(circularRef, ''), 188 | (err) => { 189 | assert.strictEqual(err.message, 'Converting circular structure to JSON'); 190 | return true; 191 | } 192 | ); 193 | }); 194 | 195 | it('[{ a: $ }] should emit error when object refers to ancestor array', () => { 196 | const circularRef = []; 197 | circularRef.push({ a: circularRef }); 198 | 199 | assert.throws( 200 | createStringifyTestFn(circularRef, ''), 201 | (err) => { 202 | assert.strictEqual(err.message, 'Converting circular structure to JSON'); 203 | return true; 204 | } 205 | ); 206 | }); 207 | 208 | it('{ a: [$] } should emit error when array\'s element refers to ancestor object', () => { 209 | const circularRef = {}; 210 | circularRef.a = [circularRef]; 211 | 212 | assert.throws( 213 | createStringifyTestFn(circularRef, ''), 214 | (err) => { 215 | assert.strictEqual(err.message, 'Converting circular structure to JSON'); 216 | return true; 217 | } 218 | ); 219 | }); 220 | 221 | it('[[$]] should emit error when array\'s element refers to ancestor object', () => { 222 | const circularRef = []; 223 | circularRef.push(circularRef); 224 | 225 | assert.throws( 226 | createStringifyTestFn(circularRef, ''), 227 | (err) => { 228 | assert.strictEqual(err.message, 'Converting circular structure to JSON'); 229 | return true; 230 | } 231 | ); 232 | }); 233 | 234 | it('should not fail on reuse empty object/array', () => { 235 | const obj = {}; 236 | const obj2 = { a: 1 }; 237 | const arr = []; 238 | const arr2 = [1]; 239 | const noCycle = { 240 | o1: obj, o2: obj, o3: obj2, o4: obj2, 241 | a1: arr, a2: arr, a3: arr2, a4: arr2 242 | }; 243 | 244 | return createStringifyTestFn(noCycle, wellformedStringify(noCycle)); 245 | }); 246 | }); 247 | 248 | describe('errors', () => { 249 | it('"Do not know how to serialize" error', () => assert.throws( 250 | createStringifyTestFn({ test: 1n }, ''), 251 | /TypeError: Do not know how to serialize a BigInt/ 252 | )); 253 | 254 | it('should catch errors on value resolving', () => assert.throws( 255 | createStringifyTestFn({ toJSON() { 256 | throw new Error('test'); 257 | } }, ''), 258 | /Error: test/ 259 | )); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /src/stringify-info.js: -------------------------------------------------------------------------------- 1 | import { normalizeStringifyOptions, replaceValue } from './utils.js'; 2 | 3 | const hasOwn = typeof Object.hasOwn === 'function' 4 | ? Object.hasOwn 5 | : (object, key) => Object.hasOwnProperty.call(object, key); 6 | 7 | // https://tc39.es/ecma262/#table-json-single-character-escapes 8 | const escapableCharCodeSubstitution = { // JSON Single Character Escape Sequences 9 | 0x08: '\\b', 10 | 0x09: '\\t', 11 | 0x0a: '\\n', 12 | 0x0c: '\\f', 13 | 0x0d: '\\r', 14 | 0x22: '\\\"', 15 | 0x5c: '\\\\' 16 | }; 17 | 18 | const charLength2048 = Uint8Array.from({ length: 2048 }, (_, code) => { 19 | if (hasOwn(escapableCharCodeSubstitution, code)) { 20 | return 2; // \X 21 | } 22 | 23 | if (code < 0x20) { 24 | return 6; // \uXXXX 25 | } 26 | 27 | return code < 128 ? 1 : 2; // UTF8 bytes 28 | }); 29 | 30 | function isLeadingSurrogate(code) { 31 | return code >= 0xD800 && code <= 0xDBFF; 32 | } 33 | 34 | function isTrailingSurrogate(code) { 35 | return code >= 0xDC00 && code <= 0xDFFF; 36 | } 37 | 38 | function stringLength(str) { 39 | // Fast path to compute length when a string contains only characters encoded as single bytes 40 | if (!/[^\x20\x21\x23-\x5B\x5D-\x7F]/.test(str)) { 41 | return str.length + 2; 42 | } 43 | 44 | let len = 0; 45 | let prevLeadingSurrogate = false; 46 | 47 | for (let i = 0; i < str.length; i++) { 48 | const code = str.charCodeAt(i); 49 | 50 | if (code < 2048) { 51 | len += charLength2048[code]; 52 | } else if (isLeadingSurrogate(code)) { 53 | len += 6; // \uXXXX since no pair with trailing surrogate yet 54 | prevLeadingSurrogate = true; 55 | continue; 56 | } else if (isTrailingSurrogate(code)) { 57 | len = prevLeadingSurrogate 58 | ? len - 2 // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes 59 | : len + 6; // \uXXXX 60 | } else { 61 | len += 3; // code >= 2048 is 3 bytes length for UTF8 62 | } 63 | 64 | prevLeadingSurrogate = false; 65 | } 66 | 67 | return len + 2; // +2 for quotes 68 | } 69 | 70 | // avoid producing a string from a number 71 | function intLength(num) { 72 | let len = 0; 73 | 74 | if (num < 0) { 75 | len = 1; 76 | num = -num; 77 | } 78 | 79 | if (num >= 1e9) { 80 | len += 9; 81 | num = (num - num % 1e9) / 1e9; 82 | } 83 | 84 | if (num >= 1e4) { 85 | if (num >= 1e6) { 86 | return len + (num >= 1e8 87 | ? 9 88 | : num >= 1e7 ? 8 : 7 89 | ); 90 | } 91 | return len + (num >= 1e5 ? 6 : 5); 92 | } 93 | 94 | return len + (num >= 1e2 95 | ? num >= 1e3 ? 4 : 3 96 | : num >= 10 ? 2 : 1 97 | ); 98 | }; 99 | 100 | function primitiveLength(value) { 101 | switch (typeof value) { 102 | case 'string': 103 | return stringLength(value); 104 | 105 | case 'number': 106 | return Number.isFinite(value) 107 | ? Number.isInteger(value) 108 | ? intLength(value) 109 | : String(value).length 110 | : 4 /* null */; 111 | 112 | case 'boolean': 113 | return value ? 4 /* true */ : 5 /* false */; 114 | 115 | case 'undefined': 116 | case 'object': 117 | return 4; /* null */ 118 | 119 | default: 120 | return 0; 121 | } 122 | } 123 | 124 | export function stringifyInfo(value, ...args) { 125 | const { replacer, getKeys, ...options } = normalizeStringifyOptions(...args); 126 | const continueOnCircular = Boolean(options.continueOnCircular); 127 | const space = options.space?.length || 0; 128 | 129 | const keysLength = new Map(); 130 | const visited = new Map(); 131 | const circular = new Set(); 132 | const stack = []; 133 | const root = { '': value }; 134 | let stop = false; 135 | let bytes = 0; 136 | let spaceBytes = 0; 137 | let objects = 0; 138 | 139 | walk(root, '', value); 140 | 141 | // when value is undefined or replaced for undefined 142 | if (bytes === 0) { 143 | bytes += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null? 144 | } 145 | 146 | return { 147 | bytes: isNaN(bytes) ? Infinity : bytes + spaceBytes, 148 | spaceBytes: space > 0 && isNaN(bytes) ? Infinity : spaceBytes, 149 | circular: [...circular] 150 | }; 151 | 152 | function walk(holder, key, value) { 153 | if (stop) { 154 | return; 155 | } 156 | 157 | value = replaceValue(holder, key, value, replacer); 158 | 159 | if (value === null || typeof value !== 'object') { 160 | // primitive 161 | if (value !== undefined || Array.isArray(holder)) { 162 | bytes += primitiveLength(value); 163 | } 164 | } else { 165 | // check for circular references 166 | if (stack.includes(value)) { 167 | circular.add(value); 168 | bytes += 4; // treat as null 169 | 170 | if (!continueOnCircular) { 171 | stop = true; 172 | } 173 | 174 | return; 175 | } 176 | 177 | // Using 'visited' allows avoiding hang-ups in cases of highly interconnected object graphs; 178 | // for example, a list of git commits with references to parents can lead to N^2 complexity for traversal, 179 | // and N when 'visited' is used 180 | if (visited.has(value)) { 181 | bytes += visited.get(value); 182 | 183 | return; 184 | } 185 | 186 | objects++; 187 | 188 | const prevObjects = objects; 189 | const valueBytes = bytes; 190 | let valueLength = 0; 191 | 192 | stack.push(value); 193 | 194 | if (Array.isArray(value)) { 195 | // array 196 | valueLength = value.length; 197 | 198 | for (let i = 0; i < valueLength; i++) { 199 | walk(value, i, value[i]); 200 | } 201 | } else { 202 | // object 203 | let prevLength = bytes; 204 | 205 | for (const key of getKeys(value)) { 206 | walk(value, key, value[key]); 207 | 208 | if (prevLength !== bytes) { 209 | let keyLen = keysLength.get(key); 210 | 211 | if (keyLen === undefined) { 212 | keysLength.set(key, keyLen = stringLength(key) + 1); // "key": 213 | } 214 | 215 | // value is printed 216 | bytes += keyLen; 217 | valueLength++; 218 | prevLength = bytes; 219 | } 220 | } 221 | } 222 | 223 | bytes += valueLength === 0 224 | ? 2 // {} or [] 225 | : 1 + valueLength; // {} or [] + commas 226 | 227 | if (space > 0 && valueLength > 0) { 228 | spaceBytes += 229 | // a space between ":" and a value for each object entry 230 | (Array.isArray(value) ? 0 : valueLength) + 231 | // the formula results from folding the following components: 232 | // - for each key-value or element: ident + newline 233 | // (1 + stack.length * space) * valueLength 234 | // - ident (one space less) before "}" or "]" + newline 235 | // (stack.length - 1) * space + 1 236 | (1 + stack.length * space) * (valueLength + 1) - space; 237 | } 238 | 239 | stack.pop(); 240 | 241 | // add to 'visited' only objects that contain nested objects 242 | if (prevObjects !== objects) { 243 | visited.set(value, bytes - valueBytes); 244 | } 245 | } 246 | } 247 | }; 248 | -------------------------------------------------------------------------------- /src/stringify-info.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Buffer } from 'buffer'; // needed for Deno 3 | import { inspect } from 'util'; 4 | import { stringifyInfo } from './stringify-info.js'; 5 | import { 6 | allUtf8LengthDiffChars, 7 | tests, 8 | spaceTests, 9 | spaces 10 | } from './stringify-cases.js'; 11 | 12 | const wellformedStringify = JSON.stringify; 13 | const strBytesLength = str => Buffer.byteLength(str, 'utf8'); 14 | 15 | function createInfoTest(value, ...args) { 16 | const title = value === allUtf8LengthDiffChars 17 | ? `All UTF8 length diff chars ${value[0]}..${value[value.length - 1]}` 18 | : inspect(value, { depth: null }); 19 | 20 | it(title.replace(/[\u0000-\u001f\u0100-\uffff]/g, m => '\\u' + m.charCodeAt().toString(16).padStart(4, '0')), () => { 21 | const native = String(wellformedStringify(value, ...args)); 22 | const nonFormatted = args[1] ? String(wellformedStringify(value, args[0])) : native; 23 | const info = stringifyInfo(value, ...args); 24 | const bytes = strBytesLength(native); 25 | const spaceBytes = nonFormatted !== native 26 | ? bytes - strBytesLength(nonFormatted) 27 | : 0; 28 | 29 | assert.deepStrictEqual(info, { 30 | bytes, 31 | spaceBytes, 32 | circular: [] 33 | }); 34 | }); 35 | } 36 | 37 | describe('stringifyInfo()', () => { 38 | describe('default', () => { 39 | for (const value of tests) { 40 | createInfoTest(value); 41 | } 42 | }); 43 | 44 | describe('replacer option', () => { 45 | // various values for a replace as an allowlist 46 | createInfoTest( 47 | { '3': 'ok', b: [2, 3, { c: 5, a: 4 }, 7, { d: 1 }], 2: 'fail', 1: 'ok', a: 1, c: 6, '': 'fail' }, 48 | ['a', 'a', new String('b'), { toString: () => 'c' }, 1, '2', new Number(3), null, () => {}, Symbol(), false] 49 | ); 50 | }); 51 | 52 | describe('space option', () => { 53 | for (const space of spaces) { 54 | describe('space ' + wellformedStringify(space), () => { 55 | for (const value of spaceTests) { 56 | createInfoTest(value, null, space); 57 | } 58 | }); 59 | } 60 | }); 61 | 62 | it('options', () => { 63 | const actual = stringifyInfo({ foo: 123, bar: 456 }, { 64 | replacer: ['foo'], 65 | space: 4 66 | }, 2); // should ignore third argument when options passed 67 | 68 | assert.deepStrictEqual(actual, { 69 | bytes: 18, 70 | spaceBytes: 7, 71 | circular: [] 72 | }); 73 | }); 74 | 75 | describe('circular', () => { 76 | it('should stop on first circular reference by default', () => { 77 | const circularRef = {}; 78 | const circularRef2 = []; 79 | circularRef.a = circularRef; 80 | circularRef.b = 1234567890; 81 | circularRef.c = circularRef2; 82 | circularRef2.push(circularRef, circularRef2); 83 | const info = stringifyInfo(circularRef); 84 | 85 | assert.deepStrictEqual(info.circular, [circularRef]); 86 | }); 87 | 88 | it('should visit all circular reference when options.continueOnCircular', () => { 89 | const circularRef = {}; 90 | const circularRef2 = []; 91 | circularRef.a = circularRef; 92 | circularRef.b = 1234567890; 93 | circularRef.c = circularRef2; 94 | circularRef2.push(circularRef, circularRef2); 95 | const info = stringifyInfo(circularRef, { continueOnCircular: true }); 96 | 97 | assert.deepStrictEqual(info.circular, [circularRef, circularRef2]); 98 | }); 99 | }); 100 | 101 | it('should no throw on unsupported types', () => 102 | assert.strictEqual(stringifyInfo([1n, 123]).bytes, '[,123]'.length) 103 | ); 104 | 105 | describe('undefined return', () => { 106 | const values = [ 107 | undefined, 108 | function() {}, 109 | Symbol() 110 | ]; 111 | 112 | for (const value of values) { 113 | it(String(value), () => 114 | assert.strictEqual(stringifyInfo(value).bytes, 9) 115 | ); 116 | } 117 | }); 118 | 119 | describe('infinite size', () => { 120 | const value = []; 121 | const str = 'str'.repeat(100); 122 | 123 | for (var i = 0; i < 1100; i++) { 124 | value.push({ 125 | foo: str, 126 | bar: 12312313, 127 | baz: [str, 123, str, new Date(2021, 5, 15), str], 128 | [str]: str, 129 | prev: value[i - 1] || null, 130 | a: value[i - 1] || null 131 | }); 132 | } 133 | 134 | it('without formatting', () => { 135 | assert.deepStrictEqual(stringifyInfo(value), { 136 | bytes: Infinity, 137 | spaceBytes: 0, 138 | circular: [] 139 | }); 140 | }); 141 | 142 | it('with formatting (space option)', () => { 143 | assert.deepStrictEqual(stringifyInfo(value, { space: 4 }), { 144 | bytes: Infinity, 145 | spaceBytes: Infinity, 146 | circular: [] 147 | }); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function isIterable(value) { 2 | return ( 3 | typeof value === 'object' && 4 | value !== null && 5 | ( 6 | typeof value[Symbol.iterator] === 'function' || 7 | typeof value[Symbol.asyncIterator] === 'function' 8 | ) 9 | ); 10 | } 11 | 12 | export function replaceValue(holder, key, value, replacer) { 13 | if (value && typeof value.toJSON === 'function') { 14 | value = value.toJSON(); 15 | } 16 | 17 | if (replacer !== null) { 18 | value = replacer.call(holder, String(key), value); 19 | } 20 | 21 | switch (typeof value) { 22 | case 'function': 23 | case 'symbol': 24 | value = undefined; 25 | break; 26 | 27 | case 'object': 28 | if (value !== null) { 29 | const cls = value.constructor; 30 | if (cls === String || cls === Number || cls === Boolean) { 31 | value = value.valueOf(); 32 | } 33 | } 34 | break; 35 | } 36 | 37 | return value; 38 | } 39 | 40 | export function normalizeReplacer(replacer) { 41 | if (typeof replacer === 'function') { 42 | return replacer; 43 | } 44 | 45 | if (Array.isArray(replacer)) { 46 | const allowlist = new Set(replacer 47 | .map(item => { 48 | const cls = item && item.constructor; 49 | return cls === String || cls === Number ? String(item) : null; 50 | }) 51 | .filter(item => typeof item === 'string') 52 | ); 53 | 54 | return [...allowlist]; 55 | } 56 | 57 | return null; 58 | } 59 | 60 | export function normalizeSpace(space) { 61 | if (typeof space === 'number') { 62 | if (!Number.isFinite(space) || space < 1) { 63 | return false; 64 | } 65 | 66 | return ' '.repeat(Math.min(space, 10)); 67 | } 68 | 69 | if (typeof space === 'string') { 70 | return space.slice(0, 10) || false; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | export function normalizeStringifyOptions(optionsOrReplacer, space) { 77 | if (optionsOrReplacer === null || Array.isArray(optionsOrReplacer) || typeof optionsOrReplacer !== 'object') { 78 | optionsOrReplacer = { 79 | replacer: optionsOrReplacer, 80 | space 81 | }; 82 | } 83 | 84 | let replacer = normalizeReplacer(optionsOrReplacer.replacer); 85 | let getKeys = Object.keys; 86 | 87 | if (Array.isArray(replacer)) { 88 | const allowlist = replacer; 89 | 90 | getKeys = () => allowlist; 91 | replacer = null; 92 | } 93 | 94 | return { 95 | ...optionsOrReplacer, 96 | replacer, 97 | getKeys, 98 | space: normalizeSpace(optionsOrReplacer.space) 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /src/web-streams.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { parseChunked } from './parse-chunked.js'; 3 | import { stringifyChunked } from './stringify-chunked.js'; 4 | import { isIterable } from './utils.js'; 5 | 6 | export function parseFromWebStream(stream) { 7 | // 2024/6/17: currently, an @@asyncIterator on a ReadableStream is not widely supported, 8 | // therefore use a fallback using a reader 9 | // https://caniuse.com/mdn-api_readablestream_--asynciterator 10 | return parseChunked(isIterable(stream) ? stream : async function*() { 11 | const reader = stream.getReader(); 12 | 13 | try { 14 | while (true) { 15 | const { value, done } = await reader.read(); 16 | 17 | if (done) { 18 | break; 19 | } 20 | 21 | yield value; 22 | } 23 | } finally { 24 | reader.releaseLock(); 25 | } 26 | }); 27 | } 28 | 29 | export function createStringifyWebStream(value, replacer, space) { 30 | // 2024/6/17: the ReadableStream.from() static method is supported 31 | // in Node.js 20.6+ and Firefox only 32 | if (typeof ReadableStream.from === 'function') { 33 | return ReadableStream.from(stringifyChunked(value, replacer, space)); 34 | } 35 | 36 | // emulate ReadableStream.from() 37 | return new ReadableStream({ 38 | start() { 39 | this.generator = stringifyChunked(value, replacer, space); 40 | }, 41 | pull(controller) { 42 | const { value, done } = this.generator.next(); 43 | 44 | if (done) { 45 | controller.close(); 46 | } else { 47 | controller.enqueue(value); 48 | } 49 | }, 50 | cancel() { 51 | this.generator = null; 52 | } 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/web-streams.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import assert from 'assert'; 3 | import { createStringifyWebStream, parseFromWebStream } from './web-streams.js'; 4 | 5 | const describeIfSupported = typeof ReadableStream === 'function' ? describe : describe.skip; 6 | 7 | function createReadableStream(chunks) { 8 | chunks = [...chunks]; 9 | 10 | return new ReadableStream({ 11 | pull(controller) { 12 | if (chunks.length > 0) { 13 | controller.enqueue(chunks.shift()); 14 | } else { 15 | controller.close(); 16 | } 17 | } 18 | }); 19 | } 20 | 21 | async function consumeWebStreamChunks(stream) { 22 | const reader = stream.getReader(); 23 | const chunks = []; 24 | 25 | while (true) { 26 | const { value, done } = await reader.read(); 27 | 28 | if (done) { 29 | break; 30 | } 31 | 32 | chunks.push(value); 33 | } 34 | 35 | return chunks; 36 | } 37 | 38 | describeIfSupported('parseFromWebStream()', () => { 39 | it('should parse ReadableStream', async () => { 40 | const actual = await parseFromWebStream(createReadableStream(['{"foo', '":123', '}'])); 41 | 42 | assert.deepStrictEqual(actual, { foo: 123 }); 43 | }); 44 | 45 | it('should parse ReadableStream with no @@asyncIterator', async () => { 46 | const nonIterableReadableStream = Object.assign(createReadableStream(['{"foo', '":123', '}']), { 47 | [Symbol.asyncIterator]: null 48 | }); 49 | const actual = await parseFromWebStream(nonIterableReadableStream); 50 | 51 | assert.deepStrictEqual(actual, { foo: 123 }); 52 | }); 53 | }); 54 | 55 | describeIfSupported('createStringifyWebStream()', () => { 56 | it('default settings', async () => { 57 | const actual = await consumeWebStreamChunks(createStringifyWebStream({ foo: 123, bar: 456 })); 58 | 59 | assert.deepStrictEqual(actual, [ 60 | '{"foo":123,"bar":456}' 61 | ]); 62 | }); 63 | 64 | it('basic settings', async () => { 65 | const actual = await consumeWebStreamChunks(createStringifyWebStream({ foo: 123, bar: 456 }, ['foo'], 2)); 66 | 67 | assert.deepStrictEqual(actual, [ 68 | '{\n "foo": 123\n}' 69 | ]); 70 | }); 71 | 72 | it('custom highWaterMark', async () => { 73 | const actual = await consumeWebStreamChunks(createStringifyWebStream({ foo: 123, bar: 456 }, { 74 | highWaterMark: 1 75 | })); 76 | 77 | assert.deepStrictEqual(actual, [ 78 | '{"foo":123', 79 | ',"bar":456', 80 | '}' 81 | ]); 82 | }); 83 | 84 | it('custom options', async () => { 85 | const actual = await consumeWebStreamChunks(createStringifyWebStream({ foo: 123, bar: 456 }, { 86 | highWaterMark: 1, 87 | replacer: ['foo'], 88 | space: 4 89 | }, 2)); // should ignore third argument when options passed 90 | 91 | assert.deepStrictEqual(actual, [ 92 | '{\n "foo": 123', 93 | '\n}' 94 | ]); 95 | }); 96 | 97 | it('should support cancel', async () => { 98 | const stream = createStringifyWebStream({ foo: 123, bar: 456 }, { 99 | highWaterMark: 1 100 | }); 101 | const reader = stream.getReader(); 102 | 103 | assert.deepStrictEqual(await reader.read(), { value: '{"foo":123', done: false }); 104 | await reader.cancel(); 105 | assert.deepStrictEqual(await reader.read(), { value: undefined, done: true }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test-e2e/commonjs.cjs: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | 4 | it('basic require', async () => { 5 | const { stringifyInfo } = require('@discoveryjs/json-ext'); 6 | const { bytes } = stringifyInfo({ foo: 123 }); 7 | 8 | assert.strictEqual(bytes, 11); 9 | }); 10 | 11 | it('should export package.json', async () => { 12 | const packageJson = require('@discoveryjs/json-ext/package.json'); 13 | 14 | assert.strictEqual(packageJson.name, '@discoveryjs/json-ext'); 15 | }); 16 | 17 | describe('export files', () => { 18 | const files = [ 19 | 'dist/json-ext.js', 20 | 'dist/json-ext.min.js' 21 | ]; 22 | 23 | for (const filename of files) { 24 | it(filename, () => { 25 | const { stringifyInfo } = require(`@discoveryjs/json-ext/${filename}`); 26 | const { bytes } = stringifyInfo({ foo: 123 }); 27 | 28 | assert.strictEqual(bytes, 11); 29 | }); 30 | } 31 | }); 32 | 33 | it('should not be able to access to files not defined by exports', () => { 34 | const filename = 'cjs/index.cjs'; 35 | 36 | assert(fs.existsSync(filename), `${filename} should exist`); 37 | assert.throws( 38 | () => require(`@discoveryjs/json-ext/${filename}`), 39 | new RegExp(`Package subpath '\\./${filename}' is not defined by "exports"`) 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /test-e2e/esm.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import fs from 'fs'; 3 | 4 | it('basic import', async () => { 5 | const { stringifyInfo } = await import('@discoveryjs/json-ext'); 6 | const { bytes } = stringifyInfo({ foo: 123 }); 7 | 8 | assert.strictEqual(bytes, 11); 9 | }); 10 | 11 | // import attributes (i.e. "import(..., { with ... })") cause syntax error currently, disable the test for now; 12 | // it('package.json', async () => { 13 | // const packageJson = await import('@discoveryjs/json-ext/package.json', { with { type: "json" } }); // should expose package.json 14 | // assert.strictEqual(packageJson.name, '@discoveryjs/json-ext'); 15 | // }); 16 | 17 | describe('export files', () => { 18 | const files = [ 19 | 'dist/json-ext.js', 20 | 'dist/json-ext.min.js' 21 | ]; 22 | 23 | for (const filename of files) { 24 | it(filename, async () => { 25 | const { default: { stringifyInfo } } = await import(`@discoveryjs/json-ext/${filename}`); 26 | const { bytes } = stringifyInfo({ foo: 123 }); 27 | 28 | assert.strictEqual(bytes, 11); 29 | }); 30 | } 31 | }); 32 | 33 | it('should not be able to access to files not defined by exports', async () => { 34 | const filename = 'src/index.js'; 35 | 36 | assert(fs.existsSync(filename), `${filename} should exist`); 37 | await assert.rejects( 38 | () => import(`@discoveryjs/json-ext/${filename}`), 39 | new RegExp(`Package subpath '\\./${filename}' is not defined by "exports"`) 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /test-fixture/stringify-medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discoveryjs/cli", 3 | "version": "1.5.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.8.3", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", 10 | "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", 11 | "requires": { 12 | "@babel/highlight": "^7.8.3" 13 | } 14 | }, 15 | "@babel/compat-data": { 16 | "version": "7.9.0", 17 | "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.9.0.tgz", 18 | "integrity": "sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==", 19 | "requires": { 20 | "browserslist": "^4.9.1", 21 | "invariant": "^2.2.4", 22 | "semver": "^5.5.0" 23 | } 24 | }, 25 | "@babel/core": { 26 | "version": "7.9.0", 27 | "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", 28 | "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", 29 | "requires": { 30 | "@babel/code-frame": "^7.8.3", 31 | "@babel/generator": "^7.9.0", 32 | "@babel/helper-module-transforms": "^7.9.0", 33 | "@babel/helpers": "^7.9.0", 34 | "@babel/parser": "^7.9.0", 35 | "@babel/template": "^7.8.6", 36 | "@babel/traverse": "^7.9.0", 37 | "@babel/types": "^7.9.0", 38 | "convert-source-map": "^1.7.0", 39 | "debug": "^4.1.0", 40 | "gensync": "^1.0.0-beta.1", 41 | "json5": "^2.1.2", 42 | "lodash": "^4.17.13", 43 | "resolve": "^1.3.2", 44 | "semver": "^5.4.1", 45 | "source-map": "^0.5.0" 46 | } 47 | }, 48 | "@babel/generator": { 49 | "version": "7.9.4", 50 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz", 51 | "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==", 52 | "requires": { 53 | "@babel/types": "^7.9.0", 54 | "jsesc": "^2.5.1", 55 | "lodash": "^4.17.13", 56 | "source-map": "^0.5.0" 57 | } 58 | }, 59 | "@babel/helper-annotate-as-pure": { 60 | "version": "7.8.3", 61 | "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", 62 | "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", 63 | "requires": { 64 | "@babel/types": "^7.8.3" 65 | } 66 | }, 67 | "@babel/helper-builder-binary-assignment-operator-visitor": { 68 | "version": "7.8.3", 69 | "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz", 70 | "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==", 71 | "requires": { 72 | "@babel/helper-explode-assignable-expression": "^7.8.3", 73 | "@babel/types": "^7.8.3" 74 | } 75 | }, 76 | "@babel/helper-compilation-targets": { 77 | "version": "7.8.7", 78 | "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz", 79 | "integrity": "sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==", 80 | "requires": { 81 | "@babel/compat-data": "^7.8.6", 82 | "browserslist": "^4.9.1", 83 | "invariant": "^2.2.4", 84 | "levenary": "^1.1.1", 85 | "semver": "^5.5.0" 86 | } 87 | }, 88 | "@babel/helper-create-regexp-features-plugin": { 89 | "version": "7.8.8", 90 | "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz", 91 | "integrity": "sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==", 92 | "requires": { 93 | "@babel/helper-annotate-as-pure": "^7.8.3", 94 | "@babel/helper-regex": "^7.8.3", 95 | "regexpu-core": "^4.7.0" 96 | } 97 | }, 98 | "@babel/helper-define-map": { 99 | "version": "7.8.3", 100 | "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz", 101 | "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==", 102 | "requires": { 103 | "@babel/helper-function-name": "^7.8.3", 104 | "@babel/types": "^7.8.3", 105 | "lodash": "^4.17.13" 106 | } 107 | }, 108 | "@babel/helper-explode-assignable-expression": { 109 | "version": "7.8.3", 110 | "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz", 111 | "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==", 112 | "requires": { 113 | "@babel/traverse": "^7.8.3", 114 | "@babel/types": "^7.8.3" 115 | } 116 | }, 117 | "@babel/helper-function-name": { 118 | "version": "7.8.3", 119 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", 120 | "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", 121 | "requires": { 122 | "@babel/helper-get-function-arity": "^7.8.3", 123 | "@babel/template": "^7.8.3", 124 | "@babel/types": "^7.8.3" 125 | } 126 | }, 127 | "@babel/helper-get-function-arity": { 128 | "version": "7.8.3", 129 | "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", 130 | "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", 131 | "requires": { 132 | "@babel/types": "^7.8.3" 133 | } 134 | }, 135 | "@babel/helper-hoist-variables": { 136 | "version": "7.8.3", 137 | "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz", 138 | "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==", 139 | "requires": { 140 | "@babel/types": "^7.8.3" 141 | } 142 | }, 143 | "@babel/helper-member-expression-to-functions": { 144 | "version": "7.8.3", 145 | "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", 146 | "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", 147 | "requires": { 148 | "@babel/types": "^7.8.3" 149 | } 150 | }, 151 | "@babel/helper-module-imports": { 152 | "version": "7.8.3", 153 | "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", 154 | "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", 155 | "requires": { 156 | "@babel/types": "^7.8.3" 157 | } 158 | }, 159 | "@babel/helper-module-transforms": { 160 | "version": "7.9.0", 161 | "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", 162 | "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", 163 | "requires": { 164 | "@babel/helper-module-imports": "^7.8.3", 165 | "@babel/helper-replace-supers": "^7.8.6", 166 | "@babel/helper-simple-access": "^7.8.3", 167 | "@babel/helper-split-export-declaration": "^7.8.3", 168 | "@babel/template": "^7.8.6", 169 | "@babel/types": "^7.9.0", 170 | "lodash": "^4.17.13" 171 | } 172 | }, 173 | "@babel/helper-optimise-call-expression": { 174 | "version": "7.8.3", 175 | "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", 176 | "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", 177 | "requires": { 178 | "@babel/types": "^7.8.3" 179 | } 180 | }, 181 | "@babel/helper-plugin-utils": { 182 | "version": "7.8.3", 183 | "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", 184 | "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==" 185 | }, 186 | "@babel/helper-regex": { 187 | "version": "7.8.3", 188 | "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz", 189 | "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==", 190 | "requires": { 191 | "lodash": "^4.17.13" 192 | } 193 | }, 194 | "@babel/helper-remap-async-to-generator": { 195 | "version": "7.8.3", 196 | "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz", 197 | "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==", 198 | "requires": { 199 | "@babel/helper-annotate-as-pure": "^7.8.3", 200 | "@babel/helper-wrap-function": "^7.8.3", 201 | "@babel/template": "^7.8.3", 202 | "@babel/traverse": "^7.8.3", 203 | "@babel/types": "^7.8.3" 204 | } 205 | }, 206 | "@babel/helper-replace-supers": { 207 | "version": "7.8.6", 208 | "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz", 209 | "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==", 210 | "requires": { 211 | "@babel/helper-member-expression-to-functions": "^7.8.3", 212 | "@babel/helper-optimise-call-expression": "^7.8.3", 213 | "@babel/traverse": "^7.8.6", 214 | "@babel/types": "^7.8.6" 215 | } 216 | }, 217 | "@babel/helper-simple-access": { 218 | "version": "7.8.3", 219 | "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", 220 | "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", 221 | "requires": { 222 | "@babel/template": "^7.8.3", 223 | "@babel/types": "^7.8.3" 224 | } 225 | }, 226 | "@babel/helper-split-export-declaration": { 227 | "version": "7.8.3", 228 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", 229 | "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", 230 | "requires": { 231 | "@babel/types": "^7.8.3" 232 | } 233 | }, 234 | "@babel/helper-validator-identifier": { 235 | "version": "7.9.0", 236 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", 237 | "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==" 238 | }, 239 | "@babel/helper-wrap-function": { 240 | "version": "7.8.3", 241 | "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", 242 | "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==", 243 | "requires": { 244 | "@babel/helper-function-name": "^7.8.3", 245 | "@babel/template": "^7.8.3", 246 | "@babel/traverse": "^7.8.3", 247 | "@babel/types": "^7.8.3" 248 | } 249 | }, 250 | "@babel/helpers": { 251 | "version": "7.9.2", 252 | "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", 253 | "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", 254 | "requires": { 255 | "@babel/template": "^7.8.3", 256 | "@babel/traverse": "^7.9.0", 257 | "@babel/types": "^7.9.0" 258 | } 259 | }, 260 | "@babel/highlight": { 261 | "version": "7.9.0", 262 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", 263 | "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", 264 | "requires": { 265 | "@babel/helper-validator-identifier": "^7.9.0", 266 | "chalk": "^2.0.0", 267 | "js-tokens": "^4.0.0" 268 | }, 269 | "dependencies": { 270 | "chalk": { 271 | "version": "2.4.2", 272 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 273 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 274 | "requires": { 275 | "ansi-styles": "^3.2.1", 276 | "escape-string-regexp": "^1.0.5", 277 | "supports-color": "^5.3.0" 278 | } 279 | } 280 | } 281 | }, 282 | "@babel/parser": { 283 | "version": "7.9.4", 284 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", 285 | "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" 286 | }, 287 | "@babel/plugin-proposal-async-generator-functions": { 288 | "version": "7.8.3", 289 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz", 290 | "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==", 291 | "requires": { 292 | "@babel/helper-plugin-utils": "^7.8.3", 293 | "@babel/helper-remap-async-to-generator": "^7.8.3", 294 | "@babel/plugin-syntax-async-generators": "^7.8.0" 295 | } 296 | }, 297 | "@babel/plugin-proposal-dynamic-import": { 298 | "version": "7.8.3", 299 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", 300 | "integrity": "sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==", 301 | "requires": { 302 | "@babel/helper-plugin-utils": "^7.8.3", 303 | "@babel/plugin-syntax-dynamic-import": "^7.8.0" 304 | } 305 | }, 306 | "@babel/plugin-proposal-json-strings": { 307 | "version": "7.8.3", 308 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz", 309 | "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==", 310 | "requires": { 311 | "@babel/helper-plugin-utils": "^7.8.3", 312 | "@babel/plugin-syntax-json-strings": "^7.8.0" 313 | } 314 | }, 315 | "@babel/plugin-proposal-nullish-coalescing-operator": { 316 | "version": "7.8.3", 317 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", 318 | "integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", 319 | "requires": { 320 | "@babel/helper-plugin-utils": "^7.8.3", 321 | "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" 322 | } 323 | }, 324 | "@babel/plugin-proposal-numeric-separator": { 325 | "version": "7.8.3", 326 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz", 327 | "integrity": "sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==", 328 | "requires": { 329 | "@babel/helper-plugin-utils": "^7.8.3", 330 | "@babel/plugin-syntax-numeric-separator": "^7.8.3" 331 | } 332 | }, 333 | "@babel/plugin-proposal-object-rest-spread": { 334 | "version": "7.9.0", 335 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz", 336 | "integrity": "sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==", 337 | "requires": { 338 | "@babel/helper-plugin-utils": "^7.8.3", 339 | "@babel/plugin-syntax-object-rest-spread": "^7.8.0" 340 | } 341 | }, 342 | "@babel/plugin-proposal-optional-catch-binding": { 343 | "version": "7.8.3", 344 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz", 345 | "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==", 346 | "requires": { 347 | "@babel/helper-plugin-utils": "^7.8.3", 348 | "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" 349 | } 350 | }, 351 | "@babel/plugin-proposal-optional-chaining": { 352 | "version": "7.9.0", 353 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", 354 | "integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", 355 | "requires": { 356 | "@babel/helper-plugin-utils": "^7.8.3", 357 | "@babel/plugin-syntax-optional-chaining": "^7.8.0" 358 | } 359 | }, 360 | "@babel/plugin-proposal-unicode-property-regex": { 361 | "version": "7.8.8", 362 | "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz", 363 | "integrity": "sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==", 364 | "requires": { 365 | "@babel/helper-create-regexp-features-plugin": "^7.8.8", 366 | "@babel/helper-plugin-utils": "^7.8.3" 367 | } 368 | }, 369 | "@babel/plugin-syntax-async-generators": { 370 | "version": "7.8.4", 371 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", 372 | "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", 373 | "requires": { 374 | "@babel/helper-plugin-utils": "^7.8.0" 375 | } 376 | }, 377 | "@babel/plugin-syntax-dynamic-import": { 378 | "version": "7.8.3", 379 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", 380 | "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", 381 | "requires": { 382 | "@babel/helper-plugin-utils": "^7.8.0" 383 | } 384 | }, 385 | "@babel/plugin-syntax-json-strings": { 386 | "version": "7.8.3", 387 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", 388 | "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", 389 | "requires": { 390 | "@babel/helper-plugin-utils": "^7.8.0" 391 | } 392 | }, 393 | "@babel/plugin-syntax-nullish-coalescing-operator": { 394 | "version": "7.8.3", 395 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", 396 | "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", 397 | "requires": { 398 | "@babel/helper-plugin-utils": "^7.8.0" 399 | } 400 | }, 401 | "@babel/plugin-syntax-numeric-separator": { 402 | "version": "7.8.3", 403 | "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz", 404 | "integrity": "sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==", 405 | "requires": { 406 | "@babel/helper-plugin-utils": "^7.8.3" 407 | } 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /test-fixture/stringify-small.json: -------------------------------------------------------------------------------- 1 | {"test":"Hello world"} 2 | --------------------------------------------------------------------------------