postcss-theme-fold
Version:
[![NPM Version][npm-img]][npm-url] [![github (ci)][github-ci]][github-ci]
219 lines (218 loc) • 12.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const postcss_1 = require("postcss");
const cache_1 = require("./cache");
const shared_1 = require("./shared");
const extract_variables_from_themes_1 = require("./extract-variables-from-themes");
const uniq_1 = require("./uniq");
const processed_map_1 = require("./processed-map");
const extract_variables_from_string_1 = require("./extract-variables-from-string");
function getVariableMeta(themeMap, variableName) {
const variableMeta = { themeSelector: '', value: '' };
for (const [themeSelector, variablesMap] of themeMap) {
if (variablesMap.has(variableName)) {
variableMeta.themeSelector = themeSelector;
variableMeta.value = variablesMap.get(variableName);
}
}
return variableMeta;
}
exports.default = (0, postcss_1.plugin)('postcss-theme-fold', (options = { themes: [], globalSelectors: [] }) => {
if (options.themes.length === 0) {
throw new Error('Theme options not provided.');
}
if (options.mode === undefined) {
if (options.themes.length === 1) {
options.mode = 'single-theme';
}
else {
options.mode = 'multi-themes';
}
}
if (options.mode === 'single-theme') {
if (options.themes.length > 2) {
throw new Error('For single mode themes should contains one theme.');
}
}
const preserveSet = Array.isArray(options.preserve) ? new Set(options.preserve) : undefined;
return async (root) => {
const themesSet = await (0, cache_1.getFromCache)(options.themes, () => (0, extract_variables_from_themes_1.extractVariablesFromThemes)(options.themes));
const uniqVariables = new Set([...themesSet].reduce((res, themeMap) => {
for (const [, variablesMap] of themeMap) {
res = res.concat([...variablesMap.keys()]);
}
return res;
}, []));
const processedSelectorsSet = new Set();
const processedPropsMap = new Map();
root.walkRules((rule) => {
var _a, _b, _c;
// Remove theme selectors cuz css variables not needed in runtime.
if (shared_1.THEME_SELECTOR_RE.test(rule.selector)) {
rule.remove();
return;
}
if (rule.nodes === undefined) {
return;
}
const rules = [];
for (const theme of themesSet) {
const nextRule = rule.clone();
const processedProps = {};
const themeSelectors = {};
const brokenNodes = [];
// Cast to `EnhancedChildNode` cuz before we already check nodes for undefined.
for (const node of nextRule.nodes) {
if (options.preserve) {
// Create original node for preserve processing.
node.original = node.clone();
node.original.parent = node.parent;
}
if (node.type === 'decl') {
if (options.shouldProcessVariable !== undefined &&
options.shouldProcessVariable(node) === false) {
continue;
}
const variables = (0, extract_variables_from_string_1.extractVariablesFromString)(node.value);
// Use this variables for debug.
const usedVariables = [];
for (const variable of variables) {
// Variable absence in uniqVariables means that
// it's not present in any theme.
node.processed = uniqVariables.has(variable);
if (!node.processed) {
continue;
}
const { value, themeSelector } = getVariableMeta(theme, variable);
// When variable not found then skip this rule for processing.
if (node.value && value !== '') {
// Mark node as processed and remove them later.
const variableRe = new RegExp(`var\\(${variable}\\)`);
node.value = node.value.replace(variableRe, value);
const nextProp = processedProps[node.prop] || { selectors: [], nodes: [] };
// Accumulate theme selectors only for multi themes.
if (options.mode === 'multi-themes') {
nextProp.selectors.push(themeSelector);
}
usedVariables.push(variable);
nextProp.nodes.push(node);
processedProps[node.prop] = nextProp;
const parent = node.parent;
if (parent.type === 'rule') {
const rootSelector = options.mode === 'multi-themes' ? themeSelector : parent.selector.trim();
(0, processed_map_1.addValueToMap)(processedPropsMap, rootSelector, {
selector: parent.selector.trim(),
value: node.value,
prop: node.prop,
});
}
}
else {
// If variable is absent in current theme,
// it can pe present in another, however, but we have to warn about it.
node.broken = true;
brokenNodes.push(node);
if (!options.disableWarnings) {
// prettier-ignore
console.error(`❗️❗️❗️ Missing value for ${variable} for ${[...theme.keys()].join(', ')}. Deleting css rule...`);
}
}
}
if (options.debug) {
const commentNode = (0, postcss_1.comment)({ text: usedVariables.join(', ') });
(_a = processedProps[node.prop]) === null || _a === void 0 ? void 0 : _a.nodes.unshift(commentNode);
}
const hasVariablesToPreserve = preserveSet && variables.some(variable => preserveSet.has(variable));
if (hasVariablesToPreserve) {
const originalNode = node.original;
for (let variable of variables) {
const variableRe = new RegExp(`var\\(${variable}\\)`);
const { value: resolvedVariable } = getVariableMeta(theme, variable);
const replaceWith = preserveSet.has(variable) ? `var(${variable}, ${resolvedVariable})` : resolvedVariable;
originalNode.value = originalNode.value.replace(variableRe, replaceWith);
}
(_b = processedProps[node.prop]) === null || _b === void 0 ? void 0 : _b.nodes.push(originalNode);
}
else if (options.preserve === true) {
(_c = processedProps[node.prop]) === null || _c === void 0 ? void 0 : _c.nodes.push(node.original);
}
}
}
// When `processedProps` is empty this means rule not have css variables from theme,
// and we not cache this in `processedSelectorsSet` cuz him may be declared in other place.
// `processedProps` absence can possibly mean that variable value is missing only in
// one particular theme, not it every theme.
if (Object.keys(processedProps).length === 0) {
// Also that can mean that we met some rule without variables -
// rules like this do not have broken or processed nodes, so we can
// break to prevert double adding.
if (!brokenNodes.length) {
rules.push(nextRule);
break;
}
else {
// ...and continue for rules, which have some chance to be
// processed in another themes (with broken nodes).
continue;
}
}
// Prevent duplicate already processed selectors.
if (!processedSelectorsSet.has(nextRule.selector)) {
processedSelectorsSet.add(nextRule.selector);
// Remove already processed and broken (without value) nodes from base rule.
nextRule.nodes = nextRule.nodes.filter((node) => !node.processed && !node.broken);
if (nextRule.nodes.length > 0) {
// Push nextRule before forkedRule,
// cuz them maybe contains not processed selectors.
rules.push(nextRule);
}
}
// Create shape with unique theme selectors and nodes.
for (const key in processedProps) {
const processedProp = processedProps[key];
if (!processedProp) {
continue;
}
const selector = (0, uniq_1.uniq)(processedProp.selectors).join('');
const nodes = (0, uniq_1.uniq)(processedProp.nodes);
if (themeSelectors[selector] === undefined) {
themeSelectors[selector] = [];
}
themeSelectors[selector].push(...nodes);
}
for (const themeSelector in themeSelectors) {
const forkedRule = rule.clone();
// Add extra theme selectors for forked rule.
forkedRule.selectors = forkedRule.selectors.flatMap((selector) => {
// Only work for single root selector, e.g. `.utilityfocus .Button {...}`.
const maybeGlobalSelector = (options.globalSelectors || []).find((globalSelector) => {
const [firstSelector] = selector.split(' ');
return firstSelector === globalSelector;
});
if (maybeGlobalSelector === undefined) {
return [`${themeSelector} ${selector}`];
}
const nextSelector = selector.replace(maybeGlobalSelector, '');
if (options.mode === 'multi-themes') {
return [
`${maybeGlobalSelector} ${themeSelector} ${nextSelector}`,
`${maybeGlobalSelector}${themeSelector} ${nextSelector}`,
];
}
return `${maybeGlobalSelector} ${themeSelector} ${nextSelector}`;
});
forkedRule.nodes = themeSelectors[themeSelector];
// Prevent duplicate already processed selectors.
const rootSelector = options.mode === 'multi-themes' ? themeSelector : forkedRule.selector.trim();
if (!processedSelectorsSet.has(forkedRule.selector) ||
(0, processed_map_1.hasUnprocessedNodes)(processedPropsMap, rootSelector)) {
(0, processed_map_1.checkNodesProcessed)(processedPropsMap, (forkedRule.nodes || []), themeSelector);
processedSelectorsSet.add(forkedRule.selector);
rules.push(forkedRule);
}
}
}
rule.replaceWith(...rules);
});
};
});
;