Skip to content

Commit

Permalink
StyleX plug-in for resolving atomic styles to values for props.xstyle (
Browse files Browse the repository at this point in the history
…#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).
  • Loading branch information
Brian Vaughn authored Dec 8, 2021
1 parent 5041c37 commit ad60746
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -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",
],
}
`);
});
});
110 changes: 110 additions & 0 deletions packages/react-devtools-shared/src/backend/StyleX/utils.js
Original file line number Diff line number Diff line change
@@ -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<string, string> = 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<string>,
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<string, any>(
Object.entries(resolvedStyles).sort(),
);
}

function crawlObjectProperties(
entry: Object,
sources: Set<string>,
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;
}
4 changes: 4 additions & 0 deletions packages/react-devtools-shared/src/backend/legacy/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,10 @@ export function attach(
rootType: null,
rendererPackageName: null,
rendererVersion: null,

plugins: {
stylex: null,
},
};
}

Expand Down
Loading

0 comments on commit ad60746

Please sign in to comment.