From 44f572d13e6b5d64aa1db3726f54e310f7ec2aa9 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Thu, 25 Mar 2021 17:09:51 -0400 Subject: [PATCH] Respect apply sibling order --- src/lib/expandApplyAtRules.js | 257 ++++++++++++++++++++-------------- tests/10-apply.test.css | 30 +++- tests/10-apply.test.html | 1 + tests/10-apply.test.js | 8 +- 4 files changed, 188 insertions(+), 108 deletions(-) diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index b969074..5124e21 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -40,135 +40,182 @@ function extractApplyCandidates(params) { return [candidates, false] } -function expandApplyAtRules(context) { - return function processApply(root) { - let applyCandidates = new Set() +function partitionApplyParents(root) { + let applyParents = new Set() + + root.walkAtRules('apply', (rule) => { + applyParents.add(rule.parent) + }) + + for (let rule of applyParents) { + let nodeGroups = [] + let lastGroup = [] + + for (let node of rule.nodes) { + if (node.type === 'atrule' && node.name === 'apply') { + if (lastGroup.length > 0) { + nodeGroups.push(lastGroup) + lastGroup = [] + } + nodeGroups.push([node]) + } else { + lastGroup.push(node) + } + } - // Collect all @apply rules and candidates - let applies = [] - root.walkAtRules('apply', (rule) => { - let [candidates, important] = extractApplyCandidates(rule.params) + if (lastGroup.length > 0) { + nodeGroups.push(lastGroup) + } - 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) { - // Fill up some caches! - let applyClassCache = 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` - */ - // TODO: Should we use postcss-selector-parser for this instead? - function replaceSelector(selector, utilitySelectors, candidate) { - let needle = `.${escapeClassName(candidate)}` - let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g) - - return selector - .split(/\s*,\s*/g) - .map((s) => { - let replaced = [] - - for (let utilitySelector of utilitySelectorsList) { - let replacedSelector = utilitySelector.replace(needle, s) - if (replacedSelector === utilitySelector) { - continue - } - replaced.push(replacedSelector) - } - return replaced.join(', ') - }) - .join(', ') - } + if (nodeGroups.length === 1) { + continue + } - /** @type {Map} */ - let perParentApplies = new Map() + for (let group of [...nodeGroups].reverse()) { + let newParent = rule.clone({ nodes: [] }) + newParent.append(group) + rule.after(newParent) + } - // Collect all apply candidates and their rules - for (let apply of applies) { - let candidates = perParentApplies.get(apply.parent) || [] + rule.remove() + } +} - perParentApplies.set(apply.parent, candidates) +function processApply(root, context) { + let applyCandidates = new Set() - let [applyCandidates, important] = extractApplyCandidates(apply.params) + // Collect all @apply rules and candidates + let applies = [] + root.walkAtRules('apply', (rule) => { + let [candidates] = extractApplyCandidates(rule.params) - for (let applyCandidate of applyCandidates) { - 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.` - ) + 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) { + // Fill up some caches! + let applyClassCache = 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` + */ + // TODO: Should we use postcss-selector-parser for this instead? + function replaceSelector(selector, utilitySelectors, candidate) { + let needle = `.${escapeClassName(candidate)}` + let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g) + + return selector + .split(/\s*,\s*/g) + .map((s) => { + let replaced = [] + + for (let utilitySelector of utilitySelectorsList) { + let replacedSelector = utilitySelector.replace(needle, s) + if (replacedSelector === utilitySelector) { + continue + } + replaced.push(replacedSelector) } + return replaced.join(', ') + }) + .join(', ') + } + + /** @type {Map} */ + let perParentApplies = new Map() + + // Collect all apply candidates and their rules + for (let apply of applies) { + let candidates = perParentApplies.get(apply.parent) || [] - let rules = applyClassCache.get(applyCandidate) + perParentApplies.set(apply.parent, candidates) - candidates.push([applyCandidate, important, rules]) + let [applyCandidates, important] = extractApplyCandidates(apply.params) + + for (let applyCandidate of applyCandidates) { + 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 (const [parent, candidates] of perParentApplies) { - let siblings = [] + for (const [parent, candidates] of perParentApplies) { + let siblings = [] - for (let [applyCandidate, important, rules] of candidates) { - for (let [meta, node] of rules) { - let root = postcss.root({ nodes: [node.clone()] }) - let canRewriteSelector = - node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes') + for (let [applyCandidate, important, rules] of candidates) { + for (let [meta, node] of rules) { + let root = postcss.root({ nodes: [node.clone()] }) + let canRewriteSelector = + node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes') - if (canRewriteSelector) { - root.walkRules((rule) => { - rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate) + if (canRewriteSelector) { + root.walkRules((rule) => { + rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate) - rule.walkDecls((d) => { - d.important = important - }) + rule.walkDecls((d) => { + d.important = important }) - } - - siblings.push([meta, root.nodes[0]]) + }) } + + siblings.push([meta, root.nodes[0]]) } + } - // Inject the rules, sorted, correctly - const nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1]) + // Inject the rules, sorted, correctly + let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1]) - // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 } - parent.after(nodes) - } + // console.log(parent) + // `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() - } + 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) } + + // Do it again, in case we have other `@apply` rules + processApply(root, context) + } +} + +function expandApplyAtRules(context) { + return (root) => { + partitionApplyParents(root) + processApply(root, context) } } diff --git a/tests/10-apply.test.css b/tests/10-apply.test.css index dd7ee96..60b6f08 100644 --- a/tests/10-apply.test.css +++ b/tests/10-apply.test.css @@ -265,6 +265,32 @@ color: green; font-weight: 700; } +.add-sibling-properties { + padding: 2rem; + padding-left: 1rem; + padding-right: 1rem; +} +.add-sibling-properties:hover { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +@media (min-width: 1024px) { + .add-sibling-properties { + padding-left: 2.5rem; + padding-right: 2.5rem; + } +} +@media (min-width: 1280px) { + .add-sibling-properties:focus { + padding-left: 0.25rem; + padding-right: 0.25rem; + } +} +.add-sibling-properties { + padding-top: 3px; + color: green; + font-weight: 700; +} h1 { font-size: 1.5rem; line-height: 2rem; @@ -285,13 +311,13 @@ h2 { font-size: 1.5rem; line-height: 2rem; } -@media (min-width: 640px) { +@media (min-width: 1024px) { h2 { font-size: 1.5rem; line-height: 2rem; } } -@media (min-width: 1024px) { +@media (min-width: 640px) { h2 { font-size: 1.5rem; line-height: 2rem; diff --git a/tests/10-apply.test.html b/tests/10-apply.test.html index b63cc4b..4cffa15 100644 --- a/tests/10-apply.test.html +++ b/tests/10-apply.test.html @@ -31,6 +31,7 @@
+
diff --git a/tests/10-apply.test.js b/tests/10-apply.test.js index a9fa533..d513cd1 100644 --- a/tests/10-apply.test.js +++ b/tests/10-apply.test.js @@ -78,7 +78,7 @@ test('@apply', () => { @apply use-dependant-only-a font-normal; } .btn { - @apply font-bold py-2 px-4 rounded; + @apply font-bold py-2 px-4 rounded; } .btn-blue { @apply btn bg-blue-500 hover:bg-blue-700 text-white; @@ -99,6 +99,12 @@ test('@apply', () => { .use-with-other-properties-component { @apply use-with-other-properties-base; } + .add-sibling-properties { + padding: 2rem; + @apply px-4 hover:px-2 lg:px-10 xl:focus:px-1; + padding-top: 3px; + @apply use-with-other-properties-base; + } h1 { @apply text-2xl lg:text-2xl sm:text-3xl;