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

Commit c5bdcf4

Browse files
committed
implement @apply first pass
1 parent c5ce40c commit c5bdcf4

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

src/index.js

+122
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,10 @@ function isObject(value) {
484484
return typeof value === 'object' && value !== null
485485
}
486486

487+
function isPlainObject(value) {
488+
return isObject(value) && !Array.isArray(value)
489+
}
490+
487491
function isEmpty(obj) {
488492
return Object.keys(obj).length === 0
489493
}
@@ -687,6 +691,8 @@ module.exports = (pluginOptions = {}) => {
687691
return postcss([
688692
// substituteTailwindAtRules
689693
function (root) {
694+
let applyCandidates = new Set()
695+
690696
// Make sure this file contains Tailwind directives. If not, we can save
691697
// a lot of work and bail early. Also we don't have to register our touch
692698
// file as a dependency since the output of this CSS does not depend on
@@ -719,6 +725,15 @@ module.exports = (pluginOptions = {}) => {
719725
}
720726
})
721727

728+
// Collect all @apply rules and candidates
729+
let applies = []
730+
root.walkAtRules('apply', (rule) => {
731+
for (let util of rule.params.split(/[\s\t\n]+/g)) {
732+
applyCandidates.add(util)
733+
}
734+
applies.push(rule)
735+
})
736+
722737
if (!foundTailwind) {
723738
return root
724739
}
@@ -767,6 +782,113 @@ module.exports = (pluginOptions = {}) => {
767782
candidates,
768783
context
769784
)
785+
786+
// Start the @apply process if we have rules with @apply in them
787+
if (applies.length > 0) {
788+
// Fill up some caches!
789+
generateRules(context.tailwindConfig, applyCandidates, context)
790+
791+
/**
792+
* When we have an apply like this:
793+
*
794+
* .abc {
795+
* @apply hover:font-bold;
796+
* }
797+
*
798+
* What we essentially will do is resolve to this:
799+
*
800+
* .abc {
801+
* @apply .hover\:font-bold:hover {
802+
* font-weight: 500;
803+
* }
804+
* }
805+
*
806+
* Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
807+
* What happens in this function is that we prepend a `.` and escape the candidate.
808+
* This will result in `.hover\:font-bold`
809+
* Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
810+
*/
811+
// TODO: Should we use postcss-selector-parser for this instead?
812+
function replaceSelector(selector, utilitySelector, candidate) {
813+
return selector
814+
.split(/\s*,\s*/g)
815+
.map((s) => utilitySelector.replace(`.${escape(candidate)}`, s))
816+
.join(', ')
817+
}
818+
819+
function updateSelectors(rule, apply, candidate) {
820+
return rule.map(([selector, rule]) => {
821+
if (!isPlainObject(rule)) {
822+
return [selector, updateSelectors(rule, apply, candidate)]
823+
}
824+
return [replaceSelector(apply.parent.selector, selector, candidate), rule]
825+
})
826+
}
827+
828+
for (let apply of applies) {
829+
let siblings = []
830+
let applyCandidates = apply.params.split(/[\s\t\n]+/g)
831+
for (let applyCandidate of applyCandidates) {
832+
// TODO: Check for user css rules?
833+
if (!context.classCache.has(applyCandidate)) {
834+
throw new Error('Utility does not exist!')
835+
}
836+
837+
let [layerName, rules] = context.classCache.get(applyCandidate)
838+
for (let [sort, [selector, rule]] of rules) {
839+
// Nested rules...
840+
if (!isPlainObject(rule)) {
841+
siblings.push([
842+
sort,
843+
toPostCssNode(
844+
[selector, updateSelectors(rule, apply, applyCandidate)],
845+
context.postCssNodeCache
846+
),
847+
])
848+
} else {
849+
let appliedSelector = replaceSelector(
850+
apply.parent.selector,
851+
selector,
852+
applyCandidate
853+
)
854+
855+
if (appliedSelector !== apply.parent.selector) {
856+
siblings.push([
857+
sort,
858+
toPostCssNode(
859+
[
860+
replaceSelector(apply.parent.selector, selector, applyCandidate),
861+
rule,
862+
],
863+
context.postCssNodeCache
864+
),
865+
])
866+
continue
867+
}
868+
869+
// Add declarations directly
870+
for (let property in rule) {
871+
apply.before(postcss.decl({ prop: property, value: rule[property] }))
872+
}
873+
}
874+
}
875+
}
876+
877+
// Inject the rules, sorted, correctly
878+
for (let [sort, sibling] of siblings.sort(([a], [z]) => Math.sign(Number(z - a)))) {
879+
// `apply.parent` is refering to the node at `.abc` in: .abc { @apply mt-2 }
880+
apply.parent.after(sibling)
881+
}
882+
883+
// If there are left-over declarations, just remove the @apply
884+
if (apply.parent.nodes.length > 1) {
885+
apply.remove()
886+
} else {
887+
// The node is empty, drop the full node
888+
apply.parent.remove()
889+
}
890+
}
891+
}
770892
env.DEBUG && console.timeEnd('Generate rules')
771893

772894
// We only ever add to the classCache, so if it didn't grow, there is nothing new.

src/index.test.css

+94
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,100 @@
44
'Segoe UI Symbol', 'Noto Color Emoji';
55
color: #3b82f6;
66
}
7+
.apply-test {
8+
margin-top: 1.5rem;
9+
--tw-bg-opacity: 1;
10+
background-color: rgba(236, 72, 153, var(--tw-bg-opacity));
11+
}
12+
.apply-test:hover {
13+
font-weight: 700;
14+
}
15+
.apply-test:focus:hover {
16+
font-weight: 700;
17+
}
18+
@media (min-width: 640px) {
19+
.apply-test {
20+
--tw-bg-opacity: 1;
21+
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
22+
}
23+
}
24+
@media (min-width: 640px) {
25+
.apply-test:focus:nth-child(even) {
26+
--tw-bg-opacity: 1;
27+
background-color: rgba(251, 207, 232, var(--tw-bg-opacity));
28+
}
29+
}
30+
.apply-components {
31+
width: 100%;
32+
margin-left: auto;
33+
margin-right: auto;
34+
}
35+
@media (min-width: 1536px) {
36+
.apply-components {
37+
max-width: 1536px;
38+
}
39+
}
40+
@media (min-width: 1280px) {
41+
.apply-components {
42+
max-width: 1280px;
43+
}
44+
}
45+
@media (min-width: 1024px) {
46+
.apply-components {
47+
max-width: 1024px;
48+
}
49+
}
50+
@media (min-width: 768px) {
51+
.apply-components {
52+
max-width: 768px;
53+
}
54+
}
55+
@media (min-width: 640px) {
56+
.apply-components {
57+
max-width: 640px;
58+
}
59+
}
60+
.drop-empty-rules:hover {
61+
font-weight: 700;
62+
}
63+
.group:hover .apply-group {
64+
font-weight: 700;
65+
}
66+
.dark .apply-dark-mode {
67+
font-weight: 700;
68+
}
69+
.apply-with-existing:hover {
70+
font-weight: 400;
71+
}
72+
@media (min-width: 640px) {
73+
.apply-with-existing:hover {
74+
--tw-bg-opacity: 1;
75+
background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
76+
}
77+
}
78+
.multiple,
79+
.selectors {
80+
font-weight: 700;
81+
}
82+
.group:hover .multiple,
83+
.group:hover .selectors {
84+
font-weight: 400;
85+
}
86+
.nested {
87+
.example {
88+
font-weight: 700;
89+
}
90+
.example:hover {
91+
font-weight: 400;
92+
}
93+
}
94+
@media (min-width: 640px) {
95+
@media (prefers-reduced-motion: no-preference) {
96+
.group:active .crazy-example:focus {
97+
opacity: 0.1;
98+
}
99+
}
100+
}
7101
.container {
8102
width: 100%;
9103
}

src/index.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ function run(input, config = {}) {
1111

1212
test('it works', () => {
1313
let config = {
14+
darkMode: 'class',
1415
purge: [path.resolve(__dirname, './index.test.html')],
1516
}
1617

@@ -20,6 +21,35 @@ test('it works', () => {
2021
font-family: theme('fontFamily.sans');
2122
color: theme('colors.blue.500');
2223
}
24+
.apply-test {
25+
@apply mt-6 bg-pink-500 hover:font-bold focus:hover:font-bold sm:bg-green-500 sm:focus:even:bg-pink-200;
26+
}
27+
.apply-components {
28+
@apply container mx-auto;
29+
}
30+
.drop-empty-rules {
31+
@apply hover:font-bold;
32+
}
33+
.apply-group {
34+
@apply group-hover:font-bold;
35+
}
36+
.apply-dark-mode {
37+
@apply dark:font-bold;
38+
}
39+
.apply-with-existing:hover {
40+
@apply font-normal sm:bg-green-500;
41+
}
42+
.multiple, .selectors {
43+
@apply font-bold group-hover:font-normal;
44+
}
45+
.nested {
46+
.example {
47+
@apply font-bold hover:font-normal;
48+
}
49+
}
50+
.crazy-example {
51+
@apply sm:motion-safe:group-active:focus:opacity-10;
52+
}
2353
@tailwind components;
2454
@tailwind utilities;
2555
`,

0 commit comments

Comments
 (0)