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..6783fea27f7ef --- /dev/null +++ b/packages/react-devtools-shared/src/backend/StyleX/__tests__/utils-test.js @@ -0,0 +1,231 @@ +/** + * 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 getStyleXData; + 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(() => { + getStyleXData = require('../utils').getStyleXData; + + 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( + getStyleXData({ + // The source/module styles are defined in + Example__style: 'Example__style', + + // Map of CSS style to StyleX class name, booleans, or nested structures + display: 'foo', + flexDirection: 'baz', + alignItems: 'bar', + }), + ).toMatchInlineSnapshot(` + Object { + "resolvedStyles": Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + }, + "sources": Array [ + "Example__style", + ], + } + `); + }); + + it('should support multiple style objects', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXData([ + {Example1__style: 'Example1__style', display: 'foo'}, + { + Example2__style: 'Example2__style', + flexDirection: 'baz', + alignItems: 'bar', + }, + ]), + ).toMatchInlineSnapshot(` + Object { + "resolvedStyles": Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + }, + "sources": Array [ + "Example1__style", + "Example2__style", + ], + } + `); + }); + + it('should filter empty rules', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXData([ + false, + {Example1__style: 'Example1__style', display: 'foo'}, + false, + false, + { + Example2__style: 'Example2__style', + flexDirection: 'baz', + alignItems: 'bar', + }, + false, + ]), + ).toMatchInlineSnapshot(` + Object { + "resolvedStyles": Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + }, + "sources": Array [ + "Example1__style", + "Example2__style", + ], + } + `); + }); + + it('should support pseudo-classes', () => { + defineStyles(` + .foo { + color: black; + } + .bar: { + color: blue; + } + .baz { + text-decoration: none; + } + `); + + expect( + getStyleXData({ + // The source/module styles are defined in + Example__style: 'Example__style', + + // Map of CSS style to StyleX class name, booleans, or nested structures + color: 'foo', + ':hover': { + color: 'bar', + textDecoration: 'baz', + }, + }), + ).toMatchInlineSnapshot(` + Object { + "resolvedStyles": Object { + ":hover": Object { + "color": "blue", + "textDecoration": "none", + }, + "color": "black", + }, + "sources": Array [ + "Example__style", + ], + } + `); + }); + + it('should support nested selectors', () => { + defineStyles(` + .foo { + display: flex; + } + .bar: { + align-items: center; + } + .baz { + flex-direction: center; + } + `); + + expect( + getStyleXData([ + {Example1__style: 'Example1__style', display: 'foo'}, + false, + [ + false, + {Example2__style: 'Example2__style', flexDirection: 'baz'}, + {Example3__style: 'Example3__style', alignItems: 'bar'}, + ], + false, + ]), + ).toMatchInlineSnapshot(` + Object { + "resolvedStyles": Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "center", + }, + "sources": Array [ + "Example1__style", + "Example2__style", + "Example3__style", + ], + } + `); + }); +}); 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..6bf5a2978927e --- /dev/null +++ b/packages/react-devtools-shared/src/backend/StyleX/utils.js @@ -0,0 +1,110 @@ +/** + * 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 + */ + +import type {StyleXPlugin} from 'react-devtools-shared/src/types'; + +const cachedStyleNameToValueMap: Map = new Map(); + +export function getStyleXData(data: any): StyleXPlugin { + const sources = new Set(); + const resolvedStyles = {}; + + crawlData(data, sources, resolvedStyles); + + return { + sources: Array.from(sources).sort(), + resolvedStyles, + }; +} + +export function crawlData( + data: any, + sources: Set, + resolvedStyles: Object, +): void { + if (Array.isArray(data)) { + data.forEach(entry => { + if (Array.isArray(entry)) { + crawlData(entry, sources, resolvedStyles); + } else { + crawlObjectProperties(entry, sources, resolvedStyles); + } + }); + } else { + crawlObjectProperties(data, sources, resolvedStyles); + } + + resolvedStyles = Object.fromEntries( + Object.entries(resolvedStyles).sort(), + ); +} + +function crawlObjectProperties( + entry: Object, + sources: Set, + resolvedStyles: Object, +): void { + const keys = Object.keys(entry); + keys.forEach(key => { + const value = entry[key]; + if (typeof value === 'string') { + if (key === value) { + // Special case; this key is the name of the style's source/file/module. + sources.add(key); + } else { + resolvedStyles[key] = getPropertyValueForStyleName(value); + } + } else { + const nestedStyle = {}; + resolvedStyles[key] = nestedStyle; + crawlData([value], sources, 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/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index c5684f222fa31..b757d008ac294 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -838,6 +838,10 @@ export function attach( rootType: null, rendererPackageName: null, rendererVersion: null, + + plugins: { + stylex: null, + }, }; } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 8a89a5406c6ef..7aebe6e507a0f 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -82,10 +82,14 @@ import { MEMO_SYMBOL_STRING, } from './ReactSymbols'; import {format} from './utils'; -import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags'; +import { + enableProfilerChangedHookIndices, + enableStyleXFeatures, +} from 'react-devtools-feature-flags'; import is from 'shared/objectIs'; import isArray from 'shared/isArray'; import hasOwnProperty from 'shared/hasOwnProperty'; +import {getStyleXData} from './StyleX/utils'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type { @@ -108,6 +112,7 @@ import type { import type { ComponentFilter, ElementType, + Plugins, } from 'react-devtools-shared/src/types'; type getDisplayNameForFiberType = (fiber: Fiber) => string | null; @@ -3234,6 +3239,16 @@ export function attach( targetErrorBoundaryID = getNearestErrorBoundaryID(fiber); } + const plugins: Plugins = { + stylex: null, + }; + + if (enableStyleXFeatures) { + if (memoizedProps.hasOwnProperty('xstyle')) { + plugins.stylex = getStyleXData(memoizedProps.xstyle); + } + } + return { id, @@ -3293,6 +3308,8 @@ export function attach( rootType, rendererPackageName: renderer.rendererPackageName, rendererVersion: renderer.version, + + plugins, }; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 333ee30914cdd..3318c8b6a965e 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -13,6 +13,7 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type { ComponentFilter, ElementType, + Plugins, } from 'react-devtools-shared/src/types'; import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; @@ -265,6 +266,9 @@ export type InspectedElement = {| // Meta information about the renderer that created this element. rendererPackageName: string | null, rendererVersion: string | null, + + // UI plugins/visualizations for the inspected element. + plugins: Plugins, |}; export const InspectElementErrorType = 'error'; diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index 23b975e38d6ea..3849899b7df02 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -208,6 +208,7 @@ export function convertInspectedElementBackendToFrontend( owners, context, hooks, + plugins, props, rendererPackageName, rendererVersion, @@ -233,6 +234,7 @@ export function convertInspectedElementBackendToFrontend( hasLegacyContext, id, key, + plugins, rendererPackageName, rendererVersion, rootType, diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js index 50eb47967baff..f382a2b4b2e27 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js @@ -13,11 +13,12 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ +export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableLogger = true; +export const enableNamedHooksFeature = true; export const enableProfilerChangedHookIndices = true; +export const enableStyleXFeatures = true; export const isInternalFacebookBuild = true; -export const enableNamedHooksFeature = true; -export const enableLogger = true; -export const consoleManagedByDevToolsDuringStrictMode = false; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js index fd569a7c550dd..579efaedd619d 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js @@ -13,11 +13,12 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ +export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableLogger = false; +export const enableNamedHooksFeature = true; export const enableProfilerChangedHookIndices = true; +export const enableStyleXFeatures = false; export const isInternalFacebookBuild = false; -export const enableNamedHooksFeature = true; -export const enableLogger = false; -export const consoleManagedByDevToolsDuringStrictMode = false; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js index 9552dd0bd6ef0..3bd4efb101c76 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js @@ -13,8 +13,9 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ +export const consoleManagedByDevToolsDuringStrictMode = true; +export const enableLogger = false; +export const enableNamedHooksFeature = true; export const enableProfilerChangedHookIndices = true; +export const enableStyleXFeatures = false; export const isInternalFacebookBuild = false; -export const enableNamedHooksFeature = true; -export const enableLogger = false; -export const consoleManagedByDevToolsDuringStrictMode = true; diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js index b3bd5a7a92dcf..d86b0ddf73ab5 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js @@ -13,11 +13,12 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ +export const consoleManagedByDevToolsDuringStrictMode = true; +export const enableLogger = true; +export const enableNamedHooksFeature = true; export const enableProfilerChangedHookIndices = true; +export const enableStyleXFeatures = true; export const isInternalFacebookBuild = true; -export const enableNamedHooksFeature = true; -export const enableLogger = true; -export const consoleManagedByDevToolsDuringStrictMode = true; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js index 53466bf84bb7f..f1a307306242a 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js @@ -13,11 +13,12 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ +export const consoleManagedByDevToolsDuringStrictMode = true; +export const enableLogger = false; +export const enableNamedHooksFeature = true; export const enableProfilerChangedHookIndices = true; +export const enableStyleXFeatures = false; export const isInternalFacebookBuild = false; -export const enableNamedHooksFeature = true; -export const enableLogger = false; -export const consoleManagedByDevToolsDuringStrictMode = true; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.css new file mode 100644 index 0000000000000..0aa7f62e27923 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.css @@ -0,0 +1,6 @@ +.Source { + color: var(--color-dim); + margin-left: 1rem; + overflow: auto; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.js new file mode 100644 index 0000000000000..8045803fd0118 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.js @@ -0,0 +1,76 @@ +/** + * 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 + */ + +import * as React from 'react'; +import KeyValue from './KeyValue'; +import Store from '../../store'; +import sharedStyles from './InspectedElementSharedStyles.css'; +import styles from './InspectedElementStyleXPlugin.css'; +import {enableStyleXFeatures} from 'react-devtools-feature-flags'; + +import type {InspectedElement} from './types'; +import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types'; + +type Props = {| + bridge: FrontendBridge, + element: Element, + inspectedElement: InspectedElement, + store: Store, +|}; + +export default function InspectedElementStyleXPlugin({ + bridge, + element, + inspectedElement, + store, +}: Props) { + if (!enableStyleXFeatures) { + return null; + } + + const styleXPlugin = inspectedElement.plugins.stylex; + if (styleXPlugin == null) { + return null; + } + + const {resolvedStyles, sources} = styleXPlugin; + + return ( +
+
+
stylex
+
+ {sources.map(source => ( +
+ {source} +
+ ))} + {Object.entries(resolvedStyles).map(([name, value]) => ( +
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 8424621fcf16c..ad1272b722cb3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -23,6 +23,7 @@ import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWa import InspectedElementHooksTree from './InspectedElementHooksTree'; import InspectedElementPropsTree from './InspectedElementPropsTree'; import InspectedElementStateTree from './InspectedElementStateTree'; +import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin'; import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle'; import NativeStyleEditor from './NativeStyleEditor'; import Badge from './Badge'; @@ -31,6 +32,7 @@ import { copyInspectedElementPath as copyInspectedElementPathAPI, storeAsGlobal as storeAsGlobalAPI, } from 'react-devtools-shared/src/backendAPI'; +import {enableStyleXFeatures} from 'react-devtools-feature-flags'; import styles from './InspectedElementView.css'; @@ -124,6 +126,15 @@ export default function InspectedElementView({ store={store} /> + {enableStyleXFeatures && ( + + )} + = {| reset: () => void, set: (key: K, value: V) => void, |}; + +export type StyleXPlugin = {| + sources: Array, + resolvedStyles: Object, +|}; + +export type Plugins = {| + stylex: StyleXPlugin | null, +|};