-
Notifications
You must be signed in to change notification settings - Fork 4k
/
index.ts
315 lines (265 loc) · 10.7 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import { version } from '../package.json'
import { WalkAction, comment, decl, rule, toCss, walk, type AstNode, type Rule } from './ast'
import { compileCandidates } from './compile'
import * as CSS from './css-parser'
import { buildDesignSystem } from './design-system'
import { Theme } from './theme'
import { isSimpleClassSelector } from './utils/is-simple-class-selector'
export function compile(css: string): {
build(candidates: string[]): string
} {
let ast = CSS.parse(css)
if (process.env.NODE_ENV !== 'test') {
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
}
// Track all invalid candidates
let invalidCandidates = new Set<string>()
function onInvalidCandidate(candidate: string) {
invalidCandidates.add(candidate)
}
// Track `@apply` information
let containsAtApply = css.includes('@apply')
let userDefinedApplyables = new Map<string, AstNode[]>()
// Find all `@theme` declarations
let theme = new Theme()
let firstThemeRule: Rule | null = null
let keyframesRules: Rule[] = []
walk(ast, (node, { replaceWith }) => {
if (node.kind !== 'rule') return
// Track all user-defined classes for `@apply` support
if (
containsAtApply &&
// Verify that it is a valid applyable-class. An applyable class is a
// class that is a very simple selector, like `.foo` or `.bar`, but doesn't
// contain any spaces, combinators, pseudo-selectors, pseudo-elements, or
// attribute selectors.
node.selector[0] === '.' &&
isSimpleClassSelector(node.selector)
) {
// Convert the class `.foo` into a candidate `foo`
let candidate = node.selector.slice(1)
// It could be that multiple definitions exist for the same class, so we
// need to track all of them.
let nodes = userDefinedApplyables.get(candidate) ?? []
// Add all children of the current rule to the list of nodes for the
// current candidate.
for (let child of node.nodes) {
nodes.push(child)
}
// Store the list of nodes for the current candidate
userDefinedApplyables.set(candidate, nodes)
}
// Drop instances of `@media reference`
//
// We support `@import "tailwindcss/theme" reference` as a way to import an external theme file
// as a reference, which becomes `@media reference { … }` when the `@import` is processed.
if (node.selector === '@media reference') {
walk(node.nodes, (child) => {
if (child.kind !== 'rule') {
throw new Error(
'Files imported with `@import "…" reference` must only contain `@theme` blocks.',
)
}
if (child.selector === '@theme') {
child.selector = '@theme reference'
return WalkAction.Skip
}
})
replaceWith(node.nodes)
}
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
let isReference = node.selector === '@theme reference'
// Record all custom properties in the `@theme` declaration
walk(node.nodes, (child, { replaceWith }) => {
// Collect `@keyframes` rules to re-insert with theme variables later,
// since the `@theme` rule itself will be removed.
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
keyframesRules.push(child)
replaceWith([])
return WalkAction.Skip
}
if (child.kind === 'comment') return
if (child.kind === 'declaration' && child.property.startsWith('--')) {
theme.add(child.property, child.value, isReference)
return
}
let snippet = toCss([rule(node.selector, [child])])
.split('\n')
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
.join('\n')
throw new Error(
`\`@theme\` blocks must only contain custom properties or \`@keyframes\`.\n\n${snippet}`,
)
})
// Keep a reference to the first `@theme` rule to update with the full theme
// later, and delete any other `@theme` rules.
if (!firstThemeRule && !isReference) {
firstThemeRule = node
} else {
replaceWith([])
}
return WalkAction.Skip
})
// Output final set of theme variables at the position of the first `@theme`
// rule.
if (firstThemeRule) {
firstThemeRule = firstThemeRule as Rule
firstThemeRule.selector = ':root'
let nodes = []
for (let [key, value] of theme.entries()) {
if (value.isReference) continue
nodes.push(decl(key, value.value))
}
if (keyframesRules.length > 0) {
let animationParts = [...theme.namespace('--animate').values()].flatMap((animation) =>
animation.split(' '),
)
for (let keyframesRule of keyframesRules) {
// Remove any keyframes that aren't used by an animation variable.
let keyframesName = keyframesRule.selector.slice(11) // `@keyframes `.length
if (!animationParts.includes(keyframesName)) {
continue
}
// Wrap `@keyframes` in `@at-root` so they are hoisted out of `:root`
// when printing.
nodes.push(
Object.assign(keyframesRule, {
selector: '@at-root',
nodes: [rule(keyframesRule.selector, keyframesRule.nodes)],
}),
)
}
}
firstThemeRule.nodes = nodes
}
let designSystem = buildDesignSystem(theme)
let tailwindUtilitiesNode: Rule | null = null
// Find `@tailwind utilities` and replace it with the actual generated utility
// class CSS.
walk(ast, (node) => {
if (node.kind === 'rule' && node.selector === '@tailwind utilities') {
tailwindUtilitiesNode = node
// Stop walking after finding `@tailwind utilities` to avoid walking all
// of the generated CSS. This means `@tailwind utilities` can only appear
// once per file but that's the intended usage at this point in time.
return WalkAction.Stop
}
})
// Replace `@apply` rules with the actual utility classes.
if (containsAtApply) {
walk(ast, (node, { replaceWith }) => {
if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@apply')) {
let candidates = node.selector
.slice(7 /* Ignore `@apply ` when parsing the selector */)
.trim()
.split(/\s+/g)
// Replace the `@apply` rule with the actual utility classes
{
let newNodes: AstNode[] = []
// Collect all user-defined classes for the current candidates that we
// need to apply.
for (let candidate of candidates) {
let nodes = userDefinedApplyables.get(candidate)
if (!nodes) continue
for (let child of nodes) {
newNodes.push(structuredClone(child))
}
}
// Parse the candidates to an AST that we can replace the `@apply` rule with.
let candidateAst = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
// When a candidate is invalid, we want to first verify that the
// candidate is a user-defined class or not. If it is, then we can
// safely ignore this. If it's not, then we throw an error because
// the candidate is unknown.
//
// The reason we even have to check user-defined classes is
// because it could be that the user defined CSS like that is also
// a known utility class. For example, the following CSS would be:
//
// ```css
// .flex {
// --display-mode: flex;
// }
// ```
//
// When the user then uses `@apply flex`, we want to both apply
// the user-defined class and the utility class.
if (userDefinedApplyables.has(candidate)) return
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
}).astNodes
// Collect the nodes to insert in place of the `@apply` rule. When a
// rule was used, we want to insert its children instead of the rule
// because we don't want the wrapping selector.
for (let candidateNode of candidateAst) {
if (candidateNode.kind === 'rule' && candidateNode.selector[0] !== '@') {
for (let child of candidateNode.nodes) {
newNodes.push(child)
}
} else {
newNodes.push(candidateNode)
}
}
replaceWith(newNodes)
}
}
})
}
// Track all valid candidates, these are the incoming `rawCandidate` that
// resulted in a generated AST Node. All the other `rawCandidates` are invalid
// and should be ignored.
let allValidCandidates = new Set<string>()
let compiledCss = toCss(ast)
let previousAstNodeCount = 0
return {
build(newRawCandidates: string[]) {
let didChange = false
// Add all new candidates unless we know that they are invalid.
let prevSize = allValidCandidates.size
for (let candidate of newRawCandidates) {
if (!invalidCandidates.has(candidate)) {
allValidCandidates.add(candidate)
didChange ||= allValidCandidates.size !== prevSize
}
}
// If no new candidates were added, we can return the original CSS. This
// currently assumes that we only add new candidates and never remove any.
if (!didChange) {
return compiledCss
}
if (tailwindUtilitiesNode) {
let newNodes = compileCandidates(allValidCandidates, designSystem, {
onInvalidCandidate,
}).astNodes
// If no new ast nodes were generated, then we can return the original
// CSS. This currently assumes that we only add new ast nodes and never
// remove any.
if (previousAstNodeCount === newNodes.length) {
return compiledCss
}
previousAstNodeCount = newNodes.length
tailwindUtilitiesNode.nodes = newNodes
compiledCss = toCss(ast)
}
return compiledCss
},
}
}
export function __unstable__loadDesignSystem(css: string) {
// Find all `@theme` declarations
let theme = new Theme()
let ast = CSS.parse(css)
walk(ast, (node) => {
if (node.kind !== 'rule') return
if (node.selector !== '@theme' && node.selector !== '@theme reference') return
let isReference = node.selector === '@theme reference'
// Record all custom properties in the `@theme` declaration
walk([node], (node) => {
if (node.kind !== 'declaration') return
if (!node.property.startsWith('--')) return
theme.add(node.property, node.value, isReference)
})
})
return buildDesignSystem(theme)
}