From 167b67ed70db4476bda570446a241b9a7787fff3 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 22 Nov 2021 16:41:23 -0500 Subject: [PATCH] StyleX plug-in for resolving atomic styles to values for props.xstyle --- .../backend/StyleX/__tests__/utils-test.js | 182 ++++++++++++++++++ .../src/backend/StyleX/utils.js | 81 ++++++++ .../src/backend/renderer.js | 10 +- 3 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 packages/react-devtools-shared/src/backend/StyleX/__tests__/utils-test.js create mode 100644 packages/react-devtools-shared/src/backend/StyleX/utils.js diff --git a/packages/react-devtools-shared/src/backend/StyleX/__tests__/utils-test.js b/packages/react-devtools-shared/src/backend/StyleX/__tests__/utils-test.js new file mode 100644 index 0000000000000..201a31c0de534 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/StyleX/__tests__/utils-test.js @@ -0,0 +1,182 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +describe('Stylex plugin utils', () => { + let getStyleXValues; + let styleElements; + + function defineStyles(style) { + const styleElement = document.createElement('style'); + styleElement.type = 'text/css'; + styleElement.appendChild(document.createTextNode(style)); + + styleElements.push(styleElement); + + document.head.appendChild(styleElement); + } + + beforeEach(() => { + getStyleXValues = require('../utils').getStyleXValues; + + styleElements = []; + }); + + afterEach(() => { + styleElements.forEach(styleElement => { + document.head.removeChild(styleElement); + }); + }); + + it('should support simple style objects', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXValues({ + display: 'foo', + flexDirection: 'baz', + alignItems: 'bar', + }), + ).toMatchInlineSnapshot(` + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + } + `); + }); + + it('should support multiple style objects', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXValues([ + {display: 'foo'}, + {flexDirection: 'baz', alignItems: 'bar'}, + ]), + ).toMatchInlineSnapshot(` + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + } + `); + }); + + it('should filter empty rules', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXValues([ + false, + {display: 'foo'}, + false, + false, + {flexDirection: 'baz', alignItems: 'bar'}, + false, + ]), + ).toMatchInlineSnapshot(` + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + } + `); + }); + + it('should support pseudo-classes', () => { + defineStyles(` + .foo { + color: black; + } + .bar: { + color: blue; + } + .baz { + text-decoration: none; + } + `); + + expect( + getStyleXValues({ + color: 'foo', + ':hover': { + color: 'bar', + textDecoration: 'baz', + }, + }), + ).toMatchInlineSnapshot(` + Object { + ":hover": Object { + "color": "blue", + "textDecoration": "none", + }, + "color": "black", + } + `); + }); + + it('should support nested selectors', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXValues([ + {display: 'foo'}, + false, + [false, {flexDirection: 'baz'}, {alignItems: 'bar'}], + false, + ]), + ).toMatchInlineSnapshot(` + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + } + `); + }); +}); diff --git a/packages/react-devtools-shared/src/backend/StyleX/utils.js b/packages/react-devtools-shared/src/backend/StyleX/utils.js new file mode 100644 index 0000000000000..d5d2ff8ab9148 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/StyleX/utils.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const cachedStyleNameToValueMap: Map = new Map(); + +export function getStyleXValues(data: any, mappedStyles: Object = {}) { + if (Array.isArray(data)) { + data.forEach(entry => { + if (Array.isArray(entry)) { + getStyleXValues(entry, mappedStyles); + } else { + crawlObjectProperties(entry, mappedStyles); + } + }); + } else { + crawlObjectProperties(data, mappedStyles); + } + + return Object.fromEntries(Object.entries(mappedStyles).sort()); +} + +function crawlObjectProperties(entry: Object, mappedStyles: Object) { + const keys = Object.keys(entry); + keys.forEach(key => { + const value = entry[key]; + if (typeof value === 'string') { + mappedStyles[key] = getPropertyValueForStyleName(value); + } else { + const nestedStyle = {}; + mappedStyles[key] = nestedStyle; + getStyleXValues([value], nestedStyle); + } + }); +} + +function getPropertyValueForStyleName(styleName: string): string | null { + if (cachedStyleNameToValueMap.has(styleName)) { + return ((cachedStyleNameToValueMap.get(styleName): any): string); + } + + for ( + let styleSheetIndex = 0; + styleSheetIndex < document.styleSheets.length; + styleSheetIndex++ + ) { + const styleSheet = ((document.styleSheets[ + styleSheetIndex + ]: any): CSSStyleSheet); + // $FlowFixMe Flow doesn't konw about these properties + const rules = styleSheet.rules || styleSheet.cssRules; + for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) { + const rule = rules[ruleIndex]; + // $FlowFixMe Flow doesn't konw about these properties + const {cssText, selectorText, style} = rule; + + if (selectorText != null) { + if (selectorText.startsWith(`.${styleName}`)) { + const match = cssText.match(/{ *([a-z\-]+):/); + if (match !== null) { + const property = match[1]; + const value = style.getPropertyValue(property); + + cachedStyleNameToValueMap.set(styleName, value); + + return value; + } else { + return null; + } + } + } + } + } + + return null; +} diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 8a89a5406c6ef..2f9b807d03b49 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -86,6 +86,7 @@ import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags'; import is from 'shared/objectIs'; import isArray from 'shared/isArray'; import hasOwnProperty from 'shared/hasOwnProperty'; +import {getStyleXValues} from './StyleX/utils'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type { @@ -3234,6 +3235,13 @@ export function attach( targetErrorBoundaryID = getNearestErrorBoundaryID(fiber); } + const modifiedProps = { + ...memoizedProps, + }; + if (modifiedProps.hasOwnProperty('xstyle')) { + modifiedProps.xstyle = getStyleXValues(modifiedProps.xstyle); + } + return { id, @@ -3279,7 +3287,7 @@ export function attach( // TODO Review sanitization approach for the below inspectable values. context, hooks, - props: memoizedProps, + props: modifiedProps, state: showState ? memoizedState : null, errors: Array.from(errors.entries()), warnings: Array.from(warnings.entries()),