import _ from 'lodash' import selectorParser from 'postcss-selector-parser' import postcss from 'postcss' import didYouMean from 'didyoumean' import substituteTailwindAtRules from './substituteTailwindAtRules' import evaluateTailwindFunctions from './evaluateTailwindFunctions' import substituteVariantsAtRules from './substituteVariantsAtRules' import substituteResponsiveAtRules from './substituteResponsiveAtRules' import convertLayerAtRulesToControlComments from './convertLayerAtRulesToControlComments' import substituteScreenAtRules from './substituteScreenAtRules' import prefixSelector from '../util/prefixSelector' import { useMemo } from '../util/useMemo' function hasAtRule(css, atRule) { let foundAtRule = false css.walkAtRules(atRule, () => { foundAtRule = true return false }) return foundAtRule } function cloneWithoutChildren(node) { if (node.type === 'atrule') { return postcss.atRule({ name: node.name, params: node.params }) } if (node.type === 'rule') { return postcss.rule({ name: node.name, selectors: node.selectors }) } const clone = node.clone() clone.removeAll() return clone } const tailwindApplyPlaceholder = selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__', }) function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWiths) { const parser = selectorParser((selectors) => { let i = 0 selectors.walkClasses((c) => { if (classPosition === i++ && c.value === className) { c.replaceWith(tailwindApplyPlaceholder) } }) }) const processedSelectors = _.flatMap(rule.selectors, (selector) => { // You could argue we should make this replacement at the AST level, but if we believe // the placeholder string is safe from collisions then it is safe to do this is a simple // string replacement, and much, much faster. return replaceWiths.map((replaceWith) => parser.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith) ) }) const cloned = rule.clone() let current = cloned let parent = rule.parent while (parent && parent.type !== 'root') { const parentClone = cloneWithoutChildren(parent) parentClone.append(current) current.parent = parentClone current = parentClone parent = parent.parent } cloned.selectors = processedSelectors return current } const extractUtilityNamesParser = selectorParser((selectors) => { let classes = [] selectors.walkClasses((c) => classes.push(c.value)) return classes }) const extractUtilityNames = useMemo( (selector) => extractUtilityNamesParser.transformSync(selector), (selector) => selector ) const cloneRuleWithParent = useMemo( (rule) => rule.clone({ parent: rule.parent }), (rule) => rule ) function buildUtilityMap(css, lookupTree) { let index = 0 const utilityMap = {} function handle(getRule, rule) { const utilityNames = extractUtilityNames(rule.selector) utilityNames.forEach((utilityName, i) => { if (utilityMap[utilityName] === undefined) { utilityMap[utilityName] = [] } utilityMap[utilityName].push({ index, utilityName, classPosition: i, ...getRule(rule), }) index++ }) } // Lookup tree is the big lookup tree, making the rule lazy allows us to save // some memory because we don't need everything. lookupTree.walkRules( handle.bind(null, (rule) => ({ get rule() { return cloneRuleWithParent(rule) }, })) ) // This is the end user's css. This might contain rules that we want to // apply. We want immediate copies of everything in case that we have user // defined classes that are recursively applied. Down below we are modifying // the rules directly. We could do a better solution where we keep track of a // dependency tree, but that is a bit more complex. Might revisit later, // we'll see how this turns out! css.walkRules(handle.bind(null, (rule) => ({ rule: cloneRuleWithParent(rule) }))) return utilityMap } function mergeAdjacentRules(initialRule, rulesToInsert) { let previousRule = initialRule rulesToInsert.forEach((toInsert) => { if ( toInsert.type === 'rule' && previousRule.type === 'rule' && toInsert.selector === previousRule.selector ) { previousRule.append(toInsert.nodes) } else if ( toInsert.type === 'atrule' && previousRule.type === 'atrule' && toInsert.params === previousRule.params ) { const merged = mergeAdjacentRules( previousRule.nodes[previousRule.nodes.length - 1], toInsert.nodes ) previousRule.append(merged) } else { previousRule = toInsert } toInsert.walk((n) => { if (n.nodes && n.nodes.length === 0) { n.remove() } }) }) return rulesToInsert.filter((r) => r.nodes.length > 0) } function makeExtractUtilityRules(css, lookupTree, config) { const utilityMap = buildUtilityMap(css, lookupTree) return function extractUtilityRules(utilityNames, rule) { const combined = [] utilityNames.forEach((utilityName) => { if (utilityMap[utilityName] === undefined) { // Look for prefixed utility in case the user has goofed const prefixedUtility = prefixSelector(config.prefix, `.${utilityName}`).slice(1) if (utilityMap[prefixedUtility] !== undefined) { throw rule.error( `The \`${utilityName}\` class does not exist, but \`${prefixedUtility}\` does. Did you forget the prefix?` ) } const suggestedClass = didYouMean(utilityName, Object.keys(utilityMap)) const suggestionMessage = suggestedClass ? `, but \`${suggestedClass}\` does` : '' throw rule.error( `The \`${utilityName}\` class does not exist${suggestionMessage}. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, { word: utilityName } ) } combined.push(...utilityMap[utilityName]) }) return combined.sort((a, b) => a.index - b.index) } } function findParent(rule, predicate) { let parent = rule.parent while (parent) { if (predicate(parent)) { return parent } parent = parent.parent } throw new Error('No parent could be found') } function processApplyAtRules(css, lookupTree, config) { const extractUtilityRules = makeExtractUtilityRules(css, lookupTree, config) do { css.walkAtRules('apply', (applyRule) => { const parent = applyRule.parent // Direct parent const nearestParentRule = findParent(applyRule, (r) => r.type === 'rule') const currentUtilityNames = extractUtilityNames(nearestParentRule.selector) const [ importantEntries, applyUtilityNames, important = importantEntries.length > 0, ] = _.partition(applyRule.params.split(/[\s\t\n]+/g), (n) => n === '!important') if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) { const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0] throw parent.error( `You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.` ) } // Extract any post-apply declarations and re-insert them after apply rules const afterRule = parent.clone({ raws: {} }) afterRule.nodes = afterRule.nodes.slice(parent.index(applyRule) + 1) parent.nodes = parent.nodes.slice(0, parent.index(applyRule) + 1) // Sort applys to match CSS source order const applys = extractUtilityRules(applyUtilityNames, applyRule) // Get new rules with the utility portion of the selector replaced with the new selector const rulesToInsert = [] applys.forEach( nearestParentRule === parent ? (util) => rulesToInsert.push(generateRulesFromApply(util, parent.selectors)) : (util) => util.rule.nodes.forEach((n) => afterRule.append(n.clone())) ) rulesToInsert.forEach((rule) => { if (rule.type === 'atrule') { rule.walkRules((rule) => { rule.__tailwind = { ...rule.__tailwind, important } }) } else { rule.__tailwind = { ...rule.__tailwind, important } } }) const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), (root) => { root.walkDecls((d) => { d.important = important }) }) const mergedRules = mergeAdjacentRules(nearestParentRule, [...nodes, afterRule]) applyRule.remove() parent.after(mergedRules) // If the base rule has nothing in it (all applys were pseudo or responsive variants), // remove the rule fuggit. if (parent.nodes.length === 0) { parent.remove() } }) // We already know that we have at least 1 @apply rule. Otherwise this // function would not have been called. Therefore we can execute this code // at least once. This also means that in the best case scenario we only // call this 2 times, instead of 3 times. // 1st time -> before we call this function // 2nd time -> when we check if we have to do this loop again (because do {} while (check)) // .. instead of // 1st time -> before we call this function // 2nd time -> when we check the first time (because while (check) do {}) // 3rd time -> when we re-check to see if we should do this loop again } while (hasAtRule(css, 'apply')) return css } let defaultTailwindTree = null export default function substituteClassApplyAtRules(config, getProcessedPlugins, configChanged) { return function (css) { // We can stop already when we don't have any @apply rules. Vue users: you're welcome! if (!hasAtRule(css, 'apply')) { return css } // Tree already contains @tailwind rules, don't prepend default Tailwind tree if (hasAtRule(css, 'tailwind')) { return processApplyAtRules(css, postcss.root(), config) } // Tree contains no @tailwind rules, so generate all of Tailwind's styles and // prepend them to the user's CSS. Important for