From 889c04cce5fdc5cb8fc1e484fdc9bb8a4514ec28 Mon Sep 17 00:00:00 2001 From: James Berry Date: Wed, 13 Apr 2022 03:11:15 +0100 Subject: [PATCH] fix/root-typeerror --- packages/core/src/createStitches.js | 1 + packages/core/src/utility/createMemo.js | 6 +- .../core/src/utility/safeJsonStringify.js | 63 +++++++++++++++++++ packages/core/tests/issue-832.js | 13 ++++ packages/core/types/config.d.ts | 7 ++- packages/react/types/config.d.ts | 7 ++- 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/utility/safeJsonStringify.js create mode 100644 packages/core/tests/issue-832.js diff --git a/packages/core/src/createStitches.js b/packages/core/src/createStitches.js index 1223a9b8..27ab8bc7 100644 --- a/packages/core/src/createStitches.js +++ b/packages/core/src/createStitches.js @@ -34,6 +34,7 @@ export const createStitches = (config) => { theme, themeMap, utils, + root, } /** Internal stylesheet. */ diff --git a/packages/core/src/utility/createMemo.js b/packages/core/src/utility/createMemo.js index 179ee827..a86aea0e 100644 --- a/packages/core/src/utility/createMemo.js +++ b/packages/core/src/utility/createMemo.js @@ -1,13 +1,13 @@ -const stringifyReplacer = (name, data) => (typeof data === 'function' ? { '()': Function.prototype.toString.call(data) } : data) +import { safeJsonStringify } from './safeJsonStringify.js' -const stringify = (value) => JSON.stringify(value, stringifyReplacer) +const stringifyReplacer = (name, data) => (typeof data === 'function' ? { '()': Function.prototype.toString.call(data) } : data) /** @type {() => any>(value: T, apply: F, ...args: A) => ReturnType} */ export const createMemo = () => { const cache = Object.create(null) return (value, apply, ...args) => { - const vjson = stringify(value) + const vjson = safeJsonStringify(value, stringifyReplacer) return vjson in cache ? cache[vjson] : (cache[vjson] = apply(value, ...args)) } diff --git a/packages/core/src/utility/safeJsonStringify.js b/packages/core/src/utility/safeJsonStringify.js new file mode 100644 index 00000000..755b97b1 --- /dev/null +++ b/packages/core/src/utility/safeJsonStringify.js @@ -0,0 +1,63 @@ +// https://github.com/debitoor/safe-json-stringify/blob/master/index.js +import { hasOwn } from './hasOwn.js' + +function throwsMessage(err) { + return '[Throws: ' + (err ? err.message : '?') + ']' +} + +function safeGetValueFromPropertyOnObject(obj, property) { + if (hasOwn(obj, property)) { + try { + return obj[property] + } catch (error) { + return throwsMessage(error) + } + } + return obj[property] +} + +function ensureProperties(obj) { + const seen = [] // store references to objects we have seen before + + function visit(obj) { + if (obj === null || typeof obj !== 'object') { + return obj + } + + if (seen.indexOf(obj) !== -1) { + return '[Circular]' + } + + seen.push(obj) + + if (typeof obj.toJSON === 'function') { + try { + const result = visit(obj.toJSON()) + seen.pop() + return result + } catch(err) { + return throwsMessage(err) + } + } + + if (Array.isArray(obj)) { + const result = obj.map(visit) + seen.pop() + return result + } + + const result = Object.keys(obj).reduce(function(acc, prop) { + // prevent faulty defined getter properties + acc[prop] = visit(safeGetValueFromPropertyOnObject(obj, prop)) + return acc + }, {}) + seen.pop() + return result + } + + return visit(obj) +} + +export const safeJsonStringify = (data, replacer, space) => { + return JSON.stringify(ensureProperties(data), replacer, space) +} diff --git a/packages/core/tests/issue-832.js b/packages/core/tests/issue-832.js new file mode 100644 index 00000000..62e7c18a --- /dev/null +++ b/packages/core/tests/issue-832.js @@ -0,0 +1,13 @@ +import { createStitches } from '../src/index.js' +import { createMemo } from '../src/utility/createMemo.js' + +describe('Issue #832', () => { + test('Circular object bug reproduction', () => { + const circularObject = { } + circularObject.self = circularObject + + expect(() => JSON.stringify(circularObject)).toThrow() + expect(() => createMemo(circularObject)).toNotThrow() + expect(() => createStitches(circularObject)).toNotThrow() + }) +}) diff --git a/packages/core/types/config.d.ts b/packages/core/types/config.d.ts index 05ca3285..af4d13d1 100644 --- a/packages/core/types/config.d.ts +++ b/packages/core/types/config.d.ts @@ -44,6 +44,9 @@ declare namespace ConfigType { [K in keyof CSSUtil.CSSProperties]?: CSSUtil.CSSProperties[K] | V }) : never } + + /** Root interface. */ + export type Root = T extends DocumentOrShadowRoot ? T : DocumentOrShadowRoot } /** Default ThemeMap. */ @@ -197,7 +200,8 @@ export type CreateStitches = { Media extends {} = {}, Theme extends {} = {}, ThemeMap extends {} = DefaultThemeMap, - Utils extends {} = {} + Utils extends {} = {}, + Root extends DocumentOrShadowRoot = Document >( config?: { prefix?: ConfigType.Prefix @@ -205,6 +209,7 @@ export type CreateStitches = { theme?: ConfigType.Theme themeMap?: ConfigType.ThemeMap utils?: ConfigType.Utils + root?: ConfigType.Root } ): Stitches } diff --git a/packages/react/types/config.d.ts b/packages/react/types/config.d.ts index 05ca3285..af4d13d1 100644 --- a/packages/react/types/config.d.ts +++ b/packages/react/types/config.d.ts @@ -44,6 +44,9 @@ declare namespace ConfigType { [K in keyof CSSUtil.CSSProperties]?: CSSUtil.CSSProperties[K] | V }) : never } + + /** Root interface. */ + export type Root = T extends DocumentOrShadowRoot ? T : DocumentOrShadowRoot } /** Default ThemeMap. */ @@ -197,7 +200,8 @@ export type CreateStitches = { Media extends {} = {}, Theme extends {} = {}, ThemeMap extends {} = DefaultThemeMap, - Utils extends {} = {} + Utils extends {} = {}, + Root extends DocumentOrShadowRoot = Document >( config?: { prefix?: ConfigType.Prefix @@ -205,6 +209,7 @@ export type CreateStitches = { theme?: ConfigType.Theme themeMap?: ConfigType.ThemeMap utils?: ConfigType.Utils + root?: ConfigType.Root } ): Stitches }