/**
* Copyright (c) Meta Platforms, Inc. and 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/frontend/types';
import isArray from 'react-devtools-shared/src/isArray';
const cachedStyleNameToValueMap: Map<string, string> = new Map();
export function getStyleXData(data: any): StyleXPlugin {
const sources = new Set<string>();
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 (data == null) {
return;
}
if (isArray(data)) {
data.forEach(entry => {
if (entry == null) {
return;
}
if (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 {
const propertyValue = getPropertyValueForStyleName(value);
if (propertyValue != null) {
resolvedStyles[key] = propertyValue;
}
}
} 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);
let rules: CSSRuleList | null = null;
// this might throw if CORS rules are enforced https://www.w3.org/TR/cssom-1/#the-cssstylesheet-interface
try {
rules = styleSheet.cssRules;
} catch (_e) {
continue;
}
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
if (!(rules[ruleIndex] instanceof CSSStyleRule)) {
continue;
}
const rule = ((rules[ruleIndex]: any): CSSStyleRule);
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;
}