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 { 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 = {};