@samitouri / QOS-React / commits / ad607469c5

StyleX plug-in for resolving atomic styles to values for props.xstyle (#22808)

Adds the concept of "plugins" to the inspected element payload. Also adds the first plugin, one that resolves StyleX atomic style names to their values and displays them as a unified style object (rather than a nested array of objects and booleans). Source file names are displayed first, in dim color, followed by an ordered set of resolved style values. For builds with the new feature flag disabled, there is no observable change. A next step to build on top of this could be to make the style values editable, but change the logic such that editing one directly added an inline style to the item (rather than modifying the stylex class– which may be shared between multiple other components).

Brian Vaughn committed Dec 7, 2021 at 20:04 UTC ad607469c58d337d23d05e3be73087d370f7d715
16 files changed +495 -17
packages/react-devtools-shared/src/backend/StyleX/__tests__/utils-test.js new
+231
@@ -0,0 +1,231 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +describe('Stylex plugin utils', () => {
11 + let getStyleXData;
12 + let styleElements;
13 +
14 + function defineStyles(style) {
15 + const styleElement = document.createElement('style');
16 + styleElement.type = 'text/css';
17 + styleElement.appendChild(document.createTextNode(style));
18 +
19 + styleElements.push(styleElement);
20 +
21 + document.head.appendChild(styleElement);
22 + }
23 +
24 + beforeEach(() => {
25 + getStyleXData = require('../utils').getStyleXData;
26 +
27 + styleElements = [];
28 + });
29 +
30 + afterEach(() => {
31 + styleElements.forEach(styleElement => {
32 + document.head.removeChild(styleElement);
33 + });
34 + });
35 +
36 + it('should support simple style objects', () => {
37 + defineStyles(`
38 + .foo {
39 + display: flex;
40 + }
41 + .bar: {
42 + align-items: center;
43 + }
44 + .baz {
45 + flex-direction: center;
46 + }
47 + `);
48 +
49 + expect(
50 + getStyleXData({
51 + // The source/module styles are defined in
52 + Example__style: 'Example__style',
53 +
54 + // Map of CSS style to StyleX class name, booleans, or nested structures
55 + display: 'foo',
56 + flexDirection: 'baz',
57 + alignItems: 'bar',
58 + }),
59 + ).toMatchInlineSnapshot(`
60 + Object {
61 + "resolvedStyles": Object {
62 + "alignItems": "center",
63 + "display": "flex",
64 + "flexDirection": "center",
65 + },
66 + "sources": Array [
67 + "Example__style",
68 + ],
69 + }
70 + `);
71 + });
72 +
73 + it('should support multiple style objects', () => {
74 + defineStyles(`
75 + .foo {
76 + display: flex;
77 + }
78 + .bar: {
79 + align-items: center;
80 + }
81 + .baz {
82 + flex-direction: center;
83 + }
84 + `);
85 +
86 + expect(
87 + getStyleXData([
88 + {Example1__style: 'Example1__style', display: 'foo'},
89 + {
90 + Example2__style: 'Example2__style',
91 + flexDirection: 'baz',
92 + alignItems: 'bar',
93 + },
94 + ]),
95 + ).toMatchInlineSnapshot(`
96 + Object {
97 + "resolvedStyles": Object {
98 + "alignItems": "center",
99 + "display": "flex",
100 + "flexDirection": "center",
101 + },
102 + "sources": Array [
103 + "Example1__style",
104 + "Example2__style",
105 + ],
106 + }
107 + `);
108 + });
109 +
110 + it('should filter empty rules', () => {
111 + defineStyles(`
112 + .foo {
113 + display: flex;
114 + }
115 + .bar: {
116 + align-items: center;
117 + }
118 + .baz {
119 + flex-direction: center;
120 + }
121 + `);
122 +
123 + expect(
124 + getStyleXData([
125 + false,
126 + {Example1__style: 'Example1__style', display: 'foo'},
127 + false,
128 + false,
129 + {
130 + Example2__style: 'Example2__style',
131 + flexDirection: 'baz',
132 + alignItems: 'bar',
133 + },
134 + false,
135 + ]),
136 + ).toMatchInlineSnapshot(`
137 + Object {
138 + "resolvedStyles": Object {
139 + "alignItems": "center",
140 + "display": "flex",
141 + "flexDirection": "center",
142 + },
143 + "sources": Array [
144 + "Example1__style",
145 + "Example2__style",
146 + ],
147 + }
148 + `);
149 + });
150 +
151 + it('should support pseudo-classes', () => {
152 + defineStyles(`
153 + .foo {
154 + color: black;
155 + }
156 + .bar: {
157 + color: blue;
158 + }
159 + .baz {
160 + text-decoration: none;
161 + }
162 + `);
163 +
164 + expect(
165 + getStyleXData({
166 + // The source/module styles are defined in
167 + Example__style: 'Example__style',
168 +
169 + // Map of CSS style to StyleX class name, booleans, or nested structures
170 + color: 'foo',
171 + ':hover': {
172 + color: 'bar',
173 + textDecoration: 'baz',
174 + },
175 + }),
176 + ).toMatchInlineSnapshot(`
177 + Object {
178 + "resolvedStyles": Object {
179 + ":hover": Object {
180 + "color": "blue",
181 + "textDecoration": "none",
182 + },
183 + "color": "black",
184 + },
185 + "sources": Array [
186 + "Example__style",
187 + ],
188 + }
189 + `);
190 + });
191 +
192 + it('should support nested selectors', () => {
193 + defineStyles(`
194 + .foo {
195 + display: flex;
196 + }
197 + .bar: {
198 + align-items: center;
199 + }
200 + .baz {
201 + flex-direction: center;
202 + }
203 + `);
204 +
205 + expect(
206 + getStyleXData([
207 + {Example1__style: 'Example1__style', display: 'foo'},
208 + false,
209 + [
210 + false,
211 + {Example2__style: 'Example2__style', flexDirection: 'baz'},
212 + {Example3__style: 'Example3__style', alignItems: 'bar'},
213 + ],
214 + false,
215 + ]),
216 + ).toMatchInlineSnapshot(`
217 + Object {
218 + "resolvedStyles": Object {
219 + "alignItems": "center",
220 + "display": "flex",
221 + "flexDirection": "center",
222 + },
223 + "sources": Array [
224 + "Example1__style",
225 + "Example2__style",
226 + "Example3__style",
227 + ],
228 + }
229 + `);
230 + });
231 +});
packages/react-devtools-shared/src/backend/StyleX/utils.js new
+110
@@ -0,0 +1,110 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {StyleXPlugin} from 'react-devtools-shared/src/types';
11 +
12 +const cachedStyleNameToValueMap: Map<string, string> = new Map();
13 +
14 +export function getStyleXData(data: any): StyleXPlugin {
15 + const sources = new Set();
16 + const resolvedStyles = {};
17 +
18 + crawlData(data, sources, resolvedStyles);
19 +
20 + return {
21 + sources: Array.from(sources).sort(),
22 + resolvedStyles,
23 + };
24 +}
25 +
26 +export function crawlData(
27 + data: any,
28 + sources: Set<string>,
29 + resolvedStyles: Object,
30 +): void {
31 + if (Array.isArray(data)) {
32 + data.forEach(entry => {
33 + if (Array.isArray(entry)) {
34 + crawlData(entry, sources, resolvedStyles);
35 + } else {
36 + crawlObjectProperties(entry, sources, resolvedStyles);
37 + }
38 + });
39 + } else {
40 + crawlObjectProperties(data, sources, resolvedStyles);
41 + }
42 +
43 + resolvedStyles = Object.fromEntries<string, any>(
44 + Object.entries(resolvedStyles).sort(),
45 + );
46 +}
47 +
48 +function crawlObjectProperties(
49 + entry: Object,
50 + sources: Set<string>,
51 + resolvedStyles: Object,
52 +): void {
53 + const keys = Object.keys(entry);
54 + keys.forEach(key => {
55 + const value = entry[key];
56 + if (typeof value === 'string') {
57 + if (key === value) {
58 + // Special case; this key is the name of the style's source/file/module.
59 + sources.add(key);
60 + } else {
61 + resolvedStyles[key] = getPropertyValueForStyleName(value);
62 + }
63 + } else {
64 + const nestedStyle = {};
65 + resolvedStyles[key] = nestedStyle;
66 + crawlData([value], sources, nestedStyle);
67 + }
68 + });
69 +}
70 +
71 +function getPropertyValueForStyleName(styleName: string): string | null {
72 + if (cachedStyleNameToValueMap.has(styleName)) {
73 + return ((cachedStyleNameToValueMap.get(styleName): any): string);
74 + }
75 +
76 + for (
77 + let styleSheetIndex = 0;
78 + styleSheetIndex < document.styleSheets.length;
79 + styleSheetIndex++
80 + ) {
81 + const styleSheet = ((document.styleSheets[
82 + styleSheetIndex
83 + ]: any): CSSStyleSheet);
84 + // $FlowFixMe Flow doesn't konw about these properties
85 + const rules = styleSheet.rules || styleSheet.cssRules;
86 + for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
87 + const rule = rules[ruleIndex];
88 + // $FlowFixMe Flow doesn't konw about these properties
89 + const {cssText, selectorText, style} = rule;
90 +
91 + if (selectorText != null) {
92 + if (selectorText.startsWith(`.${styleName}`)) {
93 + const match = cssText.match(/{ *([a-z\-]+):/);
94 + if (match !== null) {
95 + const property = match[1];
96 + const value = style.getPropertyValue(property);
97 +
98 + cachedStyleNameToValueMap.set(styleName, value);
99 +
100 + return value;
101 + } else {
102 + return null;
103 + }
104 + }
105 + }
106 + }
107 + }
108 +
109 + return null;
110 +}
packages/react-devtools-shared/src/backend/legacy/renderer.js
+4
@@ -838,6 +838,10 @@ export function attach(
838 rootType: null,
839 rendererPackageName: null,
840 rendererVersion: null,
841 +
842 + plugins: {
843 + stylex: null,
844 + },
845 };
846 }
847
packages/react-devtools-shared/src/backend/renderer.js
+18 -1
@@ -82,10 +82,14 @@ import {
82 MEMO_SYMBOL_STRING,
83 } from './ReactSymbols';
84 import {format} from './utils';
85 -import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
85 +import {
86 + enableProfilerChangedHookIndices,
87 + enableStyleXFeatures,
88 +} from 'react-devtools-feature-flags';
89 import is from 'shared/objectIs';
90 import isArray from 'shared/isArray';
91 import hasOwnProperty from 'shared/hasOwnProperty';
92 +import {getStyleXData} from './StyleX/utils';
93
94 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
95 import type {
@@ -108,6 +112,7 @@ import type {
112 import type {
113 ComponentFilter,
114 ElementType,
115 + Plugins,
116 } from 'react-devtools-shared/src/types';
117
118 type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
@@ -3234,6 +3239,16 @@ export function attach(
3239 targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
3240 }
3241
3242 + const plugins: Plugins = {
3243 + stylex: null,
3244 + };
3245 +
3246 + if (enableStyleXFeatures) {
3247 + if (memoizedProps.hasOwnProperty('xstyle')) {
3248 + plugins.stylex = getStyleXData(memoizedProps.xstyle);
3249 + }
3250 + }
3251 +
3252 return {
3253 id,
3254
@@ -3293,6 +3308,8 @@ export function attach(
3308 rootType,
3309 rendererPackageName: renderer.rendererPackageName,
3310 rendererVersion: renderer.version,
3311 +
3312 + plugins,
3313 };
3314 }
3315
packages/react-devtools-shared/src/backend/types.js
+4
@@ -13,6 +13,7 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
13 import type {
14 ComponentFilter,
15 ElementType,
16 + Plugins,
17 } from 'react-devtools-shared/src/types';
18 import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
19
@@ -265,6 +266,9 @@ export type InspectedElement = {|
266 // Meta information about the renderer that created this element.
267 rendererPackageName: string | null,
268 rendererVersion: string | null,
269 +
270 + // UI plugins/visualizations for the inspected element.
271 + plugins: Plugins,
272 |};
273
274 export const InspectElementErrorType = 'error';
packages/react-devtools-shared/src/backendAPI.js
+2
@@ -208,6 +208,7 @@ export function convertInspectedElementBackendToFrontend(
208 owners,
209 context,
210 hooks,
211 + plugins,
212 props,
213 rendererPackageName,
214 rendererVersion,
@@ -233,6 +234,7 @@ export function convertInspectedElementBackendToFrontend(
234 hasLegacyContext,
235 id,
236 key,
237 + plugins,
238 rendererPackageName,
239 rendererVersion,
240 rootType,
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js
+4 -3
@@ -13,11 +13,12 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 +export const consoleManagedByDevToolsDuringStrictMode = false;
17 +export const enableLogger = true;
18 +export const enableNamedHooksFeature = true;
19 export const enableProfilerChangedHookIndices = true;
20 +export const enableStyleXFeatures = true;
21 export const isInternalFacebookBuild = true;
18 -export const enableNamedHooksFeature = true;
19 -export const enableLogger = true;
20 -export const consoleManagedByDevToolsDuringStrictMode = false;
22
23 /************************************************************************
24 * Do not edit the code below.
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js
+4 -3
@@ -13,11 +13,12 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 +export const consoleManagedByDevToolsDuringStrictMode = false;
17 +export const enableLogger = false;
18 +export const enableNamedHooksFeature = true;
19 export const enableProfilerChangedHookIndices = true;
20 +export const enableStyleXFeatures = false;
21 export const isInternalFacebookBuild = false;
18 -export const enableNamedHooksFeature = true;
19 -export const enableLogger = false;
20 -export const consoleManagedByDevToolsDuringStrictMode = false;
22
23 /************************************************************************
24 * Do not edit the code below.
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js
+4 -3
@@ -13,8 +13,9 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 +export const consoleManagedByDevToolsDuringStrictMode = true;
17 +export const enableLogger = false;
18 +export const enableNamedHooksFeature = true;
19 export const enableProfilerChangedHookIndices = true;
20 +export const enableStyleXFeatures = false;
21 export const isInternalFacebookBuild = false;
18 -export const enableNamedHooksFeature = true;
19 -export const enableLogger = false;
20 -export const consoleManagedByDevToolsDuringStrictMode = true;
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js
+4 -3
@@ -13,11 +13,12 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 +export const consoleManagedByDevToolsDuringStrictMode = true;
17 +export const enableLogger = true;
18 +export const enableNamedHooksFeature = true;
19 export const enableProfilerChangedHookIndices = true;
20 +export const enableStyleXFeatures = true;
21 export const isInternalFacebookBuild = true;
18 -export const enableNamedHooksFeature = true;
19 -export const enableLogger = true;
20 -export const consoleManagedByDevToolsDuringStrictMode = true;
22
23 /************************************************************************
24 * Do not edit the code below.
packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js
+4 -3
@@ -13,11 +13,12 @@
13 * It should always be imported from "react-devtools-feature-flags".
14 ************************************************************************/
15
16 +export const consoleManagedByDevToolsDuringStrictMode = true;
17 +export const enableLogger = false;
18 +export const enableNamedHooksFeature = true;
19 export const enableProfilerChangedHookIndices = true;
20 +export const enableStyleXFeatures = false;
21 export const isInternalFacebookBuild = false;
18 -export const enableNamedHooksFeature = true;
19 -export const enableLogger = false;
20 -export const consoleManagedByDevToolsDuringStrictMode = true;
22
23 /************************************************************************
24 * Do not edit the code below.
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.css new
+6
@@ -0,0 +1,6 @@
1 +.Source {
2 + color: var(--color-dim);
3 + margin-left: 1rem;
4 + overflow: auto;
5 + text-overflow: ellipsis;
6 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStyleXPlugin.js new
+76
@@ -0,0 +1,76 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import KeyValue from './KeyValue';
12 +import Store from '../../store';
13 +import sharedStyles from './InspectedElementSharedStyles.css';
14 +import styles from './InspectedElementStyleXPlugin.css';
15 +import {enableStyleXFeatures} from 'react-devtools-feature-flags';
16 +
17 +import type {InspectedElement} from './types';
18 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
19 +import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
20 +
21 +type Props = {|
22 + bridge: FrontendBridge,
23 + element: Element,
24 + inspectedElement: InspectedElement,
25 + store: Store,
26 +|};
27 +
28 +export default function InspectedElementStyleXPlugin({
29 + bridge,
30 + element,
31 + inspectedElement,
32 + store,
33 +}: Props) {
34 + if (!enableStyleXFeatures) {
35 + return null;
36 + }
37 +
38 + const styleXPlugin = inspectedElement.plugins.stylex;
39 + if (styleXPlugin == null) {
40 + return null;
41 + }
42 +
43 + const {resolvedStyles, sources} = styleXPlugin;
44 +
45 + return (
46 + <div className={sharedStyles.InspectedElementTree}>
47 + <div className={sharedStyles.HeaderRow}>
48 + <div className={sharedStyles.Header}>stylex</div>
49 + </div>
50 + {sources.map(source => (
51 + <div key={source} className={styles.Source}>
52 + {source}
53 + </div>
54 + ))}
55 + {Object.entries(resolvedStyles).map(([name, value]) => (
56 + <KeyValue
57 + key={name}
58 + alphaSort={true}
59 + bridge={bridge}
60 + canDeletePaths={false}
61 + canEditValues={false}
62 + canRenamePaths={false}
63 + depth={1}
64 + element={element}
65 + hidden={false}
66 + inspectedElement={inspectedElement}
67 + name={name}
68 + path={[name]}
69 + pathRoot="stylex"
70 + store={store}
71 + value={value}
72 + />
73 + ))}
74 + </div>
75 + );
76 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+11
@@ -23,6 +23,7 @@ import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWa
23 import InspectedElementHooksTree from './InspectedElementHooksTree';
24 import InspectedElementPropsTree from './InspectedElementPropsTree';
25 import InspectedElementStateTree from './InspectedElementStateTree';
26 +import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
27 import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
28 import NativeStyleEditor from './NativeStyleEditor';
29 import Badge from './Badge';
@@ -31,6 +32,7 @@ import {
32 copyInspectedElementPath as copyInspectedElementPathAPI,
33 storeAsGlobal as storeAsGlobalAPI,
34 } from 'react-devtools-shared/src/backendAPI';
35 +import {enableStyleXFeatures} from 'react-devtools-feature-flags';
36
37 import styles from './InspectedElementView.css';
38
@@ -124,6 +126,15 @@ export default function InspectedElementView({
126 store={store}
127 />
128
129 + {enableStyleXFeatures && (
130 + <InspectedElementStyleXPlugin
131 + bridge={bridge}
132 + element={element}
133 + inspectedElement={inspectedElement}
134 + store={store}
135 + />
136 + )}
137 +
138 <InspectedElementErrorsAndWarningsTree
139 bridge={bridge}
140 element={element}
packages/react-devtools-shared/src/devtools/views/Components/types.js
+4 -1
@@ -12,7 +12,7 @@ import type {
12 Dehydrated,
13 Unserializable,
14 } from 'react-devtools-shared/src/hydration';
15 -import type {ElementType} from 'react-devtools-shared/src/types';
15 +import type {ElementType, Plugins} from 'react-devtools-shared/src/types';
16
17 // Each element on the frontend corresponds to a Fiber on the backend.
18 // Some of its information (e.g. id, type, displayName) come from the backend.
@@ -114,6 +114,9 @@ export type InspectedElement = {|
114 // Meta information about the renderer that created this element.
115 rendererPackageName: string | null,
116 rendererVersion: string | null,
117 +
118 + // UI plugins/visualizations for the inspected element.
119 + plugins: Plugins,
120 |};
121
122 // TODO: Add profiling type
packages/react-devtools-shared/src/types.js
+9
@@ -91,3 +91,12 @@ export type LRUCache<K, V> = {|
91 reset: () => void,
92 set: (key: K, value: V) => void,
93 |};
94 +
95 +export type StyleXPlugin = {|
96 + sources: Array<string>,
97 + resolvedStyles: Object,
98 +|};
99 +
100 +export type Plugins = {|
101 + stylex: StyleXPlugin | null,
102 +|};