diff --git a/src/index.test.ts b/src/index.test.ts index 3ef96a1..98805e9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -174,7 +174,7 @@ describe('stringify & parse', () => { }, outputAnnotations: { values: { - 'a\\\\.1.b': ['set'], + 'a\\\\\\.1.b': ['set'], }, }, }, @@ -686,6 +686,26 @@ describe('stringify & parse', () => { }, }, }, + 'repro #310: meta path escape bug': { + input: { + a: ["/'a'[0]: string that becomes a regex/"], + 'a.0': /'a.0': regex that becomes a string/, + 'b.0': "/'b.0': string that becomes a regex/", + 'b\\': [/'b\\'[0]: regex that becomes a string/], + }, + output: { + a: ["/'a'[0]: string that becomes a regex/"], + 'a.0': "/'a.0': regex that becomes a string/", + 'b.0': "/'b.0': string that becomes a regex/", + 'b\\': ["/'b\\\\'[0]: regex that becomes a string/"], + }, + outputAnnotations: { + values: { + 'a\\.0': ['regexp'], + 'b\\\\.0': ['regexp'], + }, + }, + }, }; function deepFreeze(object: any, alreadySeenObjects = new Set()) { @@ -759,7 +779,13 @@ describe('stringify & parse', () => { } else { expect(json).toEqual(expectedOutput); } - expect(meta).toEqual(expectedOutputAnnotations); + if (meta) { + const { v, ...rest } = meta; + expect(v).toBe(1); + expect(rest).toEqual(expectedOutputAnnotations); + } else { + expect(meta).toEqual(expectedOutputAnnotations); + } const untransformed = SuperJSON.deserialize( JSON.parse(JSON.stringify({ json, meta })) @@ -800,6 +826,7 @@ describe('stringify & parse', () => { }); expect(meta).toEqual({ + v: 1, values: { s7: [['class', 'Train']], }, @@ -863,6 +890,7 @@ describe('stringify & parse', () => { a: '1000', }, meta: { + v: 1, values: { a: ['bigint'], }, @@ -946,7 +974,7 @@ test('regression #83: negative zero', () => { const stringified = SuperJSON.stringify(input); expect(stringified).toMatchInlineSnapshot( - `"{\\"json\\":\\"-0\\",\\"meta\\":{\\"values\\":[\\"number\\"]}}"` + `"{\\"json\\":\\"-0\\",\\"meta\\":{\\"values\\":[\\"number\\"],\\"v\\":1}}"` ); const parsed: number = SuperJSON.parse(stringified); @@ -1166,6 +1194,7 @@ test('regression #245: superjson referential equalities only use the top-most pa "b", ], }, + "v": 1, } `); @@ -1241,3 +1270,43 @@ test('doesnt iterate to keys that dont exist', () => { expect(() => SuperJSON.deserialize(res)).toThrowError('index out of bounds'); }); + +test('#310 fixes backwards compat', () => { + expect( + SuperJSON.deserialize({ + json: { + 'a\\.1': { + b: [1, 2], + }, + }, + meta: { + values: { + 'a\\\\.1.b': ['set'], + }, + }, + }) + ).toEqual({ + 'a\\.1': { + b: new Set([1, 2]), + }, + }); + + expect( + SuperJSON.deserialize({ + json: { + 'a.1': { + b: [1, 2], + }, + }, + meta: { + values: { + 'a\\.1.b': ['set'], + }, + }, + }) + ).toEqual({ + 'a.1': { + b: new Set([1, 2]), + }, + }); +}); diff --git a/src/index.ts b/src/index.ts index 5636e65..407d472 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,6 +55,8 @@ export default class SuperJSON { }; } + if (res.meta) res.meta.v = 1; + return res; } @@ -64,13 +66,14 @@ export default class SuperJSON { let result: T = copy(json) as any; if (meta?.values) { - result = applyValueAnnotations(result, meta.values, this); + result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this); } if (meta?.referentialEqualities) { result = applyReferentialEqualityAnnotations( result, - meta.referentialEqualities + meta.referentialEqualities, + meta.v ?? 0 ); } diff --git a/src/pathstringifier.test.ts b/src/pathstringifier.test.ts index c78f4de..83d1949 100644 --- a/src/pathstringifier.test.ts +++ b/src/pathstringifier.test.ts @@ -6,9 +6,9 @@ describe('parsePath', () => { it.each([ ['test.a.b', ['test', 'a', 'b']], ['test\\.a.b', ['test.a', 'b']], - ['test\\\\.a.b', ['test\\.a', 'b']], + ['test\\\\.a.b', ['test\\', 'a', 'b']], ['test\\a.b', ['test\\a', 'b']], - ['test\\\\a.b', ['test\\\\a', 'b']], + ['test\\\\a.b', ['test\\a', 'b']], ])('parsePath(%p) === %p', (input, expectedOutput) => { expect(parsePath(input)).toEqual(expectedOutput); }); diff --git a/src/pathstringifier.ts b/src/pathstringifier.ts index a3d4d37..5681a73 100644 --- a/src/pathstringifier.ts +++ b/src/pathstringifier.ts @@ -1,7 +1,8 @@ export type StringifiedPath = string; type Path = string[]; -export const escapeKey = (key: string) => key.replace(/\./g, '\\.'); +export const escapeKey = (key: string) => + key.replace(/\\/g, '\\\\').replace(/\./g, '\\.'); export const stringifyPath = (path: Path): StringifiedPath => path @@ -9,13 +10,22 @@ export const stringifyPath = (path: Path): StringifiedPath => .map(escapeKey) .join('.'); -export const parsePath = (string: StringifiedPath) => { +export const parsePath = (string: StringifiedPath, legacyPaths: boolean) => { const result: string[] = []; let segment = ''; for (let i = 0; i < string.length; i++) { let char = string.charAt(i); + if (!legacyPaths) { + const isEscapedBackslash = char === '\\' && string.charAt(i + 1) === '\\'; + if (isEscapedBackslash) { + segment += '\\'; + i++; + continue; + } + } + const isEscapedDot = char === '\\' && string.charAt(i + 1) === '.'; if (isEscapedDot) { segment += '.'; diff --git a/src/plainer.ts b/src/plainer.ts index 114d76a..644637c 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -24,18 +24,25 @@ type InnerNode = [T, Record>]; export type MinimisedTree = Tree | Record> | undefined; +const enableLegacyPaths = (version: number) => version < 1; + function traverse( tree: MinimisedTree, walker: (v: T, path: string[]) => void, + version: number, origin: string[] = [] ): void { if (!tree) { return; } + const legacyPaths = enableLegacyPaths(version); if (!isArray(tree)) { forEach(tree, (subtree, key) => - traverse(subtree, walker, [...origin, ...parsePath(key)]) + traverse(subtree, walker, version, [ + ...origin, + ...parsePath(key, legacyPaths), + ]) ); return; } @@ -43,7 +50,10 @@ function traverse( const [nodeValue, children] = tree; if (children) { forEach(children, (child, key) => { - traverse(child, walker, [...origin, ...parsePath(key)]); + traverse(child, walker, version, [ + ...origin, + ...parsePath(key, legacyPaths), + ]); }); } @@ -53,31 +63,44 @@ function traverse( export function applyValueAnnotations( plain: any, annotations: MinimisedTree, + version: number, superJson: SuperJSON ) { - traverse(annotations, (type, path) => { - plain = setDeep(plain, path, v => untransformValue(v, type, superJson)); - }); + traverse( + annotations, + (type, path) => { + plain = setDeep(plain, path, v => untransformValue(v, type, superJson)); + }, + version + ); return plain; } export function applyReferentialEqualityAnnotations( plain: any, - annotations: ReferentialEqualityAnnotations + annotations: ReferentialEqualityAnnotations, + version: number ) { + const legacyPaths = enableLegacyPaths(version); function apply(identicalPaths: string[], path: string) { - const object = getDeep(plain, parsePath(path)); + const object = getDeep(plain, parsePath(path, legacyPaths)); - identicalPaths.map(parsePath).forEach(identicalObjectPath => { - plain = setDeep(plain, identicalObjectPath, () => object); - }); + identicalPaths + .map(path => parsePath(path, legacyPaths)) + .forEach(identicalObjectPath => { + plain = setDeep(plain, identicalObjectPath, () => object); + }); } if (isArray(annotations)) { const [root, other] = annotations; root.forEach(identicalPath => { - plain = setDeep(plain, parsePath(identicalPath), () => plain); + plain = setDeep( + plain, + parsePath(identicalPath, legacyPaths), + () => plain + ); }); if (other) { @@ -239,7 +262,7 @@ export const walker = ( transformedValue[index] = recursiveResult.transformedValue; if (isArray(recursiveResult.annotations)) { - innerAnnotations[index] = recursiveResult.annotations; + innerAnnotations[escapeKey(index)] = recursiveResult.annotations; } else if (isPlainObject(recursiveResult.annotations)) { forEach(recursiveResult.annotations, (tree, key) => { innerAnnotations[escapeKey(index) + '.' + key] = tree; diff --git a/src/types.ts b/src/types.ts index cf182d0..c25bf74 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,5 +42,6 @@ export interface SuperJSONResult { meta?: { values?: MinimisedTree; referentialEqualities?: ReferentialEqualityAnnotations; + v?: number; }; }