From c5bdcf4ff91388917a555194d1dd3a50b15524db Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 28 Feb 2021 23:06:14 +0100 Subject: [PATCH 1/2] implement `@apply` first pass --- src/index.js | 122 +++++++++++++++++++++++++++++++++++++++++++++ src/index.test.css | 94 ++++++++++++++++++++++++++++++++++ src/index.test.js | 30 +++++++++++ 3 files changed, 246 insertions(+) diff --git a/src/index.js b/src/index.js index f6b136d..7495d22 100644 --- a/src/index.js +++ b/src/index.js @@ -484,6 +484,10 @@ function isObject(value) { return typeof value === 'object' && value !== null } +function isPlainObject(value) { + return isObject(value) && !Array.isArray(value) +} + function isEmpty(obj) { return Object.keys(obj).length === 0 } @@ -687,6 +691,8 @@ module.exports = (pluginOptions = {}) => { return postcss([ // substituteTailwindAtRules function (root) { + let applyCandidates = new Set() + // Make sure this file contains Tailwind directives. If not, we can save // a lot of work and bail early. Also we don't have to register our touch // file as a dependency since the output of this CSS does not depend on @@ -719,6 +725,15 @@ module.exports = (pluginOptions = {}) => { } }) + // Collect all @apply rules and candidates + let applies = [] + root.walkAtRules('apply', (rule) => { + for (let util of rule.params.split(/[\s\t\n]+/g)) { + applyCandidates.add(util) + } + applies.push(rule) + }) + if (!foundTailwind) { return root } @@ -767,6 +782,113 @@ module.exports = (pluginOptions = {}) => { candidates, context ) + + // Start the @apply process if we have rules with @apply in them + if (applies.length > 0) { + // Fill up some caches! + generateRules(context.tailwindConfig, 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, utilitySelector, candidate) { + return selector + .split(/\s*,\s*/g) + .map((s) => utilitySelector.replace(`.${escape(candidate)}`, s)) + .join(', ') + } + + function updateSelectors(rule, apply, candidate) { + return rule.map(([selector, rule]) => { + if (!isPlainObject(rule)) { + return [selector, updateSelectors(rule, apply, candidate)] + } + return [replaceSelector(apply.parent.selector, selector, candidate), rule] + }) + } + + for (let apply of applies) { + let siblings = [] + let applyCandidates = apply.params.split(/[\s\t\n]+/g) + for (let applyCandidate of applyCandidates) { + // TODO: Check for user css rules? + if (!context.classCache.has(applyCandidate)) { + throw new Error('Utility does not exist!') + } + + let [layerName, rules] = context.classCache.get(applyCandidate) + for (let [sort, [selector, rule]] of rules) { + // Nested rules... + if (!isPlainObject(rule)) { + siblings.push([ + sort, + toPostCssNode( + [selector, updateSelectors(rule, apply, applyCandidate)], + context.postCssNodeCache + ), + ]) + } else { + let appliedSelector = replaceSelector( + apply.parent.selector, + selector, + applyCandidate + ) + + if (appliedSelector !== apply.parent.selector) { + siblings.push([ + sort, + toPostCssNode( + [ + replaceSelector(apply.parent.selector, selector, applyCandidate), + rule, + ], + context.postCssNodeCache + ), + ]) + continue + } + + // Add declarations directly + for (let property in rule) { + apply.before(postcss.decl({ prop: property, value: rule[property] })) + } + } + } + } + + // Inject the rules, sorted, correctly + for (let [sort, sibling] of siblings.sort(([a], [z]) => Math.sign(Number(z - a)))) { + // `apply.parent` is refering to the node at `.abc` in: .abc { @apply mt-2 } + apply.parent.after(sibling) + } + + // 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() + } + } + } env.DEBUG && console.timeEnd('Generate rules') // We only ever add to the classCache, so if it didn't grow, there is nothing new. diff --git a/src/index.test.css b/src/index.test.css index 046ea80..053cf43 100644 --- a/src/index.test.css +++ b/src/index.test.css @@ -4,6 +4,100 @@ 'Segoe UI Symbol', 'Noto Color Emoji'; color: #3b82f6; } +.apply-test { + margin-top: 1.5rem; + --tw-bg-opacity: 1; + background-color: rgba(236, 72, 153, var(--tw-bg-opacity)); +} +.apply-test:hover { + font-weight: 700; +} +.apply-test:focus:hover { + font-weight: 700; +} +@media (min-width: 640px) { + .apply-test { + --tw-bg-opacity: 1; + background-color: rgba(16, 185, 129, var(--tw-bg-opacity)); + } +} +@media (min-width: 640px) { + .apply-test:focus:nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgba(251, 207, 232, var(--tw-bg-opacity)); + } +} +.apply-components { + width: 100%; + margin-left: auto; + margin-right: auto; +} +@media (min-width: 1536px) { + .apply-components { + max-width: 1536px; + } +} +@media (min-width: 1280px) { + .apply-components { + max-width: 1280px; + } +} +@media (min-width: 1024px) { + .apply-components { + max-width: 1024px; + } +} +@media (min-width: 768px) { + .apply-components { + max-width: 768px; + } +} +@media (min-width: 640px) { + .apply-components { + max-width: 640px; + } +} +.drop-empty-rules:hover { + font-weight: 700; +} +.group:hover .apply-group { + font-weight: 700; +} +.dark .apply-dark-mode { + font-weight: 700; +} +.apply-with-existing:hover { + font-weight: 400; +} +@media (min-width: 640px) { + .apply-with-existing:hover { + --tw-bg-opacity: 1; + background-color: rgba(16, 185, 129, var(--tw-bg-opacity)); + } +} +.multiple, +.selectors { + font-weight: 700; +} +.group:hover .multiple, +.group:hover .selectors { + font-weight: 400; +} +.nested { + .example { + font-weight: 700; + } + .example:hover { + font-weight: 400; + } +} +@media (min-width: 640px) { + @media (prefers-reduced-motion: no-preference) { + .group:active .crazy-example:focus { + opacity: 0.1; + } + } +} .container { width: 100%; } diff --git a/src/index.test.js b/src/index.test.js index 349e591..c64503a 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -11,6 +11,7 @@ function run(input, config = {}) { test('it works', () => { let config = { + darkMode: 'class', purge: [path.resolve(__dirname, './index.test.html')], } @@ -20,6 +21,35 @@ test('it works', () => { font-family: theme('fontFamily.sans'); color: theme('colors.blue.500'); } + .apply-test { + @apply mt-6 bg-pink-500 hover:font-bold focus:hover:font-bold sm:bg-green-500 sm:focus:even:bg-pink-200; + } + .apply-components { + @apply container mx-auto; + } + .drop-empty-rules { + @apply hover:font-bold; + } + .apply-group { + @apply group-hover:font-bold; + } + .apply-dark-mode { + @apply dark:font-bold; + } + .apply-with-existing:hover { + @apply font-normal sm:bg-green-500; + } + .multiple, .selectors { + @apply font-bold group-hover:font-normal; + } + .nested { + .example { + @apply font-bold hover:font-normal; + } + } + .crazy-example { + @apply sm:motion-safe:group-active:focus:opacity-10; + } @tailwind components; @tailwind utilities; `, From f7d6ac230165089863e293694693d0a77b266595 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 28 Feb 2021 23:27:18 +0100 Subject: [PATCH 2/2] add test to ensure advanced selectors like space-x-4 works --- src/index.test.css | 5 +++++ src/index.test.js | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/index.test.css b/src/index.test.css index 053cf43..4a27516 100644 --- a/src/index.test.css +++ b/src/index.test.css @@ -83,6 +83,11 @@ .group:hover .selectors { font-weight: 400; } +.list > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} .nested { .example { font-weight: 700; diff --git a/src/index.test.js b/src/index.test.js index c64503a..87d996e 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -42,6 +42,9 @@ test('it works', () => { .multiple, .selectors { @apply font-bold group-hover:font-normal; } + .list { + @apply space-x-4; + } .nested { .example { @apply font-bold hover:font-normal;