Repository: blitz-js/superjson (ts)

Issue:
Global scope?
Superjson is right now in a global scope - is there a design reason for this? 

I'd see useage being like this

```ts
// utils/sj.ts
import {superjson} from 'superjson'

export const sj = superjson()
sj.registerClass( /* .. */)
// [..]


```

Or even rather, without modifiers after init. Could possibly open up for some nice typescript inference tricks

```ts
// utils/sj.ts
import {superjson} from 'superjson'

export const sj = superjson({
  classes: [],
  custom: [],

})

// elsewhere
sj.stringify(new MyCustomError('asdf');
```

Pull request description:
refactor: allow individual superjson instances (closes #87)
this allows the creation of individual superjson instances, so there can be multiple different configurations.
it keeps the global instance in place for backwards compatibility, and b/c most users will be fine with one global config.

Patch:
diff --git a/src/class-registry.ts b/src/class-registry.ts
index 211a757..a22d3f2 100644
--- a/src/class-registry.ts
+++ b/src/class-registry.ts
@@ -6,7 +6,7 @@ export interface RegisterOptions {
   allowProps?: string[];
 }
 
-class _ClassRegistry extends Registry<Class> {
+export class ClassRegistry extends Registry<Class> {
   constructor() {
     super(c => c.name);
   }
@@ -29,5 +29,3 @@ class _ClassRegistry extends Registry<Class> {
     return this.classToAllowedProps.get(value);
   }
 }
-
-export const ClassRegistry = new _ClassRegistry();
diff --git a/src/custom-transformer-registry.ts b/src/custom-transformer-registry.ts
index 14c1594..bf335d3 100644
--- a/src/custom-transformer-registry.ts
+++ b/src/custom-transformer-registry.ts
@@ -8,20 +8,20 @@ export interface CustomTransfomer<I, O extends JSONValue> {
   deserialize: (v: O) => I;
 }
 
-const transfomers: Record<string, CustomTransfomer<any, any>> = {};
+export class CustomTransformerRegistry {
+  private transfomers: Record<string, CustomTransfomer<any, any>> = {};
 
-export const CustomTransformerRegistry = {
   register<I, O extends JSONValue>(transformer: CustomTransfomer<I, O>) {
-    transfomers[transformer.name] = transformer;
-  },
+    this.transfomers[transformer.name] = transformer;
+  }
 
   findApplicable<T>(v: T) {
-    return find(transfomers, transformer => transformer.isApplicable(v)) as
-      | CustomTransfomer<T, JSONValue>
-      | undefined;
-  },
+    return find(this.transfomers, transformer =>
+      transformer.isApplicable(v)
+    ) as CustomTransfomer<T, JSONValue> | undefined;
+  }
 
   findByName(name: string) {
-    return transfomers[name];
-  },
-};
+    return this.transfomers[name];
+  }
+}
diff --git a/src/error-props.ts b/src/error-props.ts
deleted file mode 100644
index e052a37..0000000
--- a/src/error-props.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const allowedErrorProps: string[] = [];
-
-export const allowErrorProps = (...props: string[]) => {
-  allowedErrorProps.push(...props);
-};
diff --git a/src/index.ts b/src/index.ts
index 91c57d3..dcbc954 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,11 +1,10 @@
 import { SuperJSONResult, SuperJSONValue, Class, JSONValue } from './types';
 import { ClassRegistry, RegisterOptions } from './class-registry';
-import { SymbolRegistry } from './symbol-registry';
+import { Registry } from './registry';
 import {
   CustomTransfomer,
   CustomTransformerRegistry,
 } from './custom-transformer-registry';
-import { allowErrorProps } from './error-props';
 import {
   walker,
   applyReferentialEqualityAnnotations,
@@ -14,80 +13,116 @@ import {
 } from './plainer';
 import { copy } from 'copy-anything';
 
-export const serialize = (object: SuperJSONValue): SuperJSONResult => {
-  const identities = new Map<any, any[][]>();
-  const output = walker(object, identities);
-  const res: SuperJSONResult = {
-    json: output.transformedValue,
-  };
-
-  if (output.annotations) {
-    res.meta = {
-      ...res.meta,
-      values: output.annotations,
+export default class SuperJSON {
+  serialize(object: SuperJSONValue): SuperJSONResult {
+    const identities = new Map<any, any[][]>();
+    const output = walker(object, identities, this);
+    const res: SuperJSONResult = {
+      json: output.transformedValue,
     };
+
+    if (output.annotations) {
+      res.meta = {
+        ...res.meta,
+        values: output.annotations,
+      };
+    }
+
+    const equalityAnnotations = generateReferentialEqualityAnnotations(
+      identities
+    );
+    if (equalityAnnotations) {
+      res.meta = {
+        ...res.meta,
+        referentialEqualities: equalityAnnotations,
+      };
+    }
+
+    return res;
   }
 
-  const equalityAnnotations = generateReferentialEqualityAnnotations(
-    identities
-  );
-  if (equalityAnnotations) {
-    res.meta = {
-      ...res.meta,
-      referentialEqualities: equalityAnnotations,
-    };
+  deserialize<T = unknown>(payload: SuperJSONResult): T {
+    const { json, meta } = payload;
+
+    let result: T = copy(json) as any;
+
+    if (meta?.values) {
+      result = applyValueAnnotations(result, meta.values, this);
+    }
+
+    if (meta?.referentialEqualities) {
+      result = applyReferentialEqualityAnnotations(
+        result,
+        meta.referentialEqualities
+      );
+    }
+
+    return result;
   }
 
-  return res;
-};
+  stringify(object: SuperJSONValue): string {
+    return JSON.stringify(this.serialize(object));
+  }
+
+  parse<T = unknown>(string: string): T {
+    return this.deserialize(JSON.parse(string));
+  }
 
-export const deserialize = <T = unknown>(payload: SuperJSONResult): T => {
-  const { json, meta } = payload;
+  readonly classRegistry = new ClassRegistry();
+  registerClass(v: Class, options?: RegisterOptions | string) {
+    this.classRegistry.register(v, options);
+  }
 
-  let result: T = copy(json) as any;
+  readonly symbolRegistry = new Registry<Symbol>(s => s.description ?? '');
+  registerSymbol(v: Symbol, identifier?: string) {
+    this.symbolRegistry.register(v, identifier);
+  }
 
-  if (meta?.values) {
-    result = applyValueAnnotations(result, meta.values);
+  readonly customTransformerRegistry = new CustomTransformerRegistry();
+  registerCustom<I, O extends JSONValue>(
+    transformer: Omit<CustomTransfomer<I, O>, 'name'>,
+    name: string
+  ) {
+    this.customTransformerRegistry.register({
+      name,
+      ...transformer,
+    });
   }
 
-  if (meta?.referentialEqualities) {
-    result = applyReferentialEqualityAnnotations(
-      result,
-      meta.referentialEqualities
-    );
+  readonly allowedErrorProps: string[] = [];
+  allowErrorProps(...props: string[]) {
+    this.allowedErrorProps.push(...props);
   }
 
-  return result;
-};
-
-export const stringify = (object: SuperJSONValue): string =>
-  JSON.stringify(serialize(object));
-
-export const parse = <T = unknown>(string: string): T =>
-  deserialize(JSON.parse(string));
-
-export const registerClass = (v: Class, options?: RegisterOptions | string) =>
-  ClassRegistry.register(v, options);
-
-export const registerSymbol = (v: Symbol, identifier?: string) =>
-  SymbolRegistry.register(v, identifier);
-
-export const registerCustom = <I, O extends JSONValue>(
-  transformer: Omit<CustomTransfomer<I, O>, 'name'>,
-  name: string
-) =>
-  CustomTransformerRegistry.register({
-    name,
-    ...transformer,
-  });
-
-export default {
-  stringify,
-  parse,
-  serialize,
-  deserialize,
-  registerClass,
-  registerSymbol,
-  registerCustom,
-  allowErrorProps,
-};
+  private static defaultInstance = new SuperJSON();
+  static serialize = SuperJSON.defaultInstance.serialize.bind(
+    SuperJSON.defaultInstance
+  );
+  static deserialize = SuperJSON.defaultInstance.deserialize.bind(
+    SuperJSON.defaultInstance
+  );
+  static stringify = SuperJSON.defaultInstance.stringify.bind(
+    SuperJSON.defaultInstance
+  );
+  static parse = SuperJSON.defaultInstance.parse.bind(
+    SuperJSON.defaultInstance
+  );
+  static registerClass = SuperJSON.defaultInstance.registerClass.bind(
+    SuperJSON.defaultInstance
+  );
+  static registerSymbol = SuperJSON.defaultInstance.registerSymbol.bind(
+    SuperJSON.defaultInstance
+  );
+  static registerCustom = SuperJSON.defaultInstance.registerCustom.bind(
+    SuperJSON.defaultInstance
+  );
+  static allowErrorProps = SuperJSON.defaultInstance.allowErrorProps.bind(
+    SuperJSON.defaultInstance
+  );
+}
+
+export const serialize = SuperJSON.serialize;
+export const deserialize = SuperJSON.deserialize;
+
+export const stringify = SuperJSON.stringify;
+export const parse = SuperJSON.parse;
diff --git a/src/plainer.spec.ts b/src/plainer.spec.ts
index ad96758..be11cd3 100644
--- a/src/plainer.spec.ts
+++ b/src/plainer.spec.ts
@@ -1,3 +1,4 @@
+import SuperJSON from '.';
 import { walker } from './plainer';
 
 test('walker', () => {
@@ -7,7 +8,8 @@ test('walker', () => {
         a: new Map([[NaN, null]]),
         b: /test/g,
       },
-      new Map()
+      new Map(),
+      new SuperJSON()
     )
   ).toEqual({
     transformedValue: {
diff --git a/src/plainer.ts b/src/plainer.ts
index 53a6563..a762056 100644
--- a/src/plainer.ts
+++ b/src/plainer.ts
@@ -16,6 +16,7 @@ import {
 import { includes, forEach } from './util';
 import { parsePath } from './pathstringifier';
 import { getDeep, setDeep } from './accessDeep';
+import SuperJSON from '.';
 
 type Tree<T> = InnerNode<T> | Leaf<T>;
 type Leaf<T> = [T];
@@ -51,10 +52,11 @@ function traverse<T>(
 
 export function applyValueAnnotations(
   plain: any,
-  annotations: MinimisedTree<TypeAnnotation>
+  annotations: MinimisedTree<TypeAnnotation>,
+  superJson: SuperJSON
 ) {
   traverse(annotations, (type, path) => {
-    plain = setDeep(plain, path, v => untransformValue(v, type));
+    plain = setDeep(plain, path, v => untransformValue(v, type, superJson));
   });
 
   return plain;
@@ -88,12 +90,12 @@ export function applyReferentialEqualityAnnotations(
   return plain;
 }
 
-const isDeep = (object: any): boolean =>
+const isDeep = (object: any, superJson: SuperJSON): boolean =>
   isPlainObject(object) ||
   isArray(object) ||
   isMap(object) ||
   isSet(object) ||
-  isInstanceOfRegisteredClass(object);
+  isInstanceOfRegisteredClass(object, superJson);
 
 function addIdentity(object: any, path: any[], identities: Map<any, any[][]>) {
   const existingSet = identities.get(object);
@@ -151,6 +153,7 @@ export function generateReferentialEqualityAnnotations(
 export const walker = (
   object: any,
   identities: Map<any, any[][]>,
+  superJson: SuperJSON,
   path: any[] = [],
   objectsInThisPath: any[] = []
 ): Result => {
@@ -158,8 +161,8 @@ export const walker = (
     addIdentity(object, path, identities);
   }
 
-  if (!isDeep(object)) {
-    const transformed = transformValue(object);
+  if (!isDeep(object, superJson)) {
+    const transformed = transformValue(object, superJson);
     if (transformed) {
       return {
         transformedValue: transformed.value,
@@ -178,7 +181,7 @@ export const walker = (
     };
   }
 
-  const transformationResult = transformValue(object);
+  const transformationResult = transformValue(object, superJson);
   const transformed = transformationResult?.value ?? object;
 
   if (!isPrimitive(object)) {
@@ -192,6 +195,7 @@ export const walker = (
     const recursiveResult = walker(
       value,
       identities,
+      superJson,
       [...path, index],
       objectsInThisPath
     );
diff --git a/src/symbol-registry.ts b/src/symbol-registry.ts
deleted file mode 100644
index 058683d..0000000
--- a/src/symbol-registry.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Registry } from './registry';
-
-export const SymbolRegistry = new Registry<Symbol>(s => s.description ?? '');
diff --git a/src/transformer.ts b/src/transformer.ts
index 4dd57a2..331e897 100644
--- a/src/transformer.ts
+++ b/src/transformer.ts
@@ -14,11 +14,8 @@ import {
   TypedArrayConstructor,
   isURL,
 } from './is';
-import { ClassRegistry } from './class-registry';
-import { SymbolRegistry } from './symbol-registry';
-import { CustomTransformerRegistry } from './custom-transformer-registry';
-import { allowedErrorProps } from './error-props';
 import { findArr } from './util';
+import SuperJSON from '.';
 
 export type PrimitiveTypeAnnotation = 'number' | 'undefined' | 'bigint';
 
@@ -45,10 +42,10 @@ type CompositeTypeAnnotation =
 export type TypeAnnotation = SimpleTypeAnnotation | CompositeTypeAnnotation;
 
 function simpleTransformation<I, O, A extends SimpleTypeAnnotation>(
-  isApplicable: (v: any) => v is I,
+  isApplicable: (v: any, superJson: SuperJSON) => v is I,
   annotation: A,
-  transform: (v: I) => O,
-  untransform: (v: O) => I
+  transform: (v: I, superJson: SuperJSON) => O,
+  untransform: (v: O, superJson: SuperJSON) => I
 ) {
   return {
     isApplicable,
@@ -89,24 +86,24 @@ const simpleRules = [
   simpleTransformation(
     isError,
     'Error',
-    v => {
+    (v, superJson) => {
       const baseError: any = {
         name: v.name,
         message: v.message,
       };
 
-      allowedErrorProps.forEach(prop => {
+      superJson.allowedErrorProps.forEach(prop => {
         baseError[prop] = (v as any)[prop];
       });
 
       return baseError;
     },
-    v => {
+    (v, superJson) => {
       const e = new Error(v.message);
       e.name = v.name;
       e.stack = v.stack;
 
-      allowedErrorProps.forEach(prop => {
+      superJson.allowedErrorProps.forEach(prop => {
         (e as any)[prop] = v[prop];
       });
 
@@ -175,10 +172,10 @@ const simpleRules = [
 ];
 
 function compositeTransformation<I, O, A extends CompositeTypeAnnotation>(
-  isApplicable: (v: any) => v is I,
-  annotation: (v: I) => A,
-  transform: (v: I) => O,
-  untransform: (v: O, a: A) => I
+  isApplicable: (v: any, superJson: SuperJSON) => v is I,
+  annotation: (v: I, superJson: SuperJSON) => A,
+  transform: (v: I, superJson: SuperJSON) => O,
+  untransform: (v: O, a: A, superJson: SuperJSON) => I
 ) {
   return {
     isApplicable,
@@ -189,20 +186,20 @@ function compositeTransformation<I, O, A extends CompositeTypeAnnotation>(
 }
 
 const symbolRule = compositeTransformation(
-  (s): s is Symbol => {
+  (s, superJson): s is Symbol => {
     if (isSymbol(s)) {
-      const isRegistered = !!SymbolRegistry.getIdentifier(s);
+      const isRegistered = !!superJson.symbolRegistry.getIdentifier(s);
       return isRegistered;
     }
     return false;
   },
-  s => {
-    const identifier = SymbolRegistry.getIdentifier(s);
+  (s, superJson) => {
+    const identifier = superJson.symbolRegistry.getIdentifier(s);
     return ['symbol', identifier!];
   },
   v => v.description,
-  (_, a) => {
-    const value = SymbolRegistry.getValue(a[1]);
+  (_, a, superJson) => {
+    const value = superJson.symbolRegistry.getValue(a[1]);
     if (!value) {
       throw new Error('Trying to deserialize unknown symbol');
     }
@@ -241,10 +238,11 @@ const typedArrayRule = compositeTransformation(
 );
 
 export function isInstanceOfRegisteredClass(
-  potentialClass: any
+  potentialClass: any,
+  superJson: SuperJSON
 ): potentialClass is any {
   if (potentialClass?.constructor) {
-    const isRegistered = !!ClassRegistry.getIdentifier(
+    const isRegistered = !!superJson.classRegistry.getIdentifier(
       potentialClass.constructor
     );
     return isRegistered;
@@ -254,12 +252,14 @@ export function isInstanceOfRegisteredClass(
 
 const classRule = compositeTransformation(
   isInstanceOfRegisteredClass,
-  clazz => {
-    const identifier = ClassRegistry.getIdentifier(clazz.constructor);
+  (clazz, superJson) => {
+    const identifier = superJson.classRegistry.getIdentifier(clazz.constructor);
     return ['class', identifier!];
   },
-  clazz => {
-    const allowedProps = ClassRegistry.getAllowedProps(clazz.constructor);
+  (clazz, superJson) => {
+    const allowedProps = superJson.classRegistry.getAllowedProps(
+      clazz.constructor
+    );
     if (!allowedProps) {
       return { ...clazz };
     }
@@ -270,8 +270,8 @@ const classRule = compositeTransformation(
     });
     return result;
   },
-  (v, a) => {
-    const clazz = ClassRegistry.getValue(a[1]);
+  (v, a, superJson) => {
+    const clazz = superJson.classRegistry.getValue(a[1]);
 
     if (!clazz) {
       throw new Error(
@@ -284,19 +284,23 @@ const classRule = compositeTransformation(
 );
 
 const customRule = compositeTransformation(
-  (value): value is any => {
-    return !!CustomTransformerRegistry.findApplicable(value);
+  (value, superJson): value is any => {
+    return !!superJson.customTransformerRegistry.findApplicable(value);
   },
-  value => {
-    const transformer = CustomTransformerRegistry.findApplicable(value)!;
+  (value, superJson) => {
+    const transformer = superJson.customTransformerRegistry.findApplicable(
+      value
+    )!;
     return ['custom', transformer.name];
   },
-  value => {
-    const transformer = CustomTransformerRegistry.findApplicable(value)!;
+  (value, superJson) => {
+    const transformer = superJson.customTransformerRegistry.findApplicable(
+      value
+    )!;
     return transformer.serialize(value);
   },
-  (v, a) => {
-    const transformer = CustomTransformerRegistry.findByName(a[1]);
+  (v, a, superJson) => {
+    const transformer = superJson.customTransformerRegistry.findByName(a[1]);
     if (!transformer) {
       throw new Error('Trying to deserialize unknown custom value');
     }
@@ -307,25 +311,26 @@ const customRule = compositeTransformation(
 const compositeRules = [classRule, symbolRule, customRule, typedArrayRule];
 
 export const transformValue = (
-  value: any
+  value: any,
+  superJson: SuperJSON
 ): { value: any; type: TypeAnnotation } | undefined => {
   const applicableCompositeRule = findArr(compositeRules, rule =>
-    rule.isApplicable(value)
+    rule.isApplicable(value, superJson)
   );
   if (applicableCompositeRule) {
     return {
-      value: applicableCompositeRule.transform(value as never),
-      type: applicableCompositeRule.annotation(value),
+      value: applicableCompositeRule.transform(value as never, superJson),
+      type: applicableCompositeRule.annotation(value, superJson),
     };
   }
 
   const applicableSimpleRule = findArr(simpleRules, rule =>
-    rule.isApplicable(value)
+    rule.isApplicable(value, superJson)
   );
 
   if (applicableSimpleRule) {
     return {
-      value: applicableSimpleRule.transform(value as never),
+      value: applicableSimpleRule.transform(value as never, superJson),
       type: applicableSimpleRule.annotation,
     };
   }
@@ -338,17 +343,21 @@ simpleRules.forEach(rule => {
   simpleRulesByAnnotation[rule.annotation] = rule;
 });
 
-export const untransformValue = (json: any, type: TypeAnnotation) => {
+export const untransformValue = (
+  json: any,
+  type: TypeAnnotation,
+  superJson: SuperJSON
+) => {
   if (isArray(type)) {
     switch (type[0]) {
       case 'symbol':
-        return symbolRule.untransform(json, type);
+        return symbolRule.untransform(json, type, superJson);
       case 'class':
-        return classRule.untransform(json, type);
+        return classRule.untransform(json, type, superJson);
       case 'custom':
-        return customRule.untransform(json, type);
+        return customRule.untransform(json, type, superJson);
       case 'typed-array':
-        return typedArrayRule.untransform(json, type);
+        return typedArrayRule.untransform(json, type, superJson);
       default:
         throw new Error('Unknown transformation: ' + type);
     }
@@ -358,6 +367,6 @@ export const untransformValue = (json: any, type: TypeAnnotation) => {
       throw new Error('Unknown transformation: ' + type);
     }
 
-    return transformation.untransform(json as never);
+    return transformation.untransform(json as never, superJson);
   }
 };


Test patch:
diff --git a/src/index.test.ts b/src/index.test.ts
index 365b206..7c90966 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -1076,3 +1076,20 @@ test('prototype pollution - constructor', () => {
 
   expect((Object.prototype as any).x).toBeUndefined();
 });
+
+test('superjson instances are independent of one another', () => {
+  class Car {}
+  const s1 = new SuperJSON();
+  s1.registerClass(Car);
+
+  const s2 = new SuperJSON();
+
+  const value = {
+    car: new Car(),
+  };
+
+  const res1 = s1.serialize(value);
+  expect(res1.meta?.values).toEqual({ car: [['class', 'Car']] });
+  const res2 = s2.serialize(value);
+  expect(res2.json).toEqual(value);
+});