├── .all-contributorsrc
├── .eslintrc.cjs
├── .github
├── CODEOWNERS
└── workflows
│ └── main.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .publishrc
├── LICENSE
├── README.md
├── benchmark.js
├── docs
└── superjson-banner.png
├── package.json
├── src
├── accessDeep.test.ts
├── accessDeep.ts
├── class-registry.ts
├── custom-transformer-registry.ts
├── double-indexed-kv.ts
├── index.test.ts
├── index.ts
├── is.test.ts
├── is.ts
├── non-deduped-cal.json
├── pathstringifier.test.ts
├── pathstringifier.ts
├── plainer.spec.ts
├── plainer.ts
├── registry.test.ts
├── registry.ts
├── transformer.test.ts
├── transformer.ts
├── types.ts
└── util.ts
├── tsconfig.json
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "merelinguist",
10 | "name": "Dylan Brookes",
11 | "avatar_url": "https://avatars3.githubusercontent.com/u/24858006?v=4",
12 | "profile": "https://github.com/merelinguist",
13 | "contributions": [
14 | "code",
15 | "doc",
16 | "design",
17 | "test"
18 | ]
19 | },
20 | {
21 | "login": "Skn0tt",
22 | "name": "Simon Knott",
23 | "avatar_url": "https://avatars1.githubusercontent.com/u/14912729?v=4",
24 | "profile": "http://simonknott.de",
25 | "contributions": [
26 | "code",
27 | "ideas",
28 | "test",
29 | "doc"
30 | ]
31 | },
32 | {
33 | "login": "flybayer",
34 | "name": "Brandon Bayer",
35 | "avatar_url": "https://avatars3.githubusercontent.com/u/8813276?v=4",
36 | "profile": "https://twitter.com/flybayer",
37 | "contributions": [
38 | "ideas"
39 | ]
40 | },
41 | {
42 | "login": "mrleebo",
43 | "name": "Jeremy Liberman",
44 | "avatar_url": "https://avatars3.githubusercontent.com/u/2754163?v=4",
45 | "profile": "http://jeremyliberman.com/",
46 | "contributions": [
47 | "test",
48 | "code"
49 | ]
50 | },
51 | {
52 | "login": "jorisre",
53 | "name": "Joris",
54 | "avatar_url": "https://avatars1.githubusercontent.com/u/7545547?v=4",
55 | "profile": "https://github.com/jorisre",
56 | "contributions": [
57 | "code"
58 | ]
59 | },
60 | {
61 | "login": "tomhooijenga",
62 | "name": "tomhooijenga",
63 | "avatar_url": "https://avatars0.githubusercontent.com/u/1853235?v=4",
64 | "profile": "https://github.com/tomhooijenga",
65 | "contributions": [
66 | "code",
67 | "bug"
68 | ]
69 | },
70 | {
71 | "login": "ftonato",
72 | "name": "Ademílson F. Tonato",
73 | "avatar_url": "https://avatars2.githubusercontent.com/u/5417662?v=4",
74 | "profile": "https://twitter.com/ftonato",
75 | "contributions": [
76 | "test"
77 | ]
78 | },
79 | {
80 | "login": "hasparus",
81 | "name": "Piotr Monwid-Olechnowicz",
82 | "avatar_url": "https://avatars0.githubusercontent.com/u/15332326?v=4",
83 | "profile": "https://haspar.us",
84 | "contributions": [
85 | "ideas"
86 | ]
87 | },
88 | {
89 | "login": "KATT",
90 | "name": "Alex Johansson",
91 | "avatar_url": "https://avatars1.githubusercontent.com/u/459267?v=4",
92 | "profile": "http://kattcorp.com",
93 | "contributions": [
94 | "code",
95 | "test"
96 | ]
97 | },
98 | {
99 | "login": "simonedelmann",
100 | "name": "Simon Edelmann",
101 | "avatar_url": "https://avatars.githubusercontent.com/u/2821076?v=4",
102 | "profile": "https://github.com/simonedelmann",
103 | "contributions": [
104 | "bug",
105 | "code",
106 | "ideas"
107 | ]
108 | },
109 | {
110 | "login": "samtgarson",
111 | "name": "Sam Garson",
112 | "avatar_url": "https://avatars.githubusercontent.com/u/6242344?v=4",
113 | "profile": "https://www.samgarson.com",
114 | "contributions": [
115 | "bug"
116 | ]
117 | },
118 | {
119 | "login": "markhughes",
120 | "name": "Mark Hughes",
121 | "avatar_url": "https://avatars.githubusercontent.com/u/1357323?v=4",
122 | "profile": "http://twitter.com/_markeh",
123 | "contributions": [
124 | "bug"
125 | ]
126 | },
127 | {
128 | "login": "Lxxyx",
129 | "name": "Lxxyx",
130 | "avatar_url": "https://avatars.githubusercontent.com/u/13161470?v=4",
131 | "profile": "https://blog.lxxyx.cn/",
132 | "contributions": [
133 | "code"
134 | ]
135 | },
136 | {
137 | "login": "ElMassimo",
138 | "name": "Máximo Mussini",
139 | "avatar_url": "https://avatars.githubusercontent.com/u/1158253?v=4",
140 | "profile": "http://maximomussini.com",
141 | "contributions": [
142 | "code"
143 | ]
144 | },
145 | {
146 | "login": "PeterDekkers",
147 | "name": "Peter Dekkers",
148 | "avatar_url": "https://avatars.githubusercontent.com/u/425971?v=4",
149 | "profile": "https://goodcode.nz",
150 | "contributions": [
151 | "bug"
152 | ]
153 | },
154 | {
155 | "login": "goleary",
156 | "name": "Gabe O'Leary",
157 | "avatar_url": "https://avatars.githubusercontent.com/u/16123225?v=4",
158 | "profile": "http://goleary.com",
159 | "contributions": [
160 | "doc"
161 | ]
162 | },
163 | {
164 | "login": "binajmen",
165 | "name": "Benjamin",
166 | "avatar_url": "https://avatars.githubusercontent.com/u/15611419?v=4",
167 | "profile": "https://github.com/binajmen",
168 | "contributions": [
169 | "doc"
170 | ]
171 | },
172 | {
173 | "login": "icflorescu",
174 | "name": "Ionut-Cristian Florescu",
175 | "avatar_url": "https://avatars.githubusercontent.com/u/581999?v=4",
176 | "profile": "https://www.linkedin.com/in/icflorescu",
177 | "contributions": [
178 | "bug"
179 | ]
180 | },
181 | {
182 | "login": "chrisj-back2work",
183 | "name": "Chris Johnson",
184 | "avatar_url": "https://avatars.githubusercontent.com/u/68551954?v=4",
185 | "profile": "https://github.com/chrisj-back2work",
186 | "contributions": [
187 | "doc"
188 | ]
189 | },
190 | {
191 | "login": "nicholaschiang",
192 | "name": "Nicholas Chiang",
193 | "avatar_url": "https://avatars.githubusercontent.com/u/20798889?v=4",
194 | "profile": "https://nicholaschiang.com",
195 | "contributions": [
196 | "bug",
197 | "code"
198 | ]
199 | },
200 | {
201 | "login": "datner",
202 | "name": "Datner",
203 | "avatar_url": "https://avatars.githubusercontent.com/u/22598347?v=4",
204 | "profile": "https://github.com/datner",
205 | "contributions": [
206 | "code"
207 | ]
208 | },
209 | {
210 | "login": "ruessej",
211 | "name": "ruessej",
212 | "avatar_url": "https://avatars.githubusercontent.com/u/85690286?v=4",
213 | "profile": "https://github.com/ruessej",
214 | "contributions": [
215 | "bug"
216 | ]
217 | },
218 | {
219 | "login": "orionmiz",
220 | "name": "JH.Lee",
221 | "avatar_url": "https://avatars.githubusercontent.com/u/39466936?v=4",
222 | "profile": "https://jins.dev",
223 | "contributions": [
224 | "doc"
225 | ]
226 | },
227 | {
228 | "login": "narumincho",
229 | "name": "narumincho",
230 | "avatar_url": "https://avatars.githubusercontent.com/u/16481886?v=4",
231 | "profile": "https://narumincho.notion.site",
232 | "contributions": [
233 | "code"
234 | ]
235 | },
236 | {
237 | "login": "mgreystone",
238 | "name": "Markus Greystone",
239 | "avatar_url": "https://avatars.githubusercontent.com/u/12430681?v=4",
240 | "profile": "https://github.com/mgreystone",
241 | "contributions": [
242 | "bug"
243 | ]
244 | },
245 | {
246 | "login": "darthmaim",
247 | "name": "darthmaim",
248 | "avatar_url": "https://avatars.githubusercontent.com/u/2511547?v=4",
249 | "profile": "https://gw2treasures.com/",
250 | "contributions": [
251 | "code"
252 | ]
253 | },
254 | {
255 | "login": "benjick",
256 | "name": "Max Malm",
257 | "avatar_url": "https://avatars.githubusercontent.com/u/430872?v=4",
258 | "profile": "http://www.maxmalm.se",
259 | "contributions": [
260 | "doc"
261 | ]
262 | },
263 | {
264 | "login": "tylercollier",
265 | "name": "Tyler Collier",
266 | "avatar_url": "https://avatars.githubusercontent.com/u/366538?v=4",
267 | "profile": "https://github.com/tylercollier",
268 | "contributions": [
269 | "doc"
270 | ]
271 | },
272 | {
273 | "login": "kidqueb",
274 | "name": "Nick Quebbeman",
275 | "avatar_url": "https://avatars.githubusercontent.com/u/884128?v=4",
276 | "profile": "https://github.com/kidqueb",
277 | "contributions": [
278 | "doc"
279 | ]
280 | },
281 | {
282 | "login": "tmcw",
283 | "name": "Tom MacWright",
284 | "avatar_url": "https://avatars.githubusercontent.com/u/32314?v=4",
285 | "profile": "https://macwright.com/",
286 | "contributions": [
287 | "bug",
288 | "code"
289 | ]
290 | },
291 | {
292 | "login": "peterbud",
293 | "name": "Peter Budai",
294 | "avatar_url": "https://avatars.githubusercontent.com/u/7863452?v=4",
295 | "profile": "https://github.com/peterbud",
296 | "contributions": [
297 | "bug"
298 | ]
299 | }
300 | ],
301 | "badgeTemplate": "
-orange.svg?style=flat-square\" alt=\"All Contributors\"/>",
302 | "contributorsPerLine": 7,
303 | "projectName": "superjson",
304 | "projectOwner": "blitz-js",
305 | "repoType": "github",
306 | "repoHost": "https://github.com",
307 | "skipCi": true,
308 | "commitConvention": "angular",
309 | "commitType": "docs"
310 | }
311 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['es5'],
3 | rules: {
4 | 'es5/no-for-of': 'error',
5 | 'es5/no-generators': 'error',
6 | 'es5/no-typeof-symbol': 'error',
7 | 'es5/no-es6-methods': 'error',
8 | 'es5/no-es6-static-methods': [
9 | 'error',
10 | {
11 | exceptMethods: ['Object.assign'],
12 | },
13 | ],
14 | "no-restricted-syntax": [
15 | "error",
16 | "ForInStatement"
17 | ]
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @merelinguist
2 | * @Skn0tt
3 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 |
7 | strategy:
8 | matrix:
9 | node: [20.x, 18.x, 16.x]
10 |
11 | env:
12 | CI: true
13 | YARN_IGNORE_ENGINES: true
14 |
15 | steps:
16 | - name: Begin CI...
17 | uses: actions/checkout@v2
18 |
19 | - name: Use Node
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: ${{ matrix.node }}
23 |
24 | - name: Use cached node_modules
25 | uses: actions/cache@v2
26 | with:
27 | path: node_modules
28 | key: nodeModules-${{ hashFiles('**/yarn.lock') }}
29 | restore-keys: |
30 | nodeModules-
31 |
32 | - name: Install dependencies
33 | run: yarn install --frozen-lockfile
34 |
35 | - name: Lint
36 | run: yarn lint
37 |
38 | - name: Test
39 | run: yarn test
40 |
41 | - name: Build
42 | run: yarn build
43 |
44 | - name: Run benchmark
45 | run: node benchmark.js | tee output.txt
46 | env:
47 | NODE_ENV: production
48 |
49 | - name: Store benchmark result
50 | uses: rhysd/github-action-benchmark@v1
51 | with:
52 | tool: 'benchmarkjs'
53 | output-file-path: output.txt
54 | comment-on-alert: true
55 | save-data-file: ${{ github.ref == 'refs/heads/main' }}
56 | github-token: ${{ secrets.GITHUB_TOKEN }}
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn tsdx lint
5 |
--------------------------------------------------------------------------------
/.publishrc:
--------------------------------------------------------------------------------
1 | {
2 | "validations": {
3 | "branch": "/(main)/",
4 | "vulnerableDependencies": false,
5 | "sensitiveData": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Simon Knott and superjson contributors
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 |
2 |
3 |
4 |
5 |
6 | Safely serialize JavaScript expressions to a superset of JSON, which includes Dates, BigInts, and more.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 | ## Key features
32 |
33 | - 🍱 Reliable serialization and deserialization
34 | - 🔐 Type safety with autocompletion
35 | - 🐾 Negligible runtime footprint
36 | - 💫 Framework agnostic
37 | - 🛠 Perfect fix for Next.js's serialisation limitations in `getServerSideProps` and `getInitialProps`
38 |
39 | ## Backstory
40 |
41 | At [Blitz](https://github.com/blitz-js/blitz), we have struggled with the limitations of JSON. We often find ourselves working with `Date`, `Map`, `Set` or `BigInt`, but `JSON.stringify` doesn't support any of them without going through the hassle of converting manually!
42 |
43 | Superjson solves these issues by providing a thin wrapper over `JSON.stringify` and `JSON.parse`.
44 |
45 | ## Sponsors
46 |
47 | [
](https://www.flightcontrol.dev/?ref=superjson)
48 |
49 | Superjson logo by [NUMI](https://github.com/numi-hq/open-design):
50 |
51 | [
](https://numi.tech/?ref=superjson)
52 |
53 | ## Getting started
54 |
55 | Install the library with your package manager of choice, e.g.:
56 |
57 | ```
58 | yarn add superjson
59 | ```
60 |
61 | ## Basic Usage
62 |
63 | The easiest way to use Superjson is with its `stringify` and `parse` functions. If you know how to use `JSON.stringify`, you already know Superjson!
64 |
65 | Easily stringify any expression you’d like:
66 |
67 | ```js
68 | import superjson from 'superjson';
69 |
70 | const jsonString = superjson.stringify({ date: new Date(0) });
71 |
72 | // jsonString === '{"json":{"date":"1970-01-01T00:00:00.000Z"},"meta":{"values":{date:"Date"}}}'
73 | ```
74 |
75 | And parse your JSON like so:
76 |
77 | ```js
78 | const object = superjson.parse<
79 | { date: Date }
80 | >(jsonString);
81 |
82 | // object === { date: new Date(0) }
83 | ```
84 |
85 | ## Advanced Usage
86 |
87 | For cases where you want lower level access to the `json` and `meta` data in the output, you can use the `serialize` and `deserialize` functions.
88 |
89 | One great use case for this is where you have an API that you want to be JSON compatible for all clients, but you still also want to transmit the meta data so clients can use superjson to fully deserialize it.
90 |
91 | For example:
92 |
93 | ```js
94 | const object = {
95 | normal: 'string',
96 | timestamp: new Date(),
97 | test: /superjson/,
98 | };
99 |
100 | const { json, meta } = superjson.serialize(object);
101 |
102 | /*
103 | json = {
104 | normal: 'string',
105 | timestamp: "2020-06-20T04:56:50.293Z",
106 | test: "/superjson/",
107 | };
108 |
109 | // note that `normal` is not included here; `meta` only has special cases
110 | meta = {
111 | values: {
112 | timestamp: ['Date'],
113 | test: ['regexp'],
114 | }
115 | };
116 | */
117 | ```
118 |
119 | ## Using with Next.js
120 |
121 | The `getServerSideProps`, `getInitialProps`, and `getStaticProps` data hooks provided by Next.js do not allow you to transmit Javascript objects like Dates. It will error unless you convert Dates to strings, etc.
122 |
123 | Thankfully, Superjson is a perfect tool to bypass that limitation!
124 |
125 | ### Next.js SWC Plugin (experimental, v13 or above)
126 |
127 | Next.js SWC plugins are [experimental](https://nextjs.org/docs/advanced-features/compiler#swc-plugins-experimental), but promise a significant speedup.
128 | To use the [SuperJSON SWC plugin](https://github.com/blitz-js/next-superjson-plugin), install it and add it to your `next.config.js`:
129 |
130 | ```sh
131 | yarn add next-superjson-plugin
132 | ```
133 |
134 | ```js
135 | // next.config.js
136 | module.exports = {
137 | experimental: {
138 | swcPlugins: [
139 | [
140 | 'next-superjson-plugin',
141 | {
142 | excluded: [],
143 | },
144 | ],
145 | ],
146 | },
147 | };
148 | ```
149 |
150 | ### Next.js (stable Babel transform)
151 |
152 | Install the library with your package manager of choice, e.g.:
153 |
154 | ```sh
155 | yarn add babel-plugin-superjson-next
156 | ```
157 |
158 | Add the plugin to your .babelrc. If you don't have one, create it.
159 |
160 | ```js
161 | {
162 | "presets": ["next/babel"],
163 | "plugins": [
164 | ...
165 | "superjson-next" // 👈
166 | ]
167 | }
168 | ```
169 |
170 | Done! Now you can safely use all JS datatypes in your `getServerSideProps` / etc. .
171 |
172 | ## API
173 |
174 | ### serialize
175 |
176 | Serializes any JavaScript value into a JSON-compatible object.
177 |
178 | #### Examples
179 |
180 | ```js
181 | const object = {
182 | normal: 'string',
183 | timestamp: new Date(),
184 | test: /superjson/,
185 | };
186 |
187 | const { json, meta } = serialize(object);
188 | ```
189 |
190 | Returns **`json` and `meta`, both JSON-compatible values.**
191 |
192 | ## deserialize
193 |
194 | Deserializes the output of Superjson back into your original value.
195 |
196 | #### Examples
197 |
198 | ```js
199 | const { json, meta } = serialize(object);
200 |
201 | deserialize({ json, meta });
202 | ```
203 |
204 | Returns **`your original value`**.
205 |
206 | ### stringify
207 |
208 | Serializes and then stringifies your JavaScript value.
209 |
210 | #### Examples
211 |
212 | ```js
213 | const object = {
214 | normal: 'string',
215 | timestamp: new Date(),
216 | test: /superjson/,
217 | };
218 |
219 | const jsonString = stringify(object);
220 | ```
221 |
222 | Returns **`string`**.
223 |
224 | ### parse
225 |
226 | Parses and then deserializes the JSON string returned by `stringify`.
227 |
228 | #### Examples
229 |
230 | ```js
231 | const jsonString = stringify(object);
232 |
233 | parse(jsonString);
234 | ```
235 |
236 | Returns **`your original value`**.
237 |
238 | ---
239 |
240 | Superjson supports many extra types which JSON does not. You can serialize all these:
241 |
242 | | type | supported by standard JSON? | supported by Superjson? |
243 | | ----------- | --------------------------- | ----------------------- |
244 | | `string` | ✅ | ✅ |
245 | | `number` | ✅ | ✅ |
246 | | `boolean` | ✅ | ✅ |
247 | | `null` | ✅ | ✅ |
248 | | `Array` | ✅ | ✅ |
249 | | `Object` | ✅ | ✅ |
250 | | `undefined` | ❌ | ✅ |
251 | | `bigint` | ❌ | ✅ |
252 | | `Date` | ❌ | ✅ |
253 | | `RegExp` | ❌ | ✅ |
254 | | `Set` | ❌ | ✅ |
255 | | `Map` | ❌ | ✅ |
256 | | `Error` | ❌ | ✅ |
257 | | `URL` | ❌ | ✅ |
258 |
259 | ## Recipes
260 |
261 | SuperJSON by default only supports built-in data types to keep bundle-size as low as possible.
262 | Here are some recipes you can use to extend to non-default data types.
263 |
264 | Place them in some central utility file and make sure they're executed before any other `SuperJSON` calls.
265 | In a Next.js project, `_app.ts` would be a good spot for that.
266 |
267 | ### `Decimal.js` / `Prisma.Decimal`
268 |
269 | ```ts
270 | import { Decimal } from 'decimal.js';
271 |
272 | SuperJSON.registerCustom(
273 | {
274 | isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
275 | serialize: v => v.toJSON(),
276 | deserialize: v => new Decimal(v),
277 | },
278 | 'decimal.js'
279 | );
280 | ```
281 |
282 | ## Contributors ✨
283 |
284 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
285 |
286 |
287 |
288 |
289 |
334 |
335 |
336 |
337 |
338 |
339 |
340 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
341 |
342 | ## See also
343 |
344 | Other libraries that aim to solve a similar problem:
345 |
346 | - [Serialize JavaScript](https://github.com/yahoo/serialize-javascript) by Eric Ferraiuolo
347 | - [devalue](https://github.com/Rich-Harris/devalue) by Rich Harris
348 | - [next-json](https://github.com/iccicci/next-json) by Daniele Ricci
349 |
--------------------------------------------------------------------------------
/benchmark.js:
--------------------------------------------------------------------------------
1 | import Benchmark from "benchmark"
2 | import SuperJSON from "./dist/index.js"
3 |
4 | const instances = {
5 | 'toy example': {
6 | a: new Map([
7 | [1, NaN],
8 | [2, null],
9 | [3, 'Hurray'],
10 | ]),
11 | a: /regexp/g,
12 | b: [new Set([1, 2, 3])],
13 | },
14 | 'user graph': {
15 | users: new Map([
16 | [
17 | 'abcde',
18 | {
19 | id: 'abcde',
20 | created: new Date(2020),
21 | friendIds: new Set(['a', 'b', 'c']),
22 | },
23 | ],
24 | [
25 | 'dasdfa',
26 | {
27 | id: 'dasdfa',
28 | created: new Date(2019),
29 | friendIds: new Set(['b', 'c']),
30 | },
31 | ],
32 | [
33 | 'hu-ha-hu',
34 | {
35 | id: 'hu-ha-hu',
36 | created: new Date(2018),
37 | friendIds: new Set(['b', 'c', 'd', 'f']),
38 | },
39 | ],
40 | [
41 | 'umphrey',
42 | { id: 'umphrey', created: new Date(2017), friendIds: new Set([]) },
43 | ],
44 | ]),
45 | },
46 | 'deep nested': (() => {
47 | const data = [];
48 | for (let i = 0; i < 100; i++) {
49 | let nested1 = [];
50 | let nested2 = [];
51 | for (let j = 0; j < 10; j++) {
52 | nested1[j] = {
53 | createdAt: new Date(),
54 | updatedAt: new Date(),
55 | innerNested: {
56 | createdAt: new Date(),
57 | updatedAt: new Date(),
58 | },
59 | };
60 | nested2[j] = {
61 | createdAt: new Date(),
62 | updatedAt: new Date(),
63 | innerNested: {
64 | createdAt: new Date(),
65 | updatedAt: new Date(),
66 | },
67 | };
68 | }
69 | const object = {
70 | createdAt: new Date(),
71 | updatedAt: new Date(),
72 | nested1,
73 | nested2,
74 | };
75 | data.push(object);
76 | }
77 | return data;
78 | })(),
79 | };
80 |
81 | const suite = new Benchmark.Suite('serialize & deserialize');
82 |
83 | for (const [key, instance] of Object.entries(instances)) {
84 | suite.add(key, () => {
85 | SuperJSON.deserialize(SuperJSON.serialize(instance));
86 | });
87 | }
88 |
89 | suite.on('cycle', event => {
90 | console.log('' + event.target);
91 | });
92 |
93 | suite.run();
94 |
--------------------------------------------------------------------------------
/docs/superjson-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flightcontrolhq/superjson/0ac86f0ad70c9770471c1c4c4006b4e46381fa83/docs/superjson-banner.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.2.2",
3 | "license": "MIT",
4 | "type": "module",
5 | "typings": "dist/index.d.ts",
6 | "main": "./dist/index.js",
7 | "exports": {
8 | ".": "./dist/index.js"
9 | },
10 | "files": [
11 | "dist"
12 | ],
13 | "engines": {
14 | "node": ">=16"
15 | },
16 | "scripts": {
17 | "build": "tsc",
18 | "test": "vitest run",
19 | "lint": "tsdx lint",
20 | "prepack": "yarn build",
21 | "prepare": "husky install",
22 | "publish-please": "publish-please",
23 | "prepublishOnly": "publish-please guard"
24 | },
25 | "importSort": {
26 | ".ts": {
27 | "style": "module"
28 | }
29 | },
30 | "prettier": {
31 | "printWidth": 80,
32 | "semi": true,
33 | "singleQuote": true,
34 | "trailingComma": "es5"
35 | },
36 | "name": "superjson",
37 | "author": {
38 | "name": "Simon Knott",
39 | "email": "info@simonknott.de",
40 | "url": "https://simonknott.de"
41 | },
42 | "contributors": [
43 | {
44 | "name": "Dylan Brookes",
45 | "email": "dylan@brookes.net",
46 | "url": "https://github.com/merelinguist"
47 | },
48 | {
49 | "name": "Brandon Bayer",
50 | "email": "b@bayer.w",
51 | "url": "https://twitter.com/flybayer"
52 | }
53 | ],
54 | "repository": {
55 | "type": "git",
56 | "url": "https://github.com/blitz-js/superjson"
57 | },
58 | "devDependencies": {
59 | "@types/debug": "^4.1.5",
60 | "@types/mongodb": "^3.6.12",
61 | "@types/node": "^18.7.18",
62 | "benchmark": "^2.1.4",
63 | "decimal.js": "^10.3.1",
64 | "eslint-plugin-es5": "^1.5.0",
65 | "husky": "^6.0.0",
66 | "mongodb": "^3.6.6",
67 | "publish-please": "^5.5.2",
68 | "tsdx": "^0.14.1",
69 | "typescript": "^4.2.4",
70 | "vitest": "^0.34.6"
71 | },
72 | "dependencies": {
73 | "copy-anything": "^3.0.2"
74 | },
75 | "resolutions": {
76 | "**/@typescript-eslint/eslint-plugin": "^4.11.1",
77 | "**/@typescript-eslint/parser": "^4.11.1"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/accessDeep.test.ts:
--------------------------------------------------------------------------------
1 | import { setDeep } from './accessDeep.js';
2 |
3 | import { describe, it, expect } from 'vitest';
4 |
5 | describe('setDeep', () => {
6 | it('correctly sets values in maps', () => {
7 | const obj = {
8 | a: new Map([[new Set(['NaN']), [[1, 'undefined']]]]),
9 | };
10 |
11 | setDeep(obj, ['a', 0, 0, 0], Number);
12 | setDeep(obj, ['a', 0, 1], entries => new Map(entries));
13 | setDeep(obj, ['a', 0, 1, 0, 1], () => undefined);
14 |
15 | expect(obj).toEqual({
16 | a: new Map([[new Set([NaN]), new Map([[1, undefined]])]]),
17 | });
18 | });
19 |
20 | it('correctly sets values in sets', () => {
21 | const obj = {
22 | a: new Set([10, new Set(['NaN'])]),
23 | };
24 |
25 | setDeep(obj, ['a', 1, 0], Number);
26 |
27 | expect(obj).toEqual({
28 | a: new Set([10, new Set([NaN])]),
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/accessDeep.ts:
--------------------------------------------------------------------------------
1 | import { isMap, isArray, isPlainObject, isSet } from './is.js';
2 | import { includes } from './util.js';
3 |
4 | const getNthKey = (value: Map | Set, n: number): any => {
5 | if (n > value.size) throw new Error('index out of bounds');
6 | const keys = value.keys();
7 | while (n > 0) {
8 | keys.next();
9 | n--;
10 | }
11 |
12 | return keys.next().value;
13 | };
14 |
15 | function validatePath(path: (string | number)[]) {
16 | if (includes(path, '__proto__')) {
17 | throw new Error('__proto__ is not allowed as a property');
18 | }
19 | if (includes(path, 'prototype')) {
20 | throw new Error('prototype is not allowed as a property');
21 | }
22 | if (includes(path, 'constructor')) {
23 | throw new Error('constructor is not allowed as a property');
24 | }
25 | }
26 |
27 | export const getDeep = (object: object, path: (string | number)[]): object => {
28 | validatePath(path);
29 |
30 | for (let i = 0; i < path.length; i++) {
31 | const key = path[i];
32 | if (isSet(object)) {
33 | object = getNthKey(object, +key);
34 | } else if (isMap(object)) {
35 | const row = +key;
36 | const type = +path[++i] === 0 ? 'key' : 'value';
37 |
38 | const keyOfRow = getNthKey(object, row);
39 | switch (type) {
40 | case 'key':
41 | object = keyOfRow;
42 | break;
43 | case 'value':
44 | object = object.get(keyOfRow);
45 | break;
46 | }
47 | } else {
48 | object = (object as any)[key];
49 | }
50 | }
51 |
52 | return object;
53 | };
54 |
55 | export const setDeep = (
56 | object: any,
57 | path: (string | number)[],
58 | mapper: (v: any) => any
59 | ): any => {
60 | validatePath(path);
61 |
62 | if (path.length === 0) {
63 | return mapper(object);
64 | }
65 |
66 | let parent = object;
67 |
68 | for (let i = 0; i < path.length - 1; i++) {
69 | const key = path[i];
70 |
71 | if (isArray(parent)) {
72 | const index = +key;
73 | parent = parent[index];
74 | } else if (isPlainObject(parent)) {
75 | parent = parent[key];
76 | } else if (isSet(parent)) {
77 | const row = +key;
78 | parent = getNthKey(parent, row);
79 | } else if (isMap(parent)) {
80 | const isEnd = i === path.length - 2;
81 | if (isEnd) {
82 | break;
83 | }
84 |
85 | const row = +key;
86 | const type = +path[++i] === 0 ? 'key' : 'value';
87 |
88 | const keyOfRow = getNthKey(parent, row);
89 | switch (type) {
90 | case 'key':
91 | parent = keyOfRow;
92 | break;
93 | case 'value':
94 | parent = parent.get(keyOfRow);
95 | break;
96 | }
97 | }
98 | }
99 |
100 | const lastKey = path[path.length - 1];
101 |
102 | if (isArray(parent)) {
103 | parent[+lastKey] = mapper(parent[+lastKey]);
104 | } else if (isPlainObject(parent)) {
105 | parent[lastKey] = mapper(parent[lastKey]);
106 | }
107 |
108 | if (isSet(parent)) {
109 | const oldValue = getNthKey(parent, +lastKey);
110 | const newValue = mapper(oldValue);
111 | if (oldValue !== newValue) {
112 | parent.delete(oldValue);
113 | parent.add(newValue);
114 | }
115 | }
116 |
117 | if (isMap(parent)) {
118 | const row = +path[path.length - 2];
119 | const keyToRow = getNthKey(parent, row);
120 |
121 | const type = +lastKey === 0 ? 'key' : 'value';
122 | switch (type) {
123 | case 'key': {
124 | const newKey = mapper(keyToRow);
125 | parent.set(newKey, parent.get(keyToRow));
126 |
127 | if (newKey !== keyToRow) {
128 | parent.delete(keyToRow);
129 | }
130 | break;
131 | }
132 |
133 | case 'value': {
134 | parent.set(keyToRow, mapper(parent.get(keyToRow)));
135 | break;
136 | }
137 | }
138 | }
139 |
140 | return object;
141 | };
142 |
--------------------------------------------------------------------------------
/src/class-registry.ts:
--------------------------------------------------------------------------------
1 | import { Registry } from './registry.js';
2 | import { Class } from './types.js';
3 |
4 | export interface RegisterOptions {
5 | identifier?: string;
6 | allowProps?: string[];
7 | }
8 |
9 | export class ClassRegistry extends Registry {
10 | constructor() {
11 | super(c => c.name);
12 | }
13 |
14 | private classToAllowedProps = new Map();
15 |
16 | register(value: Class, options?: string | RegisterOptions): void {
17 | if (typeof options === 'object') {
18 | if (options.allowProps) {
19 | this.classToAllowedProps.set(value, options.allowProps);
20 | }
21 |
22 | super.register(value, options.identifier);
23 | } else {
24 | super.register(value, options);
25 | }
26 | }
27 |
28 | getAllowedProps(value: Class): string[] | undefined {
29 | return this.classToAllowedProps.get(value);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/custom-transformer-registry.ts:
--------------------------------------------------------------------------------
1 | import { JSONValue } from './types.js';
2 | import { find } from './util.js';
3 |
4 | export interface CustomTransfomer {
5 | name: string;
6 | isApplicable: (v: any) => v is I;
7 | serialize: (v: I) => O;
8 | deserialize: (v: O) => I;
9 | }
10 |
11 | export class CustomTransformerRegistry {
12 | private transfomers: Record> = {};
13 |
14 | register(transformer: CustomTransfomer) {
15 | this.transfomers[transformer.name] = transformer;
16 | }
17 |
18 | findApplicable(v: T) {
19 | return find(this.transfomers, transformer =>
20 | transformer.isApplicable(v)
21 | ) as CustomTransfomer | undefined;
22 | }
23 |
24 | findByName(name: string) {
25 | return this.transfomers[name];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/double-indexed-kv.ts:
--------------------------------------------------------------------------------
1 | export class DoubleIndexedKV {
2 | keyToValue = new Map();
3 | valueToKey = new Map();
4 |
5 | set(key: K, value: V) {
6 | this.keyToValue.set(key, value);
7 | this.valueToKey.set(value, key);
8 | }
9 |
10 | getByKey(key: K): V | undefined {
11 | return this.keyToValue.get(key);
12 | }
13 |
14 | getByValue(value: V): K | undefined {
15 | return this.valueToKey.get(value);
16 | }
17 |
18 | clear() {
19 | this.keyToValue.clear();
20 | this.valueToKey.clear();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable es5/no-for-of */
2 | /* eslint-disable es5/no-es6-methods */
3 |
4 | import * as fs from 'fs';
5 |
6 | import SuperJSON from './index.js';
7 | import { JSONValue, SuperJSONResult, SuperJSONValue } from './types.js';
8 | import {
9 | isArray,
10 | isMap,
11 | isPlainObject,
12 | isPrimitive,
13 | isSet,
14 | isTypedArray,
15 | } from './is.js';
16 |
17 | import { ObjectID } from 'mongodb';
18 | import { Decimal } from 'decimal.js';
19 |
20 | import { describe, it, expect, test } from 'vitest';
21 |
22 | const isNode10 = process.version.indexOf('v10') === 0;
23 |
24 | describe('stringify & parse', () => {
25 | const cases: Record<
26 | string,
27 | {
28 | input: (() => SuperJSONValue) | SuperJSONValue;
29 | output: JSONValue | ((v: JSONValue) => void);
30 | outputAnnotations?: SuperJSONResult['meta'];
31 | customExpectations?: (value: any) => void;
32 | skipOnNode10?: boolean;
33 | dontExpectEquality?: boolean;
34 | only?: boolean;
35 | }
36 | > = {
37 | 'works for objects': {
38 | input: {
39 | a: { 1: 5, 2: { 3: 'c' } },
40 | b: null,
41 | },
42 | output: {
43 | a: { 1: 5, 2: { 3: 'c' } },
44 | b: null,
45 | },
46 | },
47 |
48 | 'special case: objects with array-like keys': {
49 | input: {
50 | a: { 0: 3, 1: 5, 2: { 3: 'c' } },
51 | b: null,
52 | },
53 | output: {
54 | a: { 0: 3, 1: 5, 2: { 3: 'c' } },
55 | b: null,
56 | },
57 | },
58 |
59 | 'works for arrays': {
60 | input: {
61 | a: [1, undefined, 2],
62 | },
63 | output: {
64 | a: [1, null, 2],
65 | },
66 | outputAnnotations: {
67 | values: {
68 | 'a.1': ['undefined'],
69 | },
70 | },
71 | },
72 |
73 | 'works for Sets': {
74 | input: {
75 | a: new Set([1, undefined, 2]),
76 | },
77 | output: {
78 | a: [1, null, 2],
79 | },
80 | outputAnnotations: {
81 | values: {
82 | a: ['set', { 1: ['undefined'] }],
83 | },
84 | },
85 | },
86 |
87 | 'works for top-level Sets': {
88 | input: new Set([1, undefined, 2]),
89 | output: [1, null, 2],
90 | outputAnnotations: {
91 | values: ['set', { 1: ['undefined'] }],
92 | },
93 | },
94 |
95 | 'works for Maps': {
96 | input: {
97 | a: new Map([
98 | [1, 'a'],
99 | [NaN, 'b'],
100 | ]),
101 | b: new Map([['2', 'b']]),
102 | d: new Map([[true, 'true key']]),
103 | },
104 |
105 | output: {
106 | a: [
107 | [1, 'a'],
108 | ['NaN', 'b'],
109 | ],
110 | b: [['2', 'b']],
111 | d: [[true, 'true key']],
112 | },
113 |
114 | outputAnnotations: {
115 | values: {
116 | a: ['map', { '1.0': ['number'] }],
117 | b: ['map'],
118 | d: ['map'],
119 | },
120 | },
121 | },
122 |
123 | 'preserves object identity': {
124 | input: () => {
125 | const a = { id: 'a' };
126 | const b = { id: 'b' };
127 | return {
128 | options: [a, b],
129 | selected: a,
130 | };
131 | },
132 | output: {
133 | options: [{ id: 'a' }, { id: 'b' }],
134 | selected: { id: 'a' },
135 | },
136 | outputAnnotations: {
137 | referentialEqualities: {
138 | selected: ['options.0'],
139 | },
140 | },
141 | customExpectations: output => {
142 | expect(output.selected).toBe(output.options[0]);
143 | },
144 | },
145 |
146 | 'works for paths containing dots': {
147 | input: {
148 | 'a.1': {
149 | b: new Set([1, 2]),
150 | },
151 | },
152 | output: {
153 | 'a.1': {
154 | b: [1, 2],
155 | },
156 | },
157 | outputAnnotations: {
158 | values: {
159 | 'a\\.1.b': ['set'],
160 | },
161 | },
162 | },
163 |
164 | 'works for paths containing backslashes': {
165 | input: {
166 | 'a\\.1': {
167 | b: new Set([1, 2]),
168 | },
169 | },
170 | output: {
171 | 'a\\.1': {
172 | b: [1, 2],
173 | },
174 | },
175 | outputAnnotations: {
176 | values: {
177 | 'a\\\\.1.b': ['set'],
178 | },
179 | },
180 | },
181 |
182 | 'works for dates': {
183 | input: {
184 | meeting: {
185 | date: new Date(2020, 1, 1),
186 | },
187 | },
188 | output: {
189 | meeting: {
190 | date: new Date(2020, 1, 1).toISOString(),
191 | },
192 | },
193 | outputAnnotations: {
194 | values: {
195 | 'meeting.date': ['Date'],
196 | },
197 | },
198 | },
199 |
200 | 'works for Errors': {
201 | input: {
202 | e: new Error('epic fail'),
203 | },
204 | output: ({ e }: any) => {
205 | expect(e.name).toBe('Error');
206 | expect(e.message).toBe('epic fail');
207 | },
208 | outputAnnotations: {
209 | values: {
210 | e: ['Error'],
211 | },
212 | },
213 | },
214 |
215 | 'works for regex': {
216 | input: {
217 | a: /hello/g,
218 | },
219 | output: {
220 | a: '/hello/g',
221 | },
222 | outputAnnotations: {
223 | values: {
224 | a: ['regexp'],
225 | },
226 | },
227 | },
228 |
229 | 'works for Infinity': {
230 | input: {
231 | a: Number.POSITIVE_INFINITY,
232 | },
233 | output: {
234 | a: 'Infinity',
235 | },
236 | outputAnnotations: {
237 | values: {
238 | a: ['number'],
239 | },
240 | },
241 | },
242 |
243 | 'works for -Infinity': {
244 | input: {
245 | a: Number.NEGATIVE_INFINITY,
246 | },
247 | output: {
248 | a: '-Infinity',
249 | },
250 | outputAnnotations: {
251 | values: {
252 | a: ['number'],
253 | },
254 | },
255 | },
256 |
257 | 'works for NaN': {
258 | input: {
259 | a: NaN,
260 | },
261 | output: {
262 | a: 'NaN',
263 | },
264 | outputAnnotations: {
265 | values: {
266 | a: ['number'],
267 | },
268 | },
269 | },
270 |
271 | 'works for bigint': {
272 | input: {
273 | a: BigInt('1021312312412312312313'),
274 | },
275 | output: {
276 | a: '1021312312412312312313',
277 | },
278 | outputAnnotations: {
279 | values: {
280 | a: ['bigint'],
281 | },
282 | },
283 | },
284 |
285 | 'works for unknown': {
286 | input: () => {
287 | type Freak = {
288 | name: string;
289 | age: unknown;
290 | };
291 |
292 | const person: Freak = {
293 | name: '@ftonato',
294 | age: 1,
295 | };
296 |
297 | return person;
298 | },
299 | output: {
300 | name: '@ftonato',
301 | age: 1,
302 | },
303 | outputAnnotations: undefined,
304 | },
305 |
306 | 'works for self-referencing objects': {
307 | input: () => {
308 | const a = { role: 'parent', children: [] as any[] };
309 | const b = { role: 'child', parents: [a] };
310 | a.children.push(b);
311 | return a;
312 | },
313 | output: {
314 | role: 'parent',
315 | children: [
316 | {
317 | role: 'child',
318 | parents: [null],
319 | },
320 | ],
321 | },
322 | outputAnnotations: {
323 | referentialEqualities: [['children.0.parents.0']],
324 | },
325 | },
326 |
327 | 'works for Maps with two keys that serialize to the same string but have a different reference': {
328 | input: new Map([
329 | [/a/g, 'foo'],
330 | [/a/g, 'bar'],
331 | ]),
332 | output: [
333 | ['/a/g', 'foo'],
334 | ['/a/g', 'bar'],
335 | ],
336 | outputAnnotations: {
337 | values: [
338 | 'map',
339 | {
340 | '0.0': ['regexp'],
341 | '1.0': ['regexp'],
342 | },
343 | ],
344 | },
345 | },
346 |
347 | "works for Maps with a key that's referentially equal to another field": {
348 | input: () => {
349 | const robbyBubble = { id: 5 };
350 | const highscores = new Map([[robbyBubble, 5000]]);
351 | return {
352 | highscores,
353 | topScorer: robbyBubble,
354 | } as any;
355 | },
356 | output: {
357 | highscores: [[{ id: 5 }, 5000]],
358 | topScorer: { id: 5 },
359 | },
360 | outputAnnotations: {
361 | values: {
362 | highscores: ['map'],
363 | },
364 | referentialEqualities: {
365 | topScorer: ['highscores.0.0'],
366 | },
367 | },
368 | },
369 |
370 | 'works for referentially equal maps': {
371 | input: () => {
372 | const map = new Map([[1, 1]]);
373 | return {
374 | a: map,
375 | b: map,
376 | };
377 | },
378 | output: {
379 | a: [[1, 1]],
380 | b: [[1, 1]],
381 | },
382 | outputAnnotations: {
383 | values: {
384 | a: ['map'],
385 | b: ['map'],
386 | },
387 | referentialEqualities: {
388 | a: ['b'],
389 | },
390 | },
391 | customExpectations: value => {
392 | expect(value.a).toBe(value.b);
393 | },
394 | },
395 |
396 | 'works for maps with non-uniform keys': {
397 | input: {
398 | map: new Map([
399 | [1, 1],
400 | ['1', 1],
401 | ]),
402 | },
403 | output: {
404 | map: [
405 | [1, 1],
406 | ['1', 1],
407 | ],
408 | },
409 | outputAnnotations: {
410 | values: {
411 | map: ['map'],
412 | },
413 | },
414 | },
415 |
416 | 'works for referentially equal values inside a set': {
417 | input: () => {
418 | const user = { id: 2 };
419 | return {
420 | users: new Set([user]),
421 | userOfTheMonth: user,
422 | };
423 | },
424 | output: {
425 | users: [{ id: 2 }],
426 | userOfTheMonth: { id: 2 },
427 | },
428 | outputAnnotations: {
429 | values: {
430 | users: ['set'],
431 | },
432 | referentialEqualities: {
433 | userOfTheMonth: ['users.0'],
434 | },
435 | },
436 | customExpectations: value => {
437 | expect(value.users.values().next().value).toBe(value.userOfTheMonth);
438 | },
439 | },
440 |
441 | 'works for referentially equal values in different maps and sets': {
442 | input: () => {
443 | const user = { id: 2 };
444 |
445 | return {
446 | workspaces: new Map([
447 | [1, { users: new Set([user]) }],
448 | [2, { users: new Set([user]) }],
449 | ]),
450 | };
451 | },
452 | output: {
453 | workspaces: [
454 | [1, { users: [{ id: 2 }] }],
455 | [2, { users: [{ id: 2 }] }],
456 | ],
457 | },
458 | outputAnnotations: {
459 | values: {
460 | workspaces: [
461 | 'map',
462 | {
463 | '0.1.users': ['set'],
464 | '1.1.users': ['set'],
465 | },
466 | ],
467 | },
468 | referentialEqualities: {
469 | 'workspaces.0.1.users.0': ['workspaces.1.1.users.0'],
470 | },
471 | },
472 | },
473 |
474 | 'works for symbols': {
475 | skipOnNode10: true,
476 | input: () => {
477 | const parent = Symbol('Parent');
478 | const child = Symbol('Child');
479 | SuperJSON.registerSymbol(parent, '1');
480 | SuperJSON.registerSymbol(child, '2');
481 |
482 | const a = { role: parent };
483 | const b = { role: child };
484 |
485 | return { a, b };
486 | },
487 | output: {
488 | a: { role: 'Parent' },
489 | b: { role: 'Child' },
490 | },
491 | outputAnnotations: {
492 | values: {
493 | 'a.role': [['symbol', '1']],
494 | 'b.role': [['symbol', '2']],
495 | },
496 | },
497 | },
498 |
499 | 'works for custom transformers': {
500 | input: () => {
501 | SuperJSON.registerCustom(
502 | {
503 | isApplicable: (v): v is ObjectID => v instanceof ObjectID,
504 | serialize: v => v.toHexString(),
505 | deserialize: v => new ObjectID(v),
506 | },
507 | 'objectid'
508 | );
509 |
510 | return {
511 | a: new ObjectID('5f7887f4f0b172093e89f126'),
512 | };
513 | },
514 | output: {
515 | a: '5f7887f4f0b172093e89f126',
516 | },
517 | outputAnnotations: {
518 | values: {
519 | a: [['custom', 'objectid']],
520 | },
521 | },
522 | },
523 |
524 | 'works for Decimal.js': {
525 | input: () => {
526 | SuperJSON.registerCustom(
527 | {
528 | isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
529 | serialize: v => v.toJSON(),
530 | deserialize: v => new Decimal(v),
531 | },
532 | 'decimal.js'
533 | );
534 |
535 | return {
536 | a: new Decimal('100.1'),
537 | };
538 | },
539 | output: {
540 | a: '100.1',
541 | },
542 | outputAnnotations: {
543 | values: {
544 | a: [['custom', 'decimal.js']],
545 | },
546 | },
547 | },
548 |
549 | 'issue #58': {
550 | skipOnNode10: true,
551 | input: () => {
552 | const cool = Symbol('cool');
553 | SuperJSON.registerSymbol(cool);
554 | return {
555 | q: [
556 | 9,
557 | {
558 | henlo: undefined,
559 | yee: new Date(2020, 1, 1),
560 | yee2: new Date(2020, 1, 1),
561 | foo1: new Date(2020, 1, 1),
562 | z: cool,
563 | },
564 | ],
565 | };
566 | },
567 | output: {
568 | q: [
569 | 9,
570 | {
571 | henlo: null,
572 | yee: new Date(2020, 1, 1).toISOString(),
573 | yee2: new Date(2020, 1, 1).toISOString(),
574 | foo1: new Date(2020, 1, 1).toISOString(),
575 | z: 'cool',
576 | },
577 | ],
578 | },
579 | outputAnnotations: {
580 | values: {
581 | 'q.1.henlo': ['undefined'],
582 | 'q.1.yee': ['Date'],
583 | 'q.1.yee2': ['Date'],
584 | 'q.1.foo1': ['Date'],
585 | 'q.1.z': [['symbol', 'cool']],
586 | },
587 | },
588 | },
589 |
590 | 'works with custom allowedProps': {
591 | input: () => {
592 | class User {
593 | constructor(public username: string, public password: string) {}
594 | }
595 | SuperJSON.registerClass(User, { allowProps: ['username'] });
596 | return new User('bongocat', 'supersecurepassword');
597 | },
598 | output: {
599 | username: 'bongocat',
600 | },
601 | outputAnnotations: {
602 | values: [['class', 'User']],
603 | },
604 | customExpectations(value) {
605 | expect(value.password).toBeUndefined();
606 | expect(value.username).toBe('bongocat');
607 | },
608 | dontExpectEquality: true,
609 | },
610 |
611 | 'works with typed arrays': {
612 | input: {
613 | a: new Int8Array([1, 2]),
614 | b: new Uint8ClampedArray(3),
615 | },
616 | output: {
617 | a: [1, 2],
618 | b: [0, 0, 0],
619 | },
620 | outputAnnotations: {
621 | values: {
622 | a: [['typed-array', 'Int8Array']],
623 | b: [['typed-array', 'Uint8ClampedArray']],
624 | },
625 | },
626 | },
627 |
628 | 'works for undefined, issue #48': {
629 | input: undefined,
630 | output: null,
631 | outputAnnotations: { values: ['undefined'] },
632 | },
633 |
634 | 'regression #109: nested classes': {
635 | input: () => {
636 | class Pet {
637 | constructor(private name: string) {}
638 |
639 | woof() {
640 | return this.name;
641 | }
642 | }
643 |
644 | class User {
645 | constructor(public pet: Pet) {}
646 | }
647 |
648 | SuperJSON.registerClass(Pet);
649 | SuperJSON.registerClass(User);
650 |
651 | const pet = new Pet('Rover');
652 | const user = new User(pet);
653 |
654 | return user;
655 | },
656 | output: {
657 | pet: {
658 | name: 'Rover',
659 | },
660 | },
661 | outputAnnotations: {
662 | values: [
663 | ['class', 'User'],
664 | {
665 | pet: [['class', 'Pet']],
666 | },
667 | ],
668 | },
669 | customExpectations(value) {
670 | expect(value.pet.woof()).toEqual('Rover');
671 | },
672 | },
673 | 'works with URL': {
674 | input: {
675 | a: new URL('https://example.com/'),
676 | b: new URL('https://github.com/blitz-js/superjson'),
677 | },
678 | output: {
679 | a: 'https://example.com/',
680 | b: 'https://github.com/blitz-js/superjson',
681 | },
682 | outputAnnotations: {
683 | values: {
684 | a: ['URL'],
685 | b: ['URL'],
686 | },
687 | },
688 | },
689 | };
690 |
691 | function deepFreeze(object: any, alreadySeenObjects = new Set()) {
692 | if (isPrimitive(object)) {
693 | return;
694 | }
695 |
696 | if (isTypedArray(object)) {
697 | return;
698 | }
699 |
700 | if (alreadySeenObjects.has(object)) {
701 | return;
702 | } else {
703 | alreadySeenObjects.add(object);
704 | }
705 |
706 | if (isPlainObject(object)) {
707 | Object.values(object).forEach(o => deepFreeze(o, alreadySeenObjects));
708 | }
709 |
710 | if (isSet(object)) {
711 | object.forEach(o => deepFreeze(o, alreadySeenObjects));
712 | }
713 |
714 | if (isArray(object)) {
715 | object.forEach(o => deepFreeze(o, alreadySeenObjects));
716 | }
717 |
718 | if (isMap(object)) {
719 | object.forEach((value, key) => {
720 | deepFreeze(key, alreadySeenObjects);
721 | deepFreeze(value, alreadySeenObjects);
722 | });
723 | }
724 |
725 | Object.freeze(object);
726 | }
727 |
728 | for (const [
729 | testName,
730 | {
731 | input,
732 | output: expectedOutput,
733 | outputAnnotations: expectedOutputAnnotations,
734 | customExpectations,
735 | skipOnNode10,
736 | dontExpectEquality,
737 | only,
738 | },
739 | ] of Object.entries(cases)) {
740 | let testFunc = test;
741 |
742 | if (skipOnNode10 && isNode10) {
743 | testFunc = test.skip;
744 | }
745 |
746 | if (only) {
747 | testFunc = test.only;
748 | }
749 |
750 | testFunc(testName, () => {
751 | const inputValue = typeof input === 'function' ? input() : input;
752 |
753 | // let's make sure SuperJSON doesn't mutate our input!
754 | deepFreeze(inputValue);
755 | const { json, meta } = SuperJSON.serialize(inputValue);
756 |
757 | if (typeof expectedOutput === 'function') {
758 | expectedOutput(json);
759 | } else {
760 | expect(json).toEqual(expectedOutput);
761 | }
762 | expect(meta).toEqual(expectedOutputAnnotations);
763 |
764 | const untransformed = SuperJSON.deserialize(
765 | JSON.parse(JSON.stringify({ json, meta }))
766 | );
767 | if (!dontExpectEquality) {
768 | expect(untransformed).toEqual(inputValue);
769 | }
770 | customExpectations?.(untransformed);
771 | });
772 | }
773 |
774 | describe('when serializing custom class instances', () => {
775 | it('revives them to their original class', () => {
776 | class Train {
777 | constructor(
778 | private topSpeed: number,
779 | private color: 'red' | 'blue' | 'yellow',
780 | private brand: string
781 | ) {}
782 |
783 | public brag() {
784 | return `I'm a ${this.brand} in freakin' ${this.color} and I go ${this.topSpeed} km/h, isn't that bonkers?`;
785 | }
786 | }
787 |
788 | SuperJSON.registerClass(Train);
789 |
790 | const { json, meta } = SuperJSON.serialize({
791 | s7: new Train(100, 'yellow', 'Bombardier') as any,
792 | });
793 |
794 | expect(json).toEqual({
795 | s7: {
796 | topSpeed: 100,
797 | color: 'yellow',
798 | brand: 'Bombardier',
799 | },
800 | });
801 |
802 | expect(meta).toEqual({
803 | values: {
804 | s7: [['class', 'Train']],
805 | },
806 | });
807 |
808 | const deserialized: any = SuperJSON.deserialize(
809 | JSON.parse(JSON.stringify({ json, meta }))
810 | );
811 | expect(deserialized.s7).toBeInstanceOf(Train);
812 | expect(typeof deserialized.s7.brag()).toBe('string');
813 | });
814 |
815 | describe('with accessor attributes', () => {
816 | it('works', () => {
817 | class Currency {
818 | constructor(private valueInUsd: number) {}
819 |
820 | // @ts-ignore
821 | get inUSD() {
822 | return this.valueInUsd;
823 | }
824 | }
825 |
826 | SuperJSON.registerClass(Currency);
827 |
828 | const { json, meta } = SuperJSON.serialize({
829 | price: new Currency(100) as any,
830 | });
831 |
832 | expect(json).toEqual({
833 | price: {
834 | valueInUsd: 100,
835 | },
836 | });
837 |
838 | const result: any = SuperJSON.parse(JSON.stringify({ json, meta }));
839 |
840 | const price: Currency = result.price;
841 |
842 | expect(price.inUSD).toBe(100);
843 | });
844 | });
845 | });
846 |
847 | describe('when given a non-SuperJSON object', () => {
848 | it.todo('has undefined behaviour');
849 | });
850 |
851 | test('regression #65: BigInt on Safari v13', () => {
852 | const oldBigInt = global.BigInt;
853 | // @ts-ignore
854 | delete global.BigInt;
855 |
856 | const input = {
857 | a: oldBigInt('1000'),
858 | };
859 |
860 | const superJSONed = SuperJSON.serialize(input);
861 | expect(superJSONed).toEqual({
862 | json: {
863 | a: '1000',
864 | },
865 | meta: {
866 | values: {
867 | a: ['bigint'],
868 | },
869 | },
870 | });
871 |
872 | const deserialised = SuperJSON.deserialize(
873 | JSON.parse(JSON.stringify(superJSONed))
874 | );
875 | expect(deserialised).toEqual({
876 | a: '1000',
877 | });
878 |
879 | global.BigInt = oldBigInt;
880 | });
881 |
882 | test('regression #80: Custom error serialisation isnt overriden', () => {
883 | class CustomError extends Error {
884 | constructor(public readonly customProperty: number) {
885 | super("I'm a custom error");
886 | // eslint-disable-next-line es5/no-es6-static-methods
887 | Object.setPrototypeOf(this, CustomError.prototype);
888 | }
889 | }
890 |
891 | expect(new CustomError(10)).toBeInstanceOf(CustomError);
892 |
893 | SuperJSON.registerClass(CustomError);
894 |
895 | const { error } = SuperJSON.deserialize(
896 | SuperJSON.serialize({
897 | error: new CustomError(10),
898 | })
899 | ) as any;
900 |
901 | expect(error).toBeInstanceOf(CustomError);
902 | expect(error.customProperty).toEqual(10);
903 | });
904 | });
905 |
906 | describe('allowErrorProps(...) (#91)', () => {
907 | it('works with simple prop values', () => {
908 | const errorWithAdditionalProps: Error & any = new Error(
909 | 'I have additional props 😄'
910 | );
911 | errorWithAdditionalProps.code = 'P2002';
912 | errorWithAdditionalProps.meta = '👾';
913 |
914 | // same as allowErrorProps("code", "meta")
915 | SuperJSON.allowErrorProps('code');
916 | SuperJSON.allowErrorProps('meta');
917 |
918 | const errorAfterTransition: any = SuperJSON.parse(
919 | SuperJSON.stringify(errorWithAdditionalProps)
920 | );
921 |
922 | expect(errorAfterTransition).toBeInstanceOf(Error);
923 | expect(errorAfterTransition.message).toEqual('I have additional props 😄');
924 | expect(errorAfterTransition.code).toEqual('P2002');
925 | expect(errorAfterTransition.meta).toEqual('👾');
926 | });
927 |
928 | it.skip('works with complex prop values', () => {
929 | const errorWithAdditionalProps: any = new Error();
930 | errorWithAdditionalProps.map = new Map();
931 |
932 | SuperJSON.allowErrorProps('map');
933 |
934 | const errorAfterTransition: any = SuperJSON.parse(
935 | SuperJSON.stringify(errorWithAdditionalProps)
936 | );
937 |
938 | expect(errorAfterTransition.map).toEqual(undefined);
939 |
940 | expect(errorAfterTransition.map).toBeInstanceOf(Map);
941 | });
942 | });
943 |
944 | test('regression #83: negative zero', () => {
945 | const input = -0;
946 |
947 | const stringified = SuperJSON.stringify(input);
948 | expect(stringified).toMatchInlineSnapshot(
949 | `"{\\"json\\":\\"-0\\",\\"meta\\":{\\"values\\":[\\"number\\"]}}"`
950 | );
951 |
952 | const parsed: number = SuperJSON.parse(stringified);
953 |
954 | expect(1 / parsed).toBe(-Infinity);
955 | });
956 |
957 | test('regression https://github.com/blitz-js/babel-plugin-superjson-next/issues/63: Nested BigInt', () => {
958 | const serialized = SuperJSON.serialize({
959 | topics: [
960 | {
961 | post_count: BigInt('22'),
962 | },
963 | ],
964 | });
965 |
966 | expect(() => JSON.stringify(serialized)).not.toThrow();
967 |
968 | expect(typeof (serialized.json as any).topics[0].post_count).toBe('string');
969 | expect(serialized.json).toEqual({
970 | topics: [
971 | {
972 | post_count: '22',
973 | },
974 | ],
975 | });
976 |
977 | SuperJSON.deserialize(serialized);
978 | expect(typeof (serialized.json as any).topics[0].post_count).toBe('string');
979 | });
980 |
981 | test('performance regression', () => {
982 | const data: any[] = [];
983 | for (let i = 0; i < 100; i++) {
984 | let nested1 = [];
985 | let nested2 = [];
986 | for (let j = 0; j < 10; j++) {
987 | nested1[j] = {
988 | createdAt: new Date(),
989 | updatedAt: new Date(),
990 | innerNested: {
991 | createdAt: new Date(),
992 | updatedAt: new Date(),
993 | },
994 | };
995 | nested2[j] = {
996 | createdAt: new Date(),
997 | updatedAt: new Date(),
998 | innerNested: {
999 | createdAt: new Date(),
1000 | updatedAt: new Date(),
1001 | },
1002 | };
1003 | }
1004 | const object = {
1005 | createdAt: new Date(),
1006 | updatedAt: new Date(),
1007 | nested1,
1008 | nested2,
1009 | };
1010 | data.push(object);
1011 | }
1012 |
1013 | const t1 = Date.now();
1014 | SuperJSON.serialize(data);
1015 | const t2 = Date.now();
1016 | const duration = t2 - t1;
1017 | expect(duration).toBeLessThan(700);
1018 | });
1019 |
1020 | test('regression #95: no undefined', () => {
1021 | const input: unknown[] = [];
1022 |
1023 | const out = SuperJSON.serialize(input);
1024 | expect(out).not.toHaveProperty('meta');
1025 |
1026 | const parsed: number = SuperJSON.deserialize(out);
1027 |
1028 | expect(parsed).toEqual(input);
1029 | });
1030 |
1031 | test('regression #108: Error#stack should not be included by default', () => {
1032 | const input = new Error("Beep boop, you don't wanna see me. I'm an error!");
1033 | expect(input).toHaveProperty('stack');
1034 |
1035 | const { stack: thatShouldBeUndefined } = SuperJSON.parse(
1036 | SuperJSON.stringify(input)
1037 | ) as any;
1038 | expect(thatShouldBeUndefined).toBeUndefined();
1039 |
1040 | SuperJSON.allowErrorProps('stack');
1041 | const { stack: thatShouldExist } = SuperJSON.parse(
1042 | SuperJSON.stringify(input)
1043 | ) as any;
1044 | expect(thatShouldExist).toEqual(input.stack);
1045 | });
1046 |
1047 | test('regression: `Object.create(null)` / object without prototype', () => {
1048 | const input: Record = Object.create(null);
1049 | input.date = new Date();
1050 |
1051 | const stringified = SuperJSON.stringify(input);
1052 | const parsed: any = SuperJSON.parse(stringified);
1053 |
1054 | expect(parsed.date).toBeInstanceOf(Date);
1055 | });
1056 |
1057 | test.each(['__proto__', 'prototype', 'constructor'])(
1058 | 'serialize prototype pollution: %s',
1059 | forbidden => {
1060 | expect(() => {
1061 | SuperJSON.serialize({
1062 | [forbidden]: 1,
1063 | });
1064 | }).toThrowError(/This is a prototype pollution risk/);
1065 | }
1066 | );
1067 |
1068 | test('prototype pollution - __proto__', () => {
1069 | expect(() => {
1070 | SuperJSON.parse(
1071 | JSON.stringify({
1072 | json: {
1073 | myValue: 1337,
1074 | },
1075 | meta: {
1076 | referentialEqualities: {
1077 | myValue: ['__proto__.x'],
1078 | },
1079 | },
1080 | })
1081 | );
1082 | }).toThrowErrorMatchingInlineSnapshot(
1083 | `"__proto__ is not allowed as a property"`
1084 | );
1085 | expect((Object.prototype as any).x).toBeUndefined();
1086 | });
1087 |
1088 | test('prototype pollution - prototype', () => {
1089 | expect(() => {
1090 | SuperJSON.parse(
1091 | JSON.stringify({
1092 | json: {
1093 | myValue: 1337,
1094 | },
1095 | meta: {
1096 | referentialEqualities: {
1097 | myValue: ['prototype.x'],
1098 | },
1099 | },
1100 | })
1101 | );
1102 | }).toThrowErrorMatchingInlineSnapshot(
1103 | `"prototype is not allowed as a property"`
1104 | );
1105 | });
1106 |
1107 | test('prototype pollution - constructor', () => {
1108 | expect(() => {
1109 | SuperJSON.parse(
1110 | JSON.stringify({
1111 | json: {
1112 | myValue: 1337,
1113 | },
1114 | meta: {
1115 | referentialEqualities: {
1116 | myValue: ['constructor.prototype.x'],
1117 | },
1118 | },
1119 | })
1120 | );
1121 | }).toThrowErrorMatchingInlineSnapshot(
1122 | `"prototype is not allowed as a property"`
1123 | );
1124 |
1125 | expect((Object.prototype as any).x).toBeUndefined();
1126 | });
1127 |
1128 | test('superjson instances are independent of one another', () => {
1129 | class Car {}
1130 | const s1 = new SuperJSON();
1131 | s1.registerClass(Car);
1132 |
1133 | const s2 = new SuperJSON();
1134 |
1135 | const value = {
1136 | car: new Car(),
1137 | };
1138 |
1139 | const res1 = s1.serialize(value);
1140 | expect(res1.meta?.values).toEqual({ car: [['class', 'Car']] });
1141 | const res2 = s2.serialize(value);
1142 | expect(res2.json).toEqual(value);
1143 | });
1144 |
1145 | test('regression #245: superjson referential equalities only use the top-most parent node', () => {
1146 | type Node = {
1147 | children: Node[];
1148 | };
1149 | const root: Node = {
1150 | children: [],
1151 | };
1152 | const input = {
1153 | a: root,
1154 | b: root,
1155 | };
1156 | const res = SuperJSON.serialize(input);
1157 |
1158 | expect(res.meta?.referentialEqualities).toHaveProperty(['a']);
1159 |
1160 | // saying that a.children is equal to b.children is redundant since its already know that a === b
1161 | expect(res.meta?.referentialEqualities).not.toHaveProperty(['a.children']);
1162 | expect(res.meta).toMatchInlineSnapshot(`
1163 | {
1164 | "referentialEqualities": {
1165 | "a": [
1166 | "b",
1167 | ],
1168 | },
1169 | }
1170 | `);
1171 |
1172 | const parsed = SuperJSON.deserialize(res);
1173 | expect(parsed).toEqual(input);
1174 | });
1175 |
1176 | test('dedupe=true', () => {
1177 | const instance = new SuperJSON({
1178 | dedupe: true,
1179 | });
1180 |
1181 | type Node = {
1182 | children: Node[];
1183 | };
1184 | const root: Node = {
1185 | children: [],
1186 | };
1187 | const input = {
1188 | a: root,
1189 | b: root,
1190 | };
1191 | const output = instance.serialize(input);
1192 |
1193 | const json = output.json as any;
1194 |
1195 | expect(json.a);
1196 |
1197 | // This has already been seen and should be deduped
1198 | expect(json.b).toBeNull();
1199 |
1200 | expect(json).toMatchInlineSnapshot(`
1201 | {
1202 | "a": {
1203 | "children": [],
1204 | },
1205 | "b": null,
1206 | }
1207 | `);
1208 |
1209 | expect(instance.deserialize(output)).toEqual(input);
1210 | });
1211 |
1212 | test('dedupe=true on a large complicated schema', () => {
1213 | const content = fs.readFileSync(__dirname + '/non-deduped-cal.json', 'utf-8');
1214 | const parsed = JSON.parse(content);
1215 |
1216 | const deserialized = SuperJSON.deserialize(parsed);
1217 |
1218 | const nondeduped = new SuperJSON({});
1219 |
1220 | const deduped = new SuperJSON({
1221 | dedupe: true,
1222 | });
1223 |
1224 | const nondedupedOut = nondeduped.deserialize(
1225 | nondeduped.serialize(deserialized)
1226 | );
1227 | const dedupedOut = deduped.deserialize(deduped.serialize(deserialized));
1228 |
1229 | expect(nondedupedOut).toEqual(deserialized);
1230 | expect(dedupedOut).toEqual(deserialized);
1231 | });
1232 |
1233 | test('doesnt iterate to keys that dont exist', () => {
1234 | const robbyBubble = { id: 5 };
1235 | const highscores = new Map([[robbyBubble, 5000]]);
1236 | const objectWithReferentialEquality = { highscores, topScorer: robbyBubble };
1237 | const res = SuperJSON.serialize(objectWithReferentialEquality);
1238 |
1239 | expect(res.meta.referentialEqualities.topScorer).toEqual(['highscores.0.0']);
1240 | res.meta.referentialEqualities.topScorer = ['highscores.99999.0'];
1241 |
1242 | expect(() => SuperJSON.deserialize(res)).toThrowError('index out of bounds');
1243 | });
1244 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Class, JSONValue, SuperJSONResult, SuperJSONValue } from './types.js';
2 | import { ClassRegistry, RegisterOptions } from './class-registry.js';
3 | import { Registry } from './registry.js';
4 | import {
5 | CustomTransfomer,
6 | CustomTransformerRegistry,
7 | } from './custom-transformer-registry.js';
8 | import {
9 | applyReferentialEqualityAnnotations,
10 | applyValueAnnotations,
11 | generateReferentialEqualityAnnotations,
12 | walker,
13 | } from './plainer.js';
14 | import { copy } from 'copy-anything';
15 |
16 | export default class SuperJSON {
17 | /**
18 | * If true, SuperJSON will make sure only one instance of referentially equal objects are serialized and the rest are replaced with `null`.
19 | */
20 | private readonly dedupe: boolean;
21 |
22 | /**
23 | * @param dedupeReferentialEqualities If true, SuperJSON will make sure only one instance of referentially equal objects are serialized and the rest are replaced with `null`.
24 | */
25 | constructor({
26 | dedupe = false,
27 | }: {
28 | dedupe?: boolean;
29 | } = {}) {
30 | this.dedupe = dedupe;
31 | }
32 |
33 | serialize(object: SuperJSONValue): SuperJSONResult {
34 | const identities = new Map();
35 | const output = walker(object, identities, this, this.dedupe);
36 | const res: SuperJSONResult = {
37 | json: output.transformedValue,
38 | };
39 |
40 | if (output.annotations) {
41 | res.meta = {
42 | ...res.meta,
43 | values: output.annotations,
44 | };
45 | }
46 |
47 | const equalityAnnotations = generateReferentialEqualityAnnotations(
48 | identities,
49 | this.dedupe
50 | );
51 | if (equalityAnnotations) {
52 | res.meta = {
53 | ...res.meta,
54 | referentialEqualities: equalityAnnotations,
55 | };
56 | }
57 |
58 | return res;
59 | }
60 |
61 | deserialize(payload: SuperJSONResult): T {
62 | const { json, meta } = payload;
63 |
64 | let result: T = copy(json) as any;
65 |
66 | if (meta?.values) {
67 | result = applyValueAnnotations(result, meta.values, this);
68 | }
69 |
70 | if (meta?.referentialEqualities) {
71 | result = applyReferentialEqualityAnnotations(
72 | result,
73 | meta.referentialEqualities
74 | );
75 | }
76 |
77 | return result;
78 | }
79 |
80 | stringify(object: SuperJSONValue): string {
81 | return JSON.stringify(this.serialize(object));
82 | }
83 |
84 | parse(string: string): T {
85 | return this.deserialize(JSON.parse(string));
86 | }
87 |
88 | readonly classRegistry = new ClassRegistry();
89 | registerClass(v: Class, options?: RegisterOptions | string) {
90 | this.classRegistry.register(v, options);
91 | }
92 |
93 | readonly symbolRegistry = new Registry(s => s.description ?? '');
94 | registerSymbol(v: Symbol, identifier?: string) {
95 | this.symbolRegistry.register(v, identifier);
96 | }
97 |
98 | readonly customTransformerRegistry = new CustomTransformerRegistry();
99 | registerCustom(
100 | transformer: Omit, 'name'>,
101 | name: string
102 | ) {
103 | this.customTransformerRegistry.register({
104 | name,
105 | ...transformer,
106 | });
107 | }
108 |
109 | readonly allowedErrorProps: string[] = [];
110 | allowErrorProps(...props: string[]) {
111 | this.allowedErrorProps.push(...props);
112 | }
113 |
114 | private static defaultInstance = new SuperJSON();
115 | static serialize = SuperJSON.defaultInstance.serialize.bind(
116 | SuperJSON.defaultInstance
117 | );
118 | static deserialize = SuperJSON.defaultInstance.deserialize.bind(
119 | SuperJSON.defaultInstance
120 | );
121 | static stringify = SuperJSON.defaultInstance.stringify.bind(
122 | SuperJSON.defaultInstance
123 | );
124 | static parse = SuperJSON.defaultInstance.parse.bind(
125 | SuperJSON.defaultInstance
126 | );
127 | static registerClass = SuperJSON.defaultInstance.registerClass.bind(
128 | SuperJSON.defaultInstance
129 | );
130 | static registerSymbol = SuperJSON.defaultInstance.registerSymbol.bind(
131 | SuperJSON.defaultInstance
132 | );
133 | static registerCustom = SuperJSON.defaultInstance.registerCustom.bind(
134 | SuperJSON.defaultInstance
135 | );
136 | static allowErrorProps = SuperJSON.defaultInstance.allowErrorProps.bind(
137 | SuperJSON.defaultInstance
138 | );
139 | }
140 |
141 | export { SuperJSON, SuperJSONResult };
142 |
143 | export const serialize = SuperJSON.serialize;
144 | export const deserialize = SuperJSON.deserialize;
145 |
146 | export const stringify = SuperJSON.stringify;
147 | export const parse = SuperJSON.parse;
148 |
149 | export const registerClass = SuperJSON.registerClass;
150 | export const registerCustom = SuperJSON.registerCustom;
151 | export const registerSymbol = SuperJSON.registerSymbol;
152 | export const allowErrorProps = SuperJSON.allowErrorProps;
153 |
--------------------------------------------------------------------------------
/src/is.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | isArray,
3 | isBoolean,
4 | isDate,
5 | isNull,
6 | isNumber,
7 | isPrimitive,
8 | isRegExp,
9 | isString,
10 | isSymbol,
11 | isUndefined,
12 | isPlainObject,
13 | isTypedArray,
14 | isURL,
15 | } from './is.js';
16 |
17 | import { test, expect } from 'vitest';
18 |
19 | test('Basic true tests', () => {
20 | expect(isUndefined(undefined)).toBe(true);
21 | expect(isNull(null)).toBe(true);
22 |
23 | expect(isArray([])).toBe(true);
24 | expect(isArray([])).toBe(true);
25 | expect(isString('')).toBe(true);
26 | expect(isString('_')).toBe(true);
27 |
28 | expect(isBoolean(true)).toBe(true);
29 | expect(isBoolean(false)).toBe(true);
30 | expect(isRegExp(/./)).toBe(true);
31 | expect(isRegExp(/./gi)).toBe(true);
32 | expect(isNumber(0)).toBe(true);
33 | expect(isNumber(1)).toBe(true);
34 | expect(isDate(new Date())).toBe(true);
35 | expect(isSymbol(Symbol())).toBe(true);
36 | expect(isTypedArray(new Uint8Array())).toBe(true);
37 | expect(isURL(new URL('https://example.com'))).toBe(true);
38 | expect(isPlainObject({})).toBe(true);
39 | // eslint-disable-next-line no-new-object
40 | expect(isPlainObject(new Object())).toBe(true);
41 | });
42 |
43 | test('Basic false tests', () => {
44 | expect(isNumber(NaN)).toBe(false);
45 | expect(isDate(new Date('_'))).toBe(false);
46 | expect(isDate(NaN)).toBe(false);
47 | expect(isUndefined(NaN)).toBe(false);
48 | expect(isNull(NaN)).toBe(false);
49 |
50 | expect(isArray(NaN)).toBe(false);
51 | expect(isString(NaN)).toBe(false);
52 |
53 | expect(isBoolean(NaN)).toBe(false);
54 | expect(isRegExp(NaN)).toBe(false);
55 | expect(isSymbol(NaN)).toBe(false);
56 |
57 | expect(isTypedArray([])).toBe(false);
58 |
59 | expect(isURL('https://example.com')).toBe(false);
60 |
61 | expect(isPlainObject(null)).toBe(false);
62 | expect(isPlainObject([])).toBe(false);
63 | expect(isPlainObject(Object.prototype)).toBe(false);
64 | expect(isPlainObject(Object.create(Array.prototype))).toBe(false);
65 | });
66 |
67 | test('Primitive tests', () => {
68 | expect(isPrimitive(0)).toBe(true);
69 | expect(isPrimitive('')).toBe(true);
70 | expect(isPrimitive('str')).toBe(true);
71 | expect(isPrimitive(Symbol())).toBe(true);
72 | expect(isPrimitive(true)).toBe(true);
73 | expect(isPrimitive(false)).toBe(true);
74 | expect(isPrimitive(null)).toBe(true);
75 | expect(isPrimitive(undefined)).toBe(true);
76 |
77 | expect(isPrimitive(NaN)).toBe(false);
78 | expect(isPrimitive([])).toBe(false);
79 | expect(isPrimitive([])).toBe(false);
80 | expect(isPrimitive({})).toBe(false);
81 | // eslint-disable-next-line no-new-object
82 | expect(isPrimitive(new Object())).toBe(false);
83 | expect(isPrimitive(new Date())).toBe(false);
84 | expect(isPrimitive(() => {})).toBe(false);
85 | });
86 |
87 | test('Date exception', () => {
88 | expect(isDate(new Date('_'))).toBe(false);
89 | });
90 |
91 | test('Regression: null-prototype object', () => {
92 | expect(isPlainObject(Object.create(null))).toBe(true);
93 | expect(isPrimitive(Object.create(null))).toBe(false);
94 | });
95 |
--------------------------------------------------------------------------------
/src/is.ts:
--------------------------------------------------------------------------------
1 | const getType = (payload: any): string =>
2 | Object.prototype.toString.call(payload).slice(8, -1);
3 |
4 | export const isUndefined = (payload: any): payload is undefined =>
5 | typeof payload === 'undefined';
6 |
7 | export const isNull = (payload: any): payload is null => payload === null;
8 |
9 | export const isPlainObject = (
10 | payload: any
11 | ): payload is { [key: string]: any } => {
12 | if (typeof payload !== 'object' || payload === null) return false;
13 | if (payload === Object.prototype) return false;
14 | if (Object.getPrototypeOf(payload) === null) return true;
15 |
16 | return Object.getPrototypeOf(payload) === Object.prototype;
17 | };
18 |
19 | export const isEmptyObject = (payload: any): payload is {} =>
20 | isPlainObject(payload) && Object.keys(payload).length === 0;
21 |
22 | export const isArray = (payload: any): payload is any[] =>
23 | Array.isArray(payload);
24 |
25 | export const isString = (payload: any): payload is string =>
26 | typeof payload === 'string';
27 |
28 | export const isNumber = (payload: any): payload is number =>
29 | typeof payload === 'number' && !isNaN(payload);
30 |
31 | export const isBoolean = (payload: any): payload is boolean =>
32 | typeof payload === 'boolean';
33 |
34 | export const isRegExp = (payload: any): payload is RegExp =>
35 | payload instanceof RegExp;
36 |
37 | export const isMap = (payload: any): payload is Map =>
38 | payload instanceof Map;
39 |
40 | export const isSet = (payload: any): payload is Set =>
41 | payload instanceof Set;
42 |
43 | export const isSymbol = (payload: any): payload is symbol =>
44 | getType(payload) === 'Symbol';
45 |
46 | export const isDate = (payload: any): payload is Date =>
47 | payload instanceof Date && !isNaN(payload.valueOf());
48 |
49 | export const isError = (payload: any): payload is Error =>
50 | payload instanceof Error;
51 |
52 | export const isNaNValue = (payload: any): payload is typeof NaN =>
53 | typeof payload === 'number' && isNaN(payload);
54 |
55 | export const isPrimitive = (
56 | payload: any
57 | ): payload is boolean | null | undefined | number | string | symbol =>
58 | isBoolean(payload) ||
59 | isNull(payload) ||
60 | isUndefined(payload) ||
61 | isNumber(payload) ||
62 | isString(payload) ||
63 | isSymbol(payload);
64 |
65 | export const isBigint = (payload: any): payload is bigint =>
66 | typeof payload === 'bigint';
67 |
68 | export const isInfinite = (payload: any): payload is number =>
69 | payload === Infinity || payload === -Infinity;
70 |
71 | export type TypedArrayConstructor =
72 | | Int8ArrayConstructor
73 | | Uint8ArrayConstructor
74 | | Uint8ClampedArrayConstructor
75 | | Int16ArrayConstructor
76 | | Uint16ArrayConstructor
77 | | Int32ArrayConstructor
78 | | Uint32ArrayConstructor
79 | | Float32ArrayConstructor
80 | | Float64ArrayConstructor;
81 |
82 | export type TypedArray = InstanceType;
83 |
84 | export const isTypedArray = (payload: any): payload is TypedArray =>
85 | ArrayBuffer.isView(payload) && !(payload instanceof DataView);
86 |
87 | export const isURL = (payload: any): payload is URL => payload instanceof URL;
88 |
--------------------------------------------------------------------------------
/src/pathstringifier.test.ts:
--------------------------------------------------------------------------------
1 | import { parsePath, escapeKey } from './pathstringifier.js';
2 |
3 | import { test, describe, it, expect } from 'vitest';
4 |
5 | describe('parsePath', () => {
6 | it.each([
7 | ['test.a.b', ['test', 'a', 'b']],
8 | ['test\\.a.b', ['test.a', 'b']],
9 | ['test\\\\.a.b', ['test\\.a', 'b']],
10 | ['test\\a.b', ['test\\a', 'b']],
11 | ['test\\\\a.b', ['test\\\\a', 'b']],
12 | ])('parsePath(%p) === %p', (input, expectedOutput) => {
13 | expect(parsePath(input)).toEqual(expectedOutput);
14 | });
15 | });
16 |
17 | describe('escapeKey', () => {
18 | test.each([
19 | ['dontescape', 'dontescape'],
20 | ['escape.me', 'escape\\.me'],
21 | ])('escapeKey(%s) === %s', (input, expectedOutput) => {
22 | expect(escapeKey(input)).toEqual(expectedOutput);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/pathstringifier.ts:
--------------------------------------------------------------------------------
1 | export type StringifiedPath = string;
2 | type Path = string[];
3 |
4 | export const escapeKey = (key: string) => key.replace(/\./g, '\\.');
5 |
6 | export const stringifyPath = (path: Path): StringifiedPath =>
7 | path
8 | .map(String)
9 | .map(escapeKey)
10 | .join('.');
11 |
12 | export const parsePath = (string: StringifiedPath) => {
13 | const result: string[] = [];
14 |
15 | let segment = '';
16 | for (let i = 0; i < string.length; i++) {
17 | let char = string.charAt(i);
18 |
19 | const isEscapedDot = char === '\\' && string.charAt(i + 1) === '.';
20 | if (isEscapedDot) {
21 | segment += '.';
22 | i++;
23 | continue;
24 | }
25 |
26 | const isEndOfSegment = char === '.';
27 | if (isEndOfSegment) {
28 | result.push(segment);
29 | segment = '';
30 | continue;
31 | }
32 |
33 | segment += char;
34 | }
35 |
36 | const lastSegment = segment;
37 | result.push(lastSegment);
38 |
39 | return result;
40 | };
41 |
--------------------------------------------------------------------------------
/src/plainer.spec.ts:
--------------------------------------------------------------------------------
1 | import SuperJSON from './index.js';
2 | import { walker } from './plainer.js';
3 |
4 | import { test, expect } from 'vitest';
5 |
6 | test('walker', () => {
7 | expect(
8 | walker(
9 | {
10 | a: new Map([[NaN, null]]),
11 | b: /test/g,
12 | },
13 | new Map(),
14 | new SuperJSON(),
15 | false
16 | )
17 | ).toEqual({
18 | transformedValue: {
19 | a: [['NaN', null]],
20 | b: '/test/g',
21 | },
22 | annotations: {
23 | a: [
24 | 'map',
25 | {
26 | '0.0': ['number'],
27 | },
28 | ],
29 | b: ['regexp'],
30 | },
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/plainer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | isArray,
3 | isEmptyObject,
4 | isMap,
5 | isPlainObject,
6 | isPrimitive,
7 | isSet,
8 | } from './is.js';
9 | import { escapeKey, stringifyPath } from './pathstringifier.js';
10 | import {
11 | isInstanceOfRegisteredClass,
12 | transformValue,
13 | TypeAnnotation,
14 | untransformValue,
15 | } from './transformer.js';
16 | import { includes, forEach } from './util.js';
17 | import { parsePath } from './pathstringifier.js';
18 | import { getDeep, setDeep } from './accessDeep.js';
19 | import SuperJSON from './index.js';
20 |
21 | type Tree = InnerNode | Leaf;
22 | type Leaf = [T];
23 | type InnerNode = [T, Record>];
24 |
25 | export type MinimisedTree = Tree | Record> | undefined;
26 |
27 | function traverse(
28 | tree: MinimisedTree,
29 | walker: (v: T, path: string[]) => void,
30 | origin: string[] = []
31 | ): void {
32 | if (!tree) {
33 | return;
34 | }
35 |
36 | if (!isArray(tree)) {
37 | forEach(tree, (subtree, key) =>
38 | traverse(subtree, walker, [...origin, ...parsePath(key)])
39 | );
40 | return;
41 | }
42 |
43 | const [nodeValue, children] = tree;
44 | if (children) {
45 | forEach(children, (child, key) => {
46 | traverse(child, walker, [...origin, ...parsePath(key)]);
47 | });
48 | }
49 |
50 | walker(nodeValue, origin);
51 | }
52 |
53 | export function applyValueAnnotations(
54 | plain: any,
55 | annotations: MinimisedTree,
56 | superJson: SuperJSON
57 | ) {
58 | traverse(annotations, (type, path) => {
59 | plain = setDeep(plain, path, v => untransformValue(v, type, superJson));
60 | });
61 |
62 | return plain;
63 | }
64 |
65 | export function applyReferentialEqualityAnnotations(
66 | plain: any,
67 | annotations: ReferentialEqualityAnnotations
68 | ) {
69 | function apply(identicalPaths: string[], path: string) {
70 | const object = getDeep(plain, parsePath(path));
71 |
72 | identicalPaths.map(parsePath).forEach(identicalObjectPath => {
73 | plain = setDeep(plain, identicalObjectPath, () => object);
74 | });
75 | }
76 |
77 | if (isArray(annotations)) {
78 | const [root, other] = annotations;
79 | root.forEach(identicalPath => {
80 | plain = setDeep(plain, parsePath(identicalPath), () => plain);
81 | });
82 |
83 | if (other) {
84 | forEach(other, apply);
85 | }
86 | } else {
87 | forEach(annotations, apply);
88 | }
89 |
90 | return plain;
91 | }
92 |
93 | const isDeep = (object: any, superJson: SuperJSON): boolean =>
94 | isPlainObject(object) ||
95 | isArray(object) ||
96 | isMap(object) ||
97 | isSet(object) ||
98 | isInstanceOfRegisteredClass(object, superJson);
99 |
100 | function addIdentity(object: any, path: any[], identities: Map) {
101 | const existingSet = identities.get(object);
102 |
103 | if (existingSet) {
104 | existingSet.push(path);
105 | } else {
106 | identities.set(object, [path]);
107 | }
108 | }
109 |
110 | interface Result {
111 | transformedValue: any;
112 | annotations?: MinimisedTree;
113 | }
114 |
115 | export type ReferentialEqualityAnnotations =
116 | | Record
117 | | [string[]]
118 | | [string[], Record];
119 |
120 | export function generateReferentialEqualityAnnotations(
121 | identitites: Map,
122 | dedupe: boolean
123 | ): ReferentialEqualityAnnotations | undefined {
124 | const result: Record = {};
125 | let rootEqualityPaths: string[] | undefined = undefined;
126 |
127 | identitites.forEach(paths => {
128 | if (paths.length <= 1) {
129 | return;
130 | }
131 |
132 | // if we're not deduping, all of these objects continue existing.
133 | // putting the shortest path first makes it easier to parse for humans
134 | // if we're deduping though, only the first entry will still exist, so we can't do this optimisation.
135 | if (!dedupe) {
136 | paths = paths
137 | .map(path => path.map(String))
138 | .sort((a, b) => a.length - b.length);
139 | }
140 |
141 | const [representativePath, ...identicalPaths] = paths;
142 |
143 | if (representativePath.length === 0) {
144 | rootEqualityPaths = identicalPaths.map(stringifyPath);
145 | } else {
146 | result[stringifyPath(representativePath)] = identicalPaths.map(
147 | stringifyPath
148 | );
149 | }
150 | });
151 |
152 | if (rootEqualityPaths) {
153 | if (isEmptyObject(result)) {
154 | return [rootEqualityPaths];
155 | } else {
156 | return [rootEqualityPaths, result];
157 | }
158 | } else {
159 | return isEmptyObject(result) ? undefined : result;
160 | }
161 | }
162 |
163 | export const walker = (
164 | object: any,
165 | identities: Map,
166 | superJson: SuperJSON,
167 | dedupe: boolean,
168 | path: any[] = [],
169 | objectsInThisPath: any[] = [],
170 | seenObjects = new Map()
171 | ): Result => {
172 | const primitive = isPrimitive(object);
173 |
174 | if (!primitive) {
175 | addIdentity(object, path, identities);
176 |
177 | const seen = seenObjects.get(object);
178 | if (seen) {
179 | // short-circuit result if we've seen this object before
180 | return dedupe
181 | ? {
182 | transformedValue: null,
183 | }
184 | : seen;
185 | }
186 | }
187 |
188 | if (!isDeep(object, superJson)) {
189 | const transformed = transformValue(object, superJson);
190 |
191 | const result: Result = transformed
192 | ? {
193 | transformedValue: transformed.value,
194 | annotations: [transformed.type],
195 | }
196 | : {
197 | transformedValue: object,
198 | };
199 | if (!primitive) {
200 | seenObjects.set(object, result);
201 | }
202 | return result;
203 | }
204 |
205 | if (includes(objectsInThisPath, object)) {
206 | // prevent circular references
207 | return {
208 | transformedValue: null,
209 | };
210 | }
211 |
212 | const transformationResult = transformValue(object, superJson);
213 | const transformed = transformationResult?.value ?? object;
214 |
215 | const transformedValue: any = isArray(transformed) ? [] : {};
216 | const innerAnnotations: Record> = {};
217 |
218 | forEach(transformed, (value, index) => {
219 | if (
220 | index === '__proto__' ||
221 | index === 'constructor' ||
222 | index === 'prototype'
223 | ) {
224 | throw new Error(
225 | `Detected property ${index}. This is a prototype pollution risk, please remove it from your object.`
226 | );
227 | }
228 |
229 | const recursiveResult = walker(
230 | value,
231 | identities,
232 | superJson,
233 | dedupe,
234 | [...path, index],
235 | [...objectsInThisPath, object],
236 | seenObjects
237 | );
238 |
239 | transformedValue[index] = recursiveResult.transformedValue;
240 |
241 | if (isArray(recursiveResult.annotations)) {
242 | innerAnnotations[index] = recursiveResult.annotations;
243 | } else if (isPlainObject(recursiveResult.annotations)) {
244 | forEach(recursiveResult.annotations, (tree, key) => {
245 | innerAnnotations[escapeKey(index) + '.' + key] = tree;
246 | });
247 | }
248 | });
249 |
250 | const result: Result = isEmptyObject(innerAnnotations)
251 | ? {
252 | transformedValue,
253 | annotations: !!transformationResult
254 | ? [transformationResult.type]
255 | : undefined,
256 | }
257 | : {
258 | transformedValue,
259 | annotations: !!transformationResult
260 | ? [transformationResult.type, innerAnnotations]
261 | : innerAnnotations,
262 | };
263 | if (!primitive) {
264 | seenObjects.set(object, result);
265 | }
266 |
267 | return result;
268 | };
269 |
--------------------------------------------------------------------------------
/src/registry.test.ts:
--------------------------------------------------------------------------------
1 | import { Registry } from './registry.js';
2 | import { Class } from './types.js';
3 |
4 | import { test, expect } from 'vitest';
5 |
6 | test('class registry', () => {
7 | const registry = new Registry(c => c.name);
8 |
9 | class Car {
10 | honk() {
11 | console.log('honk');
12 | }
13 | }
14 | registry.register(Car);
15 |
16 | expect(registry.getValue('Car')).toBe(Car);
17 | expect(registry.getIdentifier(Car)).toBe('Car');
18 |
19 | expect(() => registry.register(Car)).not.toThrow();
20 |
21 | registry.register(class Car {}, 'car2');
22 |
23 | expect(registry.getValue('car2')).not.toBeUndefined();
24 |
25 | registry.clear();
26 |
27 | expect(registry.getValue('car1')).toBeUndefined();
28 | });
29 |
--------------------------------------------------------------------------------
/src/registry.ts:
--------------------------------------------------------------------------------
1 | import { DoubleIndexedKV } from './double-indexed-kv.js';
2 |
3 | export class Registry {
4 | private kv = new DoubleIndexedKV();
5 |
6 | constructor(private readonly generateIdentifier: (v: T) => string) {}
7 |
8 | register(value: T, identifier?: string): void {
9 | if (this.kv.getByValue(value)) {
10 | return;
11 | }
12 |
13 | if (!identifier) {
14 | identifier = this.generateIdentifier(value);
15 | }
16 |
17 | this.kv.set(identifier, value);
18 | }
19 |
20 | clear(): void {
21 | this.kv.clear();
22 | }
23 |
24 | getIdentifier(value: T) {
25 | return this.kv.getByValue(value);
26 | }
27 |
28 | getValue(identifier: string) {
29 | return this.kv.getByKey(identifier);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/transformer.test.ts:
--------------------------------------------------------------------------------
1 | import SuperJSON from './index.js';
2 |
3 | import { test, expect } from 'vitest';
4 |
5 | test('throws an descriptive error when transforming', () => {
6 | const instance = new SuperJSON();
7 | class FunnyNumber {
8 | constructor(private number: number) {}
9 |
10 | // @ts-ignore
11 | get theNumber() {
12 | return this.number;
13 | }
14 | }
15 | instance.registerClass(FunnyNumber);
16 | expect(() =>
17 | instance.deserialize({
18 | json: instance.serialize({
19 | number: new FunnyNumber(2137),
20 | }).json,
21 | meta: {
22 | values: [['class', 'NotRegistered']],
23 | },
24 | })
25 | ).toThrowError(
26 | `Trying to deserialize unknown class 'NotRegistered' - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564`
27 | );
28 | });
29 |
--------------------------------------------------------------------------------
/src/transformer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | isBigint,
3 | isDate,
4 | isInfinite,
5 | isMap,
6 | isNaNValue,
7 | isRegExp,
8 | isSet,
9 | isUndefined,
10 | isSymbol,
11 | isArray,
12 | isError,
13 | isTypedArray,
14 | TypedArrayConstructor,
15 | isURL,
16 | } from './is.js';
17 | import { findArr } from './util.js';
18 | import SuperJSON from './index.js';
19 |
20 | export type PrimitiveTypeAnnotation = 'number' | 'undefined' | 'bigint';
21 |
22 | type LeafTypeAnnotation =
23 | | PrimitiveTypeAnnotation
24 | | 'regexp'
25 | | 'Date'
26 | | 'Error'
27 | | 'URL';
28 |
29 | type TypedArrayAnnotation = ['typed-array', string];
30 | type ClassTypeAnnotation = ['class', string];
31 | type SymbolTypeAnnotation = ['symbol', string];
32 | type CustomTypeAnnotation = ['custom', string];
33 |
34 | type SimpleTypeAnnotation = LeafTypeAnnotation | 'map' | 'set';
35 |
36 | type CompositeTypeAnnotation =
37 | | TypedArrayAnnotation
38 | | ClassTypeAnnotation
39 | | SymbolTypeAnnotation
40 | | CustomTypeAnnotation;
41 |
42 | export type TypeAnnotation = SimpleTypeAnnotation | CompositeTypeAnnotation;
43 |
44 | function simpleTransformation(
45 | isApplicable: (v: any, superJson: SuperJSON) => v is I,
46 | annotation: A,
47 | transform: (v: I, superJson: SuperJSON) => O,
48 | untransform: (v: O, superJson: SuperJSON) => I
49 | ) {
50 | return {
51 | isApplicable,
52 | annotation,
53 | transform,
54 | untransform,
55 | };
56 | }
57 |
58 | const simpleRules = [
59 | simpleTransformation(
60 | isUndefined,
61 | 'undefined',
62 | () => null,
63 | () => undefined
64 | ),
65 | simpleTransformation(
66 | isBigint,
67 | 'bigint',
68 | v => v.toString(),
69 | v => {
70 | if (typeof BigInt !== 'undefined') {
71 | return BigInt(v);
72 | }
73 |
74 | console.error('Please add a BigInt polyfill.');
75 |
76 | return v as any;
77 | }
78 | ),
79 | simpleTransformation(
80 | isDate,
81 | 'Date',
82 | v => v.toISOString(),
83 | v => new Date(v)
84 | ),
85 |
86 | simpleTransformation(
87 | isError,
88 | 'Error',
89 | (v, superJson) => {
90 | const baseError: any = {
91 | name: v.name,
92 | message: v.message,
93 | };
94 |
95 | superJson.allowedErrorProps.forEach(prop => {
96 | baseError[prop] = (v as any)[prop];
97 | });
98 |
99 | return baseError;
100 | },
101 | (v, superJson) => {
102 | const e = new Error(v.message);
103 | e.name = v.name;
104 | e.stack = v.stack;
105 |
106 | superJson.allowedErrorProps.forEach(prop => {
107 | (e as any)[prop] = v[prop];
108 | });
109 |
110 | return e;
111 | }
112 | ),
113 |
114 | simpleTransformation(
115 | isRegExp,
116 | 'regexp',
117 | v => '' + v,
118 | regex => {
119 | const body = regex.slice(1, regex.lastIndexOf('/'));
120 | const flags = regex.slice(regex.lastIndexOf('/') + 1);
121 | return new RegExp(body, flags);
122 | }
123 | ),
124 |
125 | simpleTransformation(
126 | isSet,
127 | 'set',
128 | // (sets only exist in es6+)
129 | // eslint-disable-next-line es5/no-es6-methods
130 | v => [...v.values()],
131 | v => new Set(v)
132 | ),
133 | simpleTransformation(
134 | isMap,
135 | 'map',
136 | v => [...v.entries()],
137 | v => new Map(v)
138 | ),
139 |
140 | simpleTransformation(
141 | (v): v is number => isNaNValue(v) || isInfinite(v),
142 | 'number',
143 | v => {
144 | if (isNaNValue(v)) {
145 | return 'NaN';
146 | }
147 |
148 | if (v > 0) {
149 | return 'Infinity';
150 | } else {
151 | return '-Infinity';
152 | }
153 | },
154 | Number
155 | ),
156 |
157 | simpleTransformation(
158 | (v): v is number => v === 0 && 1 / v === -Infinity,
159 | 'number',
160 | () => {
161 | return '-0';
162 | },
163 | Number
164 | ),
165 |
166 | simpleTransformation(
167 | isURL,
168 | 'URL',
169 | v => v.toString(),
170 | v => new URL(v)
171 | ),
172 | ];
173 |
174 | function compositeTransformation(
175 | isApplicable: (v: any, superJson: SuperJSON) => v is I,
176 | annotation: (v: I, superJson: SuperJSON) => A,
177 | transform: (v: I, superJson: SuperJSON) => O,
178 | untransform: (v: O, a: A, superJson: SuperJSON) => I
179 | ) {
180 | return {
181 | isApplicable,
182 | annotation,
183 | transform,
184 | untransform,
185 | };
186 | }
187 |
188 | const symbolRule = compositeTransformation(
189 | (s, superJson): s is Symbol => {
190 | if (isSymbol(s)) {
191 | const isRegistered = !!superJson.symbolRegistry.getIdentifier(s);
192 | return isRegistered;
193 | }
194 | return false;
195 | },
196 | (s, superJson) => {
197 | const identifier = superJson.symbolRegistry.getIdentifier(s);
198 | return ['symbol', identifier!];
199 | },
200 | v => v.description,
201 | (_, a, superJson) => {
202 | const value = superJson.symbolRegistry.getValue(a[1]);
203 | if (!value) {
204 | throw new Error('Trying to deserialize unknown symbol');
205 | }
206 | return value;
207 | }
208 | );
209 |
210 | const constructorToName = [
211 | Int8Array,
212 | Uint8Array,
213 | Int16Array,
214 | Uint16Array,
215 | Int32Array,
216 | Uint32Array,
217 | Float32Array,
218 | Float64Array,
219 | Uint8ClampedArray,
220 | ].reduce>((obj, ctor) => {
221 | obj[ctor.name] = ctor;
222 | return obj;
223 | }, {});
224 |
225 | const typedArrayRule = compositeTransformation(
226 | isTypedArray,
227 | v => ['typed-array', v.constructor.name],
228 | v => [...v],
229 | (v, a) => {
230 | const ctor = constructorToName[a[1]];
231 |
232 | if (!ctor) {
233 | throw new Error('Trying to deserialize unknown typed array');
234 | }
235 |
236 | return new ctor(v);
237 | }
238 | );
239 |
240 | export function isInstanceOfRegisteredClass(
241 | potentialClass: any,
242 | superJson: SuperJSON
243 | ): potentialClass is any {
244 | if (potentialClass?.constructor) {
245 | const isRegistered = !!superJson.classRegistry.getIdentifier(
246 | potentialClass.constructor
247 | );
248 | return isRegistered;
249 | }
250 | return false;
251 | }
252 |
253 | const classRule = compositeTransformation(
254 | isInstanceOfRegisteredClass,
255 | (clazz, superJson) => {
256 | const identifier = superJson.classRegistry.getIdentifier(clazz.constructor);
257 | return ['class', identifier!];
258 | },
259 | (clazz, superJson) => {
260 | const allowedProps = superJson.classRegistry.getAllowedProps(
261 | clazz.constructor
262 | );
263 | if (!allowedProps) {
264 | return { ...clazz };
265 | }
266 |
267 | const result: any = {};
268 | allowedProps.forEach(prop => {
269 | result[prop] = clazz[prop];
270 | });
271 | return result;
272 | },
273 | (v, a, superJson) => {
274 | const clazz = superJson.classRegistry.getValue(a[1]);
275 |
276 | if (!clazz) {
277 | throw new Error(
278 | `Trying to deserialize unknown class '${a[1]}' - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564`
279 | );
280 | }
281 |
282 | return Object.assign(Object.create(clazz.prototype), v);
283 | }
284 | );
285 |
286 | const customRule = compositeTransformation(
287 | (value, superJson): value is any => {
288 | return !!superJson.customTransformerRegistry.findApplicable(value);
289 | },
290 | (value, superJson) => {
291 | const transformer = superJson.customTransformerRegistry.findApplicable(
292 | value
293 | )!;
294 | return ['custom', transformer.name];
295 | },
296 | (value, superJson) => {
297 | const transformer = superJson.customTransformerRegistry.findApplicable(
298 | value
299 | )!;
300 | return transformer.serialize(value);
301 | },
302 | (v, a, superJson) => {
303 | const transformer = superJson.customTransformerRegistry.findByName(a[1]);
304 | if (!transformer) {
305 | throw new Error('Trying to deserialize unknown custom value');
306 | }
307 | return transformer.deserialize(v);
308 | }
309 | );
310 |
311 | const compositeRules = [classRule, symbolRule, customRule, typedArrayRule];
312 |
313 | export const transformValue = (
314 | value: any,
315 | superJson: SuperJSON
316 | ): { value: any; type: TypeAnnotation } | undefined => {
317 | const applicableCompositeRule = findArr(compositeRules, rule =>
318 | rule.isApplicable(value, superJson)
319 | );
320 | if (applicableCompositeRule) {
321 | return {
322 | value: applicableCompositeRule.transform(value as never, superJson),
323 | type: applicableCompositeRule.annotation(value, superJson),
324 | };
325 | }
326 |
327 | const applicableSimpleRule = findArr(simpleRules, rule =>
328 | rule.isApplicable(value, superJson)
329 | );
330 |
331 | if (applicableSimpleRule) {
332 | return {
333 | value: applicableSimpleRule.transform(value as never, superJson),
334 | type: applicableSimpleRule.annotation,
335 | };
336 | }
337 |
338 | return undefined;
339 | };
340 |
341 | const simpleRulesByAnnotation: Record = {};
342 | simpleRules.forEach(rule => {
343 | simpleRulesByAnnotation[rule.annotation] = rule;
344 | });
345 |
346 | export const untransformValue = (
347 | json: any,
348 | type: TypeAnnotation,
349 | superJson: SuperJSON
350 | ) => {
351 | if (isArray(type)) {
352 | switch (type[0]) {
353 | case 'symbol':
354 | return symbolRule.untransform(json, type, superJson);
355 | case 'class':
356 | return classRule.untransform(json, type, superJson);
357 | case 'custom':
358 | return customRule.untransform(json, type, superJson);
359 | case 'typed-array':
360 | return typedArrayRule.untransform(json, type, superJson);
361 | default:
362 | throw new Error('Unknown transformation: ' + type);
363 | }
364 | } else {
365 | const transformation = simpleRulesByAnnotation[type];
366 | if (!transformation) {
367 | throw new Error('Unknown transformation: ' + type);
368 | }
369 |
370 | return transformation.untransform(json as never, superJson);
371 | }
372 | };
373 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { TypeAnnotation } from './transformer.js';
2 | import { MinimisedTree, ReferentialEqualityAnnotations } from './plainer.js';
3 |
4 | export type Class = { new (...args: any[]): any };
5 |
6 | export type PrimitiveJSONValue = string | number | boolean | undefined | null;
7 |
8 | export type JSONValue = PrimitiveJSONValue | JSONArray | JSONObject;
9 |
10 | export interface JSONArray extends Array {}
11 |
12 | export interface JSONObject {
13 | [key: string]: JSONValue;
14 | }
15 |
16 | type ClassInstance = any;
17 |
18 | export type SerializableJSONValue =
19 | | Symbol
20 | | Set
21 | | Map
22 | | undefined
23 | | bigint
24 | | Date
25 | | ClassInstance
26 | | RegExp;
27 |
28 | export type SuperJSONValue =
29 | | JSONValue
30 | | SerializableJSONValue
31 | | SuperJSONArray
32 | | SuperJSONObject;
33 |
34 | export interface SuperJSONArray extends Array {}
35 |
36 | export interface SuperJSONObject {
37 | [key: string]: SuperJSONValue;
38 | }
39 |
40 | export interface SuperJSONResult {
41 | json: JSONValue;
42 | meta?: {
43 | values?: MinimisedTree;
44 | referentialEqualities?: ReferentialEqualityAnnotations;
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | function valuesOfObj(record: Record): T[] {
2 | if ('values' in Object) {
3 | // eslint-disable-next-line es5/no-es6-methods
4 | return Object.values(record);
5 | }
6 |
7 | const values: T[] = [];
8 |
9 | // eslint-disable-next-line no-restricted-syntax
10 | for (const key in record) {
11 | if (record.hasOwnProperty(key)) {
12 | values.push(record[key]);
13 | }
14 | }
15 |
16 | return values;
17 | }
18 |
19 | export function find(
20 | record: Record,
21 | predicate: (v: T) => boolean
22 | ): T | undefined {
23 | const values = valuesOfObj(record);
24 | if ('find' in values) {
25 | // eslint-disable-next-line es5/no-es6-methods
26 | return values.find(predicate);
27 | }
28 |
29 | const valuesNotNever = values as T[];
30 |
31 | for (let i = 0; i < valuesNotNever.length; i++) {
32 | const value = valuesNotNever[i];
33 | if (predicate(value)) {
34 | return value;
35 | }
36 | }
37 |
38 | return undefined;
39 | }
40 |
41 | export function forEach(
42 | record: Record,
43 | run: (v: T, key: string) => void
44 | ) {
45 | Object.entries(record).forEach(([key, value]) => run(value, key));
46 | }
47 |
48 | export function includes(arr: T[], value: T) {
49 | return arr.indexOf(value) !== -1;
50 | }
51 |
52 | export function findArr(
53 | record: T[],
54 | predicate: (v: T) => boolean
55 | ): T | undefined {
56 | for (let i = 0; i < record.length; i++) {
57 | const value = record[i];
58 | if (predicate(value)) {
59 | return value;
60 | }
61 | }
62 |
63 | return undefined;
64 | }
65 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "exclude": ["**/*.spec.ts", "**/*.test.ts"],
4 | "compilerOptions": {
5 | "target": "ES2020",
6 | "module": "ES6",
7 | "moduleResolution": "node",
8 | "lib": ["esnext"],
9 | "importHelpers": false,
10 | "declaration": true,
11 | "sourceMap": true,
12 | "rootDir": "./src",
13 | "outDir": "./dist",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | "esModuleInterop": true,
20 | "downlevelIteration": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------