UNPKG

postcss-theme-fold

Version:

[![NPM Version][npm-img]][npm-url] [![github (ci)][github-ci]][github-ci]

219 lines (218 loc) 12.4 kB
"use strict"; 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); }); }; });