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

Commit 7a2e698

Browse files
committed
Refactor PostCSS plugins to separate files
1 parent 14c4ecd commit 7a2e698

File tree

8 files changed

+603
-595
lines changed

8 files changed

+603
-595
lines changed

src/index.js

Lines changed: 7 additions & 579 deletions
Large diffs are not rendered by default.

src/lib/expandApplyAtRules.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
const postcss = require('postcss')
2+
const generateRules = require('./generateRules')
3+
const { bigSign, toPostCssNode, isPlainObject } = require('./utils')
4+
const escape = require('tailwindcss/lib/util/escapeClassName').default
5+
6+
function expandApplyAtRules(context) {
7+
return (root) => {
8+
let applyCandidates = new Set()
9+
10+
// Collect all @apply rules and candidates
11+
let applies = []
12+
root.walkAtRules('apply', (rule) => {
13+
for (let util of rule.params.split(/[\s\t\n]+/g)) {
14+
applyCandidates.add(util)
15+
}
16+
applies.push(rule)
17+
})
18+
19+
// Start the @apply process if we have rules with @apply in them
20+
if (applies.length > 0) {
21+
// Fill up some caches!
22+
generateRules(context.tailwindConfig, applyCandidates, context)
23+
24+
/**
25+
* When we have an apply like this:
26+
*
27+
* .abc {
28+
* @apply hover:font-bold;
29+
* }
30+
*
31+
* What we essentially will do is resolve to this:
32+
*
33+
* .abc {
34+
* @apply .hover\:font-bold:hover {
35+
* font-weight: 500;
36+
* }
37+
* }
38+
*
39+
* Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
40+
* What happens in this function is that we prepend a `.` and escape the candidate.
41+
* This will result in `.hover\:font-bold`
42+
* Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
43+
*/
44+
// TODO: Should we use postcss-selector-parser for this instead?
45+
function replaceSelector(selector, utilitySelector, candidate) {
46+
return selector
47+
.split(/\s*,\s*/g)
48+
.map((s) => utilitySelector.replace(`.${escape(candidate)}`, s))
49+
.join(', ')
50+
}
51+
52+
function updateSelectors(rule, apply, candidate) {
53+
return rule.map(([selector, rule]) => {
54+
if (!isPlainObject(rule)) {
55+
return [selector, updateSelectors(rule, apply, candidate)]
56+
}
57+
return [replaceSelector(apply.parent.selector, selector, candidate), rule]
58+
})
59+
}
60+
61+
for (let apply of applies) {
62+
let siblings = []
63+
let applyCandidates = apply.params.split(/[\s\t\n]+/g)
64+
for (let applyCandidate of applyCandidates) {
65+
// TODO: Check for user css rules?
66+
if (!context.classCache.has(applyCandidate)) {
67+
throw new Error('Utility does not exist!')
68+
}
69+
70+
let [layerName, rules] = context.classCache.get(applyCandidate)
71+
for (let [sort, [selector, rule]] of rules) {
72+
// Nested rules...
73+
if (!isPlainObject(rule)) {
74+
siblings.push([
75+
sort,
76+
toPostCssNode(
77+
[selector, updateSelectors(rule, apply, applyCandidate)],
78+
context.postCssNodeCache
79+
),
80+
])
81+
} else {
82+
let appliedSelector = replaceSelector(apply.parent.selector, selector, applyCandidate)
83+
84+
if (appliedSelector !== apply.parent.selector) {
85+
siblings.push([
86+
sort,
87+
toPostCssNode([appliedSelector, rule], context.postCssNodeCache),
88+
])
89+
continue
90+
}
91+
92+
// Add declarations directly
93+
for (let property in rule) {
94+
apply.before(postcss.decl({ prop: property, value: rule[property] }))
95+
}
96+
}
97+
}
98+
}
99+
100+
// Inject the rules, sorted, correctly
101+
for (let [sort, sibling] of siblings.sort(([a], [z]) => bigSign(z - a))) {
102+
// `apply.parent` is refering to the node at `.abc` in: .abc { @apply mt-2 }
103+
apply.parent.after(sibling)
104+
}
105+
106+
// If there are left-over declarations, just remove the @apply
107+
if (apply.parent.nodes.length > 1) {
108+
apply.remove()
109+
} else {
110+
// The node is empty, drop the full node
111+
apply.parent.remove()
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
module.exports = expandApplyAtRules

src/lib/generateRules.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
const { toPostCssNode } = require('./utils')
2+
3+
// Generate match permutations for a class candidate, like:
4+
// ['ring-offset-blue', '100']
5+
// ['ring-offset', 'blue-100']
6+
// ['ring', 'offset-blue-100']
7+
function* candidatePermutations(prefix, modifier = '') {
8+
yield [prefix, modifier]
9+
10+
let dashIdx = prefix.lastIndexOf('-')
11+
if (dashIdx === -1) {
12+
return
13+
}
14+
15+
yield* candidatePermutations(
16+
prefix.slice(0, dashIdx),
17+
[prefix.slice(dashIdx + 1), modifier].filter(Boolean).join('-')
18+
)
19+
}
20+
21+
// Takes a list of rule tuples and applies a variant like `hover`, sm`,
22+
// whatever to it. We used to do some extra caching here to avoid generating
23+
// a variant of the same rule more than once, but this was never hit because
24+
// we cache at the entire selector level further up the tree.
25+
//
26+
// Technically you can get a cache hit if you have `hover:focus:text-center`
27+
// and `focus:hover:text-center` in the same project, but it doesn't feel
28+
// worth the complexity for that case.
29+
30+
function applyVariant(variant, matches, { variantMap }) {
31+
if (matches.length === 0) {
32+
return matches
33+
}
34+
35+
if (variantMap.has(variant)) {
36+
let [variantSort, applyThisVariant] = variantMap.get(variant)
37+
let result = []
38+
39+
for (let [sort, rule] of matches) {
40+
let [, , options = {}] = rule
41+
42+
if (options.respectVariants === false) {
43+
result.push([sort, rule])
44+
continue
45+
}
46+
47+
let ruleWithVariant = applyThisVariant(rule)
48+
49+
if (ruleWithVariant === null) {
50+
continue
51+
}
52+
53+
let withOffset = [variantSort | sort, ruleWithVariant]
54+
result.push(withOffset)
55+
}
56+
57+
return result
58+
}
59+
60+
return []
61+
}
62+
63+
function generateRules(tailwindConfig, candidates, context) {
64+
let { componentMap, utilityMap, classCache, notClassCache, postCssNodeCache } = context
65+
66+
let layers = {
67+
components: [],
68+
utilities: [],
69+
}
70+
71+
for (let candidate of candidates) {
72+
if (notClassCache.has(candidate)) {
73+
continue
74+
}
75+
76+
if (classCache.has(candidate)) {
77+
let [layer, matches] = classCache.get(candidate)
78+
layers[layer].push(matches)
79+
continue
80+
}
81+
82+
let [classCandidate, ...variants] = candidate.split(':').reverse()
83+
84+
if (componentMap.has(classCandidate)) {
85+
let matches = componentMap.get(classCandidate)
86+
87+
if (matches.length === 0) {
88+
notClassCache.add(candidate)
89+
continue
90+
}
91+
92+
for (let variant of variants) {
93+
matches = applyVariant(variant, matches, context)
94+
}
95+
96+
classCache.set(candidate, ['components', matches])
97+
layers.components.push(matches)
98+
} else {
99+
let matchedPlugins = null
100+
101+
if (utilityMap.has(classCandidate)) {
102+
matchedPlugins = [utilityMap.get(classCandidate), 'DEFAULT']
103+
} else {
104+
let candidatePrefix = classCandidate
105+
let negative = false
106+
107+
if (candidatePrefix[0] === '-') {
108+
negative = true
109+
candidatePrefix = candidatePrefix.slice(1)
110+
}
111+
112+
for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
113+
if (utilityMap.has(prefix)) {
114+
matchedPlugins = [utilityMap.get(prefix), negative ? `-${modifier}` : modifier]
115+
break
116+
}
117+
}
118+
}
119+
120+
if (matchedPlugins === null) {
121+
notClassCache.add(candidate)
122+
continue
123+
}
124+
125+
let pluginHelpers = {
126+
candidate: classCandidate,
127+
theme: tailwindConfig.theme,
128+
}
129+
130+
let matches = []
131+
let [plugins, modifier] = matchedPlugins
132+
133+
for (let [sort, plugin] of plugins) {
134+
if (Array.isArray(plugin)) {
135+
matches.push([sort, plugin])
136+
} else {
137+
for (let result of plugin(modifier, pluginHelpers)) {
138+
matches.push([sort, result])
139+
}
140+
}
141+
}
142+
143+
for (let variant of variants) {
144+
matches = applyVariant(variant, matches, context)
145+
}
146+
147+
classCache.set(candidate, ['utilities', matches])
148+
layers.utilities.push(matches)
149+
}
150+
}
151+
152+
return {
153+
components: layers.components
154+
.flat(1)
155+
.map(([sort, rule]) => [
156+
sort | context.layerOrder.components,
157+
toPostCssNode(rule, postCssNodeCache),
158+
]),
159+
utilities: layers.utilities
160+
.flat(1)
161+
.map(([sort, rule]) => [
162+
sort | context.layerOrder.utilities,
163+
toPostCssNode(rule, postCssNodeCache),
164+
]),
165+
}
166+
}
167+
168+
module.exports = generateRules

src/lib/removeLayerAtRules.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function removeLayerAtRules(context) {
2+
return (root) => {
3+
root.walkAtRules('layer', (rule) => {
4+
rule.remove()
5+
})
6+
}
7+
}
8+
9+
module.exports = removeLayerAtRules

src/lib/setupContext.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const resolveConfig = require('tailwindcss/resolveConfig')
1616

1717
const sharedState = require('./sharedState')
1818
const corePlugins = require('../corePlugins')
19+
const { isPlainObject } = require('./utils')
20+
const { isBuffer } = require('util')
1921

2022
let contextMap = sharedState.contextMap
2123
let env = sharedState.env
@@ -51,16 +53,6 @@ function isObject(value) {
5153
return typeof value === 'object' && value !== null
5254
}
5355

54-
function isPlainObject(value) {
55-
if (Object.prototype.toString.call(value) !== '[object Object]') {
56-
return false
57-
}
58-
59-
const prototype = Object.getPrototypeOf(value)
60-
return prototype === null || prototype === Object.prototype
61-
// return isObject(value) && !Array.isArray(value)
62-
}
63-
6456
function isEmpty(obj) {
6557
return Object.keys(obj).length === 0
6658
}

src/lib/sharedState.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
let env = {
2-
TAILWIND_MODE: process.env.TAILWIND_MODE,
3-
NODE_ENV: process.env.NODE_ENV,
4-
DEBUG: process.env.DEBUG !== undefined,
5-
}
1+
const LRU = require('quick-lru')
62

73
module.exports = {
8-
env,
4+
env: {
5+
TAILWIND_MODE: process.env.TAILWIND_MODE,
6+
NODE_ENV: process.env.NODE_ENV,
7+
DEBUG: process.env.DEBUG !== undefined,
8+
},
99
contextMap: new Map(),
10+
contentMatchCache: new LRU({ maxSize: 25000 }),
1011
}

0 commit comments

Comments
 (0)