diff options
| author | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:54:57 +0100 |
|---|---|---|
| committer | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:57:48 +0100 |
| commit | b1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c (patch) | |
| tree | 49d360fd6cbc6a2754efe93524ac47ff0fbe0f7d /node_modules/tailwindcss/lib/lib/expandApplyAtRules.js | |
Docs
Diffstat (limited to 'node_modules/tailwindcss/lib/lib/expandApplyAtRules.js')
| -rw-r--r-- | node_modules/tailwindcss/lib/lib/expandApplyAtRules.js | 540 |
1 files changed, 540 insertions, 0 deletions
diff --git a/node_modules/tailwindcss/lib/lib/expandApplyAtRules.js b/node_modules/tailwindcss/lib/lib/expandApplyAtRules.js new file mode 100644 index 0000000..1e2ba98 --- /dev/null +++ b/node_modules/tailwindcss/lib/lib/expandApplyAtRules.js @@ -0,0 +1,540 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "default", { + enumerable: true, + get: function() { + return expandApplyAtRules; + } +}); +const _postcss = /*#__PURE__*/ _interop_require_default(require("postcss")); +const _postcssselectorparser = /*#__PURE__*/ _interop_require_default(require("postcss-selector-parser")); +const _generateRules = require("./generateRules"); +const _escapeClassName = /*#__PURE__*/ _interop_require_default(require("../util/escapeClassName")); +const _applyImportantSelector = require("../util/applyImportantSelector"); +const _pseudoElements = require("../util/pseudoElements"); +function _interop_require_default(obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; +} +/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */ function extractClasses(node) { + /** @type {Map<string, Set<string>>} */ let groups = new Map(); + let container = _postcss.default.root({ + nodes: [ + node.clone() + ] + }); + container.walkRules((rule)=>{ + (0, _postcssselectorparser.default)((selectors)=>{ + selectors.walkClasses((classSelector)=>{ + let parentSelector = classSelector.parent.toString(); + let classes = groups.get(parentSelector); + if (!classes) { + groups.set(parentSelector, classes = new Set()); + } + classes.add(classSelector.value); + }); + }).processSync(rule.selector); + }); + let normalizedGroups = Array.from(groups.values(), (classes)=>Array.from(classes)); + let classes = normalizedGroups.flat(); + return Object.assign(classes, { + groups: normalizedGroups + }); +} +let selectorExtractor = (0, _postcssselectorparser.default)(); +/** + * @param {string} ruleSelectors + */ function extractSelectors(ruleSelectors) { + return selectorExtractor.astSync(ruleSelectors); +} +function extractBaseCandidates(candidates, separator) { + let baseClasses = new Set(); + for (let candidate of candidates){ + baseClasses.add(candidate.split(separator).pop()); + } + return Array.from(baseClasses); +} +function prefix(context, selector) { + let prefix = context.tailwindConfig.prefix; + return typeof prefix === "function" ? prefix(selector) : prefix + selector; +} +function* pathToRoot(node) { + yield node; + while(node.parent){ + yield node.parent; + node = node.parent; + } +} +/** + * Only clone the node itself and not its children + * + * @param {*} node + * @param {*} overrides + * @returns + */ function shallowClone(node, overrides = {}) { + let children = node.nodes; + node.nodes = []; + let tmp = node.clone(overrides); + node.nodes = children; + return tmp; +} +/** + * Clone just the nodes all the way to the top that are required to represent + * this singular rule in the tree. + * + * For example, if we have CSS like this: + * ```css + * @media (min-width: 768px) { + * @supports (display: grid) { + * .foo { + * display: grid; + * grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + * } + * } + * + * @supports (backdrop-filter: blur(1px)) { + * .bar { + * backdrop-filter: blur(1px); + * } + * } + * + * .baz { + * color: orange; + * } + * } + * ``` + * + * And we're cloning `.bar` it'll return a cloned version of what's required for just that single node: + * + * ```css + * @media (min-width: 768px) { + * @supports (backdrop-filter: blur(1px)) { + * .bar { + * backdrop-filter: blur(1px); + * } + * } + * } + * ``` + * + * @param {import('postcss').Node} node + */ function nestedClone(node) { + for (let parent of pathToRoot(node)){ + if (node === parent) { + continue; + } + if (parent.type === "root") { + break; + } + node = shallowClone(parent, { + nodes: [ + node + ] + }); + } + return node; +} +/** + * @param {import('postcss').Root} root + */ function buildLocalApplyCache(root, context) { + /** @type {ApplyCache} */ let cache = new Map(); + root.walkRules((rule)=>{ + // Ignore rules generated by Tailwind + for (let node of pathToRoot(rule)){ + var _node_raws_tailwind; + if (((_node_raws_tailwind = node.raws.tailwind) === null || _node_raws_tailwind === void 0 ? void 0 : _node_raws_tailwind.layer) !== undefined) { + return; + } + } + // Clone what's required to represent this singular rule in the tree + let container = nestedClone(rule); + let sort = context.offsets.create("user"); + for (let className of extractClasses(rule)){ + let list = cache.get(className) || []; + cache.set(className, list); + list.push([ + { + layer: "user", + sort, + important: false + }, + container + ]); + } + }); + return cache; +} +/** + * @returns {ApplyCache} + */ function buildApplyCache(applyCandidates, context) { + for (let candidate of applyCandidates){ + if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) { + continue; + } + if (context.classCache.has(candidate)) { + context.applyClassCache.set(candidate, context.classCache.get(candidate).map(([meta, rule])=>[ + meta, + rule.clone() + ])); + continue; + } + let matches = Array.from((0, _generateRules.resolveMatches)(candidate, context)); + if (matches.length === 0) { + context.notClassCache.add(candidate); + continue; + } + context.applyClassCache.set(candidate, matches); + } + return context.applyClassCache; +} +/** + * Build a cache only when it's first used + * + * @param {() => ApplyCache} buildCacheFn + * @returns {ApplyCache} + */ function lazyCache(buildCacheFn) { + let cache = null; + return { + get: (name)=>{ + cache = cache || buildCacheFn(); + return cache.get(name); + }, + has: (name)=>{ + cache = cache || buildCacheFn(); + return cache.has(name); + } + }; +} +/** + * Take a series of multiple caches and merge + * them so they act like one large cache + * + * @param {ApplyCache[]} caches + * @returns {ApplyCache} + */ function combineCaches(caches) { + return { + get: (name)=>caches.flatMap((cache)=>cache.get(name) || []), + has: (name)=>caches.some((cache)=>cache.has(name)) + }; +} +function extractApplyCandidates(params) { + let candidates = params.split(/[\s\t\n]+/g); + if (candidates[candidates.length - 1] === "!important") { + return [ + candidates.slice(0, -1), + true + ]; + } + return [ + candidates, + false + ]; +} +function processApply(root, context, localCache) { + let applyCandidates = new Set(); + // Collect all @apply rules and candidates + let applies = []; + root.walkAtRules("apply", (rule)=>{ + let [candidates] = extractApplyCandidates(rule.params); + for (let util of candidates){ + applyCandidates.add(util); + } + applies.push(rule); + }); + // Start the @apply process if we have rules with @apply in them + if (applies.length === 0) { + return; + } + // Fill up some caches! + let applyClassCache = combineCaches([ + localCache, + buildApplyCache(applyCandidates, context) + ]); + /** + * When we have an apply like this: + * + * .abc { + * @apply hover:font-bold; + * } + * + * What we essentially will do is resolve to this: + * + * .abc { + * @apply .hover\:font-bold:hover { + * font-weight: 500; + * } + * } + * + * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`. + * What happens in this function is that we prepend a `.` and escape the candidate. + * This will result in `.hover\:font-bold` + * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover` + * + * @param {string} selector + * @param {string} utilitySelectors + * @param {string} candidate + */ function replaceSelector(selector, utilitySelectors, candidate) { + let selectorList = extractSelectors(selector); + let utilitySelectorsList = extractSelectors(utilitySelectors); + let candidateList = extractSelectors(`.${(0, _escapeClassName.default)(candidate)}`); + let candidateClass = candidateList.nodes[0].nodes[0]; + selectorList.each((sel)=>{ + /** @type {Set<import('postcss-selector-parser').Selector>} */ let replaced = new Set(); + utilitySelectorsList.each((utilitySelector)=>{ + let hasReplaced = false; + utilitySelector = utilitySelector.clone(); + utilitySelector.walkClasses((node)=>{ + if (node.value !== candidateClass.value) { + return; + } + // Don't replace multiple instances of the same class + // This is theoretically correct but only partially + // We'd need to generate every possible permutation of the replacement + // For example with `.foo + .foo { … }` and `section { @apply foo; }` + // We'd need to generate all of these: + // - `.foo + .foo` + // - `.foo + section` + // - `section + .foo` + // - `section + section` + if (hasReplaced) { + return; + } + // Since you can only `@apply` class names this is sufficient + // We want to replace the matched class name with the selector the user is using + // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)` + node.replaceWith(...sel.nodes.map((node)=>node.clone())); + // Record that we did something and we want to use this new selector + replaced.add(utilitySelector); + hasReplaced = true; + }); + }); + // Sort tag names before class names (but only sort each group (separated by a combinator) + // separately and not in total) + // This happens when replacing `.bar` in `.foo.bar` with a tag like `section` + for (let sel of replaced){ + let groups = [ + [] + ]; + for (let node of sel.nodes){ + if (node.type === "combinator") { + groups.push(node); + groups.push([]); + } else { + let last = groups[groups.length - 1]; + last.push(node); + } + } + sel.nodes = []; + for (let group of groups){ + if (Array.isArray(group)) { + group.sort((a, b)=>{ + if (a.type === "tag" && b.type === "class") { + return -1; + } else if (a.type === "class" && b.type === "tag") { + return 1; + } else if (a.type === "class" && b.type === "pseudo" && b.value.startsWith("::")) { + return -1; + } else if (a.type === "pseudo" && a.value.startsWith("::") && b.type === "class") { + return 1; + } + return 0; + }); + } + sel.nodes = sel.nodes.concat(group); + } + } + sel.replaceWith(...replaced); + }); + return selectorList.toString(); + } + let perParentApplies = new Map(); + // Collect all apply candidates and their rules + for (let apply of applies){ + let [candidates] = perParentApplies.get(apply.parent) || [ + [], + apply.source + ]; + perParentApplies.set(apply.parent, [ + candidates, + apply.source + ]); + let [applyCandidates, important] = extractApplyCandidates(apply.params); + if (apply.parent.type === "atrule") { + if (apply.parent.name === "screen") { + let screenType = apply.parent.params; + throw apply.error(`@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates.map((c)=>`${screenType}:${c}`).join(" ")} instead.`); + } + throw apply.error(`@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`); + } + for (let applyCandidate of applyCandidates){ + if ([ + prefix(context, "group"), + prefix(context, "peer") + ].includes(applyCandidate)) { + // TODO: Link to specific documentation page with error code. + throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`); + } + if (!applyClassCache.has(applyCandidate)) { + throw apply.error(`The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`); + } + let rules = applyClassCache.get(applyCandidate); + candidates.push([ + applyCandidate, + important, + rules + ]); + } + } + for (let [parent, [candidates, atApplySource]] of perParentApplies){ + let siblings = []; + for (let [applyCandidate, important, rules] of candidates){ + let potentialApplyCandidates = [ + applyCandidate, + ...extractBaseCandidates([ + applyCandidate + ], context.tailwindConfig.separator) + ]; + for (let [meta, node] of rules){ + let parentClasses = extractClasses(parent); + let nodeClasses = extractClasses(node); + // When we encounter a rule like `.dark .a, .b { … }` we only want to be left with `[.dark, .a]` if the base applyCandidate is `.a` or with `[.b]` if the base applyCandidate is `.b` + // So we've split them into groups + nodeClasses = nodeClasses.groups.filter((classList)=>classList.some((className)=>potentialApplyCandidates.includes(className))).flat(); + // Add base utility classes from the @apply node to the list of + // classes to check whether it intersects and therefore results in a + // circular dependency or not. + // + // E.g.: + // .foo { + // @apply hover:a; // This applies "a" but with a modifier + // } + // + // We only have to do that with base classes of the `node`, not of the `parent` + // E.g.: + // .hover\:foo { + // @apply bar; + // } + // .bar { + // @apply foo; + // } + // + // This should not result in a circular dependency because we are + // just applying `.foo` and the rule above is `.hover\:foo` which is + // unrelated. However, if we were to apply `hover:foo` then we _did_ + // have to include this one. + nodeClasses = nodeClasses.concat(extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)); + let intersects = parentClasses.some((selector)=>nodeClasses.includes(selector)); + if (intersects) { + throw node.error(`You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`); + } + let root = _postcss.default.root({ + nodes: [ + node.clone() + ] + }); + // Make sure every node in the entire tree points back at the @apply rule that generated it + root.walk((node)=>{ + node.source = atApplySource; + }); + let canRewriteSelector = node.type !== "atrule" || node.type === "atrule" && node.name !== "keyframes"; + if (canRewriteSelector) { + root.walkRules((rule)=>{ + // Let's imagine you have the following structure: + // + // .foo { + // @apply bar; + // } + // + // @supports (a: b) { + // .bar { + // color: blue + // } + // + // .something-unrelated {} + // } + // + // In this case we want to apply `.bar` but it happens to be in + // an atrule node. We clone that node instead of the nested one + // because we still want that @supports rule to be there once we + // applied everything. + // + // However it happens to be that the `.something-unrelated` is + // also in that same shared @supports atrule. This is not good, + // and this should not be there. The good part is that this is + // a clone already and it can be safely removed. The question is + // how do we know we can remove it. Basically what we can do is + // match it against the applyCandidate that you want to apply. If + // it doesn't match the we can safely delete it. + // + // If we didn't do this, then the `replaceSelector` function + // would have replaced this with something that didn't exist and + // therefore it removed the selector altogether. In this specific + // case it would result in `{}` instead of `.something-unrelated {}` + if (!extractClasses(rule).some((candidate)=>candidate === applyCandidate)) { + rule.remove(); + return; + } + // Strip the important selector from the parent selector if at the beginning + let importantSelector = typeof context.tailwindConfig.important === "string" ? context.tailwindConfig.important : null; + // We only want to move the "important" selector if this is a Tailwind-generated utility + // We do *not* want to do this for user CSS that happens to be structured the same + let isGenerated = parent.raws.tailwind !== undefined; + let parentSelector = isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0 ? parent.selector.slice(importantSelector.length) : parent.selector; + // If the selector becomes empty after replacing the important selector + // This means that it's the same as the parent selector and we don't want to replace it + // Otherwise we'll crash + if (parentSelector === "") { + parentSelector = parent.selector; + } + rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate); + // And then re-add it if it was removed + if (importantSelector && parentSelector !== parent.selector) { + rule.selector = (0, _applyImportantSelector.applyImportantSelector)(rule.selector, importantSelector); + } + rule.walkDecls((d)=>{ + d.important = meta.important || important; + }); + // Move pseudo elements to the end of the selector (if necessary) + let selector = (0, _postcssselectorparser.default)().astSync(rule.selector); + selector.each((sel)=>(0, _pseudoElements.movePseudos)(sel)); + rule.selector = selector.toString(); + }); + } + // It could be that the node we were inserted was removed because the class didn't match + // If that was the *only* rule in the parent, then we have nothing add so we skip it + if (!root.nodes[0]) { + continue; + } + // Insert it + siblings.push([ + meta.sort, + root.nodes[0] + ]); + } + } + // Inject the rules, sorted, correctly + let nodes = context.offsets.sort(siblings).map((s)=>s[1]); + // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 } + parent.after(nodes); + } + for (let apply of applies){ + // If there are left-over declarations, just remove the @apply + if (apply.parent.nodes.length > 1) { + apply.remove(); + } else { + // The node is empty, drop the full node + apply.parent.remove(); + } + } + // Do it again, in case we have other `@apply` rules + processApply(root, context, localCache); +} +function expandApplyAtRules(context) { + return (root)=>{ + // Build a cache of the user's CSS so we can use it to resolve classes used by @apply + let localCache = lazyCache(()=>buildLocalApplyCache(root, context)); + processApply(root, context, localCache); + }; +} |