Skip to content
This repository was archived by the owner on Apr 6, 2021. It is now read-only.

Implement @apply first pass #2

Merged
merged 2 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
99 changes: 99 additions & 0 deletions src/index.test.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,105 @@
'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;
}
.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;
}
.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%;
}
Expand Down
33 changes: 33 additions & 0 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function run(input, config = {}) {

test('it works', () => {
let config = {
darkMode: 'class',
purge: [path.resolve(__dirname, './index.test.html')],
}

Expand All @@ -20,6 +21,38 @@ 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;
}
.list {
@apply space-x-4;
}
.nested {
.example {
@apply font-bold hover:font-normal;
}
}
.crazy-example {
@apply sm:motion-safe:group-active:focus:opacity-10;
}
@tailwind components;
@tailwind utilities;
`,
Expand Down