Skip to content

Commit ad60746

Browse files
author
Brian Vaughn
authored
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).
1 parent 5041c37 commit ad60746

16 files changed

+495
-17
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,10 @@ export function attach(
838838
rootType: null,
839839
rendererPackageName: null,
840840
rendererVersion: null,
841+
842+
plugins: {
843+
stylex: null,
844+
},
841845
};
842846
}
843847

0 commit comments

Comments
 (0)