Repository: blitz-js/superjson (ts)

Issue:
Nested resources?
Hey there. I've been trying to use this library to serialise some classes in `getServerSideProps` in a Next.js app, but either I'm misunderstanding the aims of this library or I can't see it deserialising nested classes?

I've created a [simple recreation here](https://codesandbox.io/s/superjson-nested-recreation-ti3h7?file=/src/index.ts) (check the console output).

Some steps: 
- Given an instance of a class A, of which an attribute is itself an instance of a class B
- `stringify` the instance
- `parse` the instance
- The top level instance of class A has been hydrated, but the nested attribute of type class B is a plain object, when it should be an instance of class B

Please let me know if I'm missing something, and thanks for the OSS!

Pull request description:
Fix 109
Closes #109 

Patch:
diff --git a/src/plainer.ts b/src/plainer.ts
index 9454df7..d818cb5 100644
--- a/src/plainer.ts
+++ b/src/plainer.ts
@@ -1,4 +1,5 @@
 import { isArray, isMap, isPlainObject, isPrimitive, isSet } from './is';
+import { isInstanceOfRegisteredClass } from './transformer';
 import { includes, mapValues } from './util';
 
 interface WalkerValue {
@@ -10,7 +11,11 @@ interface WalkerValue {
 export type Walker = (v: WalkerValue) => any;
 
 const isDeep = (object: any): boolean =>
-  isPlainObject(object) || isArray(object) || isMap(object) || isSet(object);
+  isPlainObject(object) ||
+  isArray(object) ||
+  isMap(object) ||
+  isSet(object) ||
+  isInstanceOfRegisteredClass(object);
 
 export const plainer = (
   object: any,
@@ -22,7 +27,7 @@ export const plainer = (
     return walker({ isLeaf: true, node: object, path });
   }
 
-  walker({ isLeaf: false, path, node: object });
+  object = walker({ isLeaf: false, path, node: object });
 
   if (includes(alreadySeenObjects, object)) {
     return null;
@@ -38,21 +43,6 @@ export const plainer = (
     );
   }
 
-  if (isSet(object)) {
-    // (sets only exist in es6+)
-    // eslint-disable-next-line es5/no-es6-methods
-    return [...object.values()].map((value, index) =>
-      plainer(value, walker, [...path, index], alreadySeenObjects)
-    );
-  }
-
-  if (isMap(object)) {
-    return [...object.entries()].map(([key, value], index) => [
-      plainer(key, walker, [...path, index, 0], alreadySeenObjects),
-      plainer(value, walker, [...path, index, 1], alreadySeenObjects),
-    ]);
-  }
-
   if (isPlainObject(object)) {
     return mapValues(object, (value, key) =>
       plainer(value, walker, [...path, key], alreadySeenObjects)
diff --git a/src/transformer.ts b/src/transformer.ts
index b218192..77dd7be 100644
--- a/src/transformer.ts
+++ b/src/transformer.ts
@@ -144,13 +144,13 @@ const simpleRules = [
     'set',
     // (sets only exist in es6+)
     // eslint-disable-next-line es5/no-es6-methods
-    v => Object.values(v),
+    v => [...v.values()],
     v => new Set(v)
   ),
   simpleTransformation(
     isMap,
     'map',
-    v => Object.entries(v),
+    v => [...v.entries()],
     v => new Map(v)
   ),
 
@@ -217,16 +217,20 @@ const symbolRule = compositeTransformation(
   }
 );
 
+export function isInstanceOfRegisteredClass(
+  potentialClass: any
+): potentialClass is any {
+  if (potentialClass?.constructor) {
+    const isRegistered = !!ClassRegistry.getIdentifier(
+      potentialClass.constructor
+    );
+    return isRegistered;
+  }
+  return false;
+}
+
 const classRule = compositeTransformation(
-  (potentialClass): potentialClass is any => {
-    if (potentialClass?.constructor) {
-      const isRegistered = !!ClassRegistry.getIdentifier(
-        potentialClass.constructor
-      );
-      return isRegistered;
-    }
-    return false;
-  },
+  isInstanceOfRegisteredClass,
   clazz => {
     const identifier = ClassRegistry.getIdentifier(clazz.constructor);
     return ['class', identifier!];
@@ -234,7 +238,7 @@ const classRule = compositeTransformation(
   clazz => {
     const allowedProps = ClassRegistry.getAllowedProps(clazz.constructor);
     if (!allowedProps) {
-      return clazz;
+      return { ...clazz };
     }
 
     const result: any = {};


Test patch:
diff --git a/src/index.test.ts b/src/index.test.ts
index 79758e8..e840bd7 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -543,6 +543,46 @@ describe('stringify & parse', () => {
       output: null,
       outputAnnotations: { values: ['undefined'] },
     },
+
+    'regression #109: nested classes': {
+      input: () => {
+        class Pet {
+          constructor(private name: string) {}
+
+          woof() {
+            return this.name;
+          }
+        }
+
+        class User {
+          constructor(public pet: Pet) {}
+        }
+
+        SuperJSON.registerClass(Pet);
+        SuperJSON.registerClass(User);
+
+        const pet = new Pet('Rover');
+        const user = new User(pet);
+
+        return user;
+      },
+      output: {
+        pet: {
+          name: 'Rover',
+        },
+      },
+      outputAnnotations: {
+        values: [
+          ['class', 'User'],
+          {
+            pet: [['class', 'Pet']],
+          },
+        ],
+      },
+      customExpectations(value) {
+        expect(value.pet.woof()).toEqual('Rover');
+      },
+    },
   };
 
   function deepFreeze(object: any, alreadySeenObjects = new Set()) {
@@ -609,7 +649,9 @@ describe('stringify & parse', () => {
       }
       expect(meta).toEqual(expectedOutputAnnotations);
 
-      const untransformed = SuperJSON.deserialize({ json, meta });
+      const untransformed = SuperJSON.deserialize(
+        JSON.parse(JSON.stringify({ json, meta }))
+      );
       if (!dontExpectEquality) {
         expect(untransformed).toEqual(inputValue);
       }
@@ -864,7 +906,7 @@ test('performance regression', () => {
   SuperJSON.serialize(data);
   const t2 = Date.now();
   const duration = t2 - t1;
-  expect(duration).toBeLessThan(500);
+  expect(duration).toBeLessThan(700);
 });
 
 test('regression #95: no undefined', () => {
diff --git a/src/plainer.test.ts b/src/plainer.test.ts
index 3014f42..7ca49d6 100644
--- a/src/plainer.test.ts
+++ b/src/plainer.test.ts
@@ -20,6 +20,13 @@ describe('plainer', () => {
 
     const output = plainer(input, ({ path, node }) => {
       annotations[path.join('.')] = node;
+      if (node instanceof Map) {
+        return [...node.entries()];
+      }
+      if (node instanceof Set) {
+        // eslint-disable-next-line es5/no-es6-methods
+        return [...node.values()];
+      }
       return node;
     });
 
@@ -60,8 +67,16 @@ describe('plainer', () => {
           2 => "hallo",
           undefined => null,
         },
+        "a.0": Array [
+          2,
+          "hallo",
+        ],
         "a.0.0": 2,
         "a.0.1": "hallo",
+        "a.1": Array [
+          undefined,
+          null,
+        ],
         "a.1.0": undefined,
         "a.1.1": null,
         "b": Object {