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
+|};