source: frontend/node_modules/tailwindcss/src/lib/expandApplyAtRules.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 19.0 KB
Line 
1import postcss from 'postcss'
2import parser from 'postcss-selector-parser'
3
4import { resolveMatches } from './generateRules'
5import escapeClassName from '../util/escapeClassName'
6import { applyImportantSelector } from '../util/applyImportantSelector'
7import { movePseudos } from '../util/pseudoElements'
8
9/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
10
11function extractClasses(node) {
12 /** @type {Map<string, Set<string>>} */
13 let groups = new Map()
14
15 let container = postcss.root({ nodes: [node.clone()] })
16
17 container.walkRules((rule) => {
18 parser((selectors) => {
19 selectors.walkClasses((classSelector) => {
20 let parentSelector = classSelector.parent.toString()
21
22 let classes = groups.get(parentSelector)
23 if (!classes) {
24 groups.set(parentSelector, (classes = new Set()))
25 }
26
27 classes.add(classSelector.value)
28 })
29 }).processSync(rule.selector)
30 })
31
32 let normalizedGroups = Array.from(groups.values(), (classes) => Array.from(classes))
33 let classes = normalizedGroups.flat()
34
35 return Object.assign(classes, { groups: normalizedGroups })
36}
37
38let selectorExtractor = parser()
39
40/**
41 * @param {string} ruleSelectors
42 */
43function extractSelectors(ruleSelectors) {
44 return selectorExtractor.astSync(ruleSelectors)
45}
46
47function extractBaseCandidates(candidates, separator) {
48 let baseClasses = new Set()
49
50 for (let candidate of candidates) {
51 baseClasses.add(candidate.split(separator).pop())
52 }
53
54 return Array.from(baseClasses)
55}
56
57function prefix(context, selector) {
58 let prefix = context.tailwindConfig.prefix
59 return typeof prefix === 'function' ? prefix(selector) : prefix + selector
60}
61
62function* pathToRoot(node) {
63 yield node
64 while (node.parent) {
65 yield node.parent
66 node = node.parent
67 }
68}
69
70/**
71 * Only clone the node itself and not its children
72 *
73 * @param {*} node
74 * @param {*} overrides
75 * @returns
76 */
77function shallowClone(node, overrides = {}) {
78 let children = node.nodes
79 node.nodes = []
80
81 let tmp = node.clone(overrides)
82
83 node.nodes = children
84
85 return tmp
86}
87
88/**
89 * Clone just the nodes all the way to the top that are required to represent
90 * this singular rule in the tree.
91 *
92 * For example, if we have CSS like this:
93 * ```css
94 * @media (min-width: 768px) {
95 * @supports (display: grid) {
96 * .foo {
97 * display: grid;
98 * grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
99 * }
100 * }
101 *
102 * @supports (backdrop-filter: blur(1px)) {
103 * .bar {
104 * backdrop-filter: blur(1px);
105 * }
106 * }
107 *
108 * .baz {
109 * color: orange;
110 * }
111 * }
112 * ```
113 *
114 * And we're cloning `.bar` it'll return a cloned version of what's required for just that single node:
115 *
116 * ```css
117 * @media (min-width: 768px) {
118 * @supports (backdrop-filter: blur(1px)) {
119 * .bar {
120 * backdrop-filter: blur(1px);
121 * }
122 * }
123 * }
124 * ```
125 *
126 * @param {import('postcss').Node} node
127 */
128function nestedClone(node) {
129 for (let parent of pathToRoot(node)) {
130 if (node === parent) {
131 continue
132 }
133
134 if (parent.type === 'root') {
135 break
136 }
137
138 node = shallowClone(parent, {
139 nodes: [node],
140 })
141 }
142
143 return node
144}
145
146/**
147 * @param {import('postcss').Root} root
148 */
149function buildLocalApplyCache(root, context) {
150 /** @type {ApplyCache} */
151 let cache = new Map()
152
153 root.walkRules((rule) => {
154 // Ignore rules generated by Tailwind
155 for (let node of pathToRoot(rule)) {
156 if (node.raws.tailwind?.layer !== undefined) {
157 return
158 }
159 }
160
161 // Clone what's required to represent this singular rule in the tree
162 let container = nestedClone(rule)
163 let sort = context.offsets.create('user')
164
165 for (let className of extractClasses(rule)) {
166 let list = cache.get(className) || []
167 cache.set(className, list)
168
169 list.push([
170 {
171 layer: 'user',
172 sort,
173 important: false,
174 },
175 container,
176 ])
177 }
178 })
179
180 return cache
181}
182
183/**
184 * @returns {ApplyCache}
185 */
186function buildApplyCache(applyCandidates, context) {
187 for (let candidate of applyCandidates) {
188 if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) {
189 continue
190 }
191
192 if (context.classCache.has(candidate)) {
193 context.applyClassCache.set(
194 candidate,
195 context.classCache.get(candidate).map(([meta, rule]) => [meta, rule.clone()])
196 )
197 continue
198 }
199
200 let matches = Array.from(resolveMatches(candidate, context))
201
202 if (matches.length === 0) {
203 context.notClassCache.add(candidate)
204 continue
205 }
206
207 context.applyClassCache.set(candidate, matches)
208 }
209
210 return context.applyClassCache
211}
212
213/**
214 * Build a cache only when it's first used
215 *
216 * @param {() => ApplyCache} buildCacheFn
217 * @returns {ApplyCache}
218 */
219function lazyCache(buildCacheFn) {
220 let cache = null
221
222 return {
223 get: (name) => {
224 cache = cache || buildCacheFn()
225
226 return cache.get(name)
227 },
228 has: (name) => {
229 cache = cache || buildCacheFn()
230
231 return cache.has(name)
232 },
233 }
234}
235
236/**
237 * Take a series of multiple caches and merge
238 * them so they act like one large cache
239 *
240 * @param {ApplyCache[]} caches
241 * @returns {ApplyCache}
242 */
243function combineCaches(caches) {
244 return {
245 get: (name) => caches.flatMap((cache) => cache.get(name) || []),
246 has: (name) => caches.some((cache) => cache.has(name)),
247 }
248}
249
250function extractApplyCandidates(params) {
251 let candidates = params.split(/[\s\t\n]+/g)
252
253 if (candidates[candidates.length - 1] === '!important') {
254 return [candidates.slice(0, -1), true]
255 }
256
257 return [candidates, false]
258}
259
260function processApply(root, context, localCache) {
261 let applyCandidates = new Set()
262
263 // Collect all @apply rules and candidates
264 let applies = []
265 root.walkAtRules('apply', (rule) => {
266 let [candidates] = extractApplyCandidates(rule.params)
267
268 for (let util of candidates) {
269 applyCandidates.add(util)
270 }
271
272 applies.push(rule)
273 })
274
275 // Start the @apply process if we have rules with @apply in them
276 if (applies.length === 0) {
277 return
278 }
279
280 // Fill up some caches!
281 let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
282
283 /**
284 * When we have an apply like this:
285 *
286 * .abc {
287 * @apply hover:font-bold;
288 * }
289 *
290 * What we essentially will do is resolve to this:
291 *
292 * .abc {
293 * @apply .hover\:font-bold:hover {
294 * font-weight: 500;
295 * }
296 * }
297 *
298 * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
299 * What happens in this function is that we prepend a `.` and escape the candidate.
300 * This will result in `.hover\:font-bold`
301 * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
302 *
303 * @param {string} selector
304 * @param {string} utilitySelectors
305 * @param {string} candidate
306 */
307 function replaceSelector(selector, utilitySelectors, candidate) {
308 let selectorList = extractSelectors(selector)
309 let utilitySelectorsList = extractSelectors(utilitySelectors)
310 let candidateList = extractSelectors(`.${escapeClassName(candidate)}`)
311 let candidateClass = candidateList.nodes[0].nodes[0]
312
313 selectorList.each((sel) => {
314 /** @type {Set<import('postcss-selector-parser').Selector>} */
315 let replaced = new Set()
316
317 utilitySelectorsList.each((utilitySelector) => {
318 let hasReplaced = false
319 utilitySelector = utilitySelector.clone()
320
321 utilitySelector.walkClasses((node) => {
322 if (node.value !== candidateClass.value) {
323 return
324 }
325
326 // Don't replace multiple instances of the same class
327 // This is theoretically correct but only partially
328 // We'd need to generate every possible permutation of the replacement
329 // For example with `.foo + .foo { … }` and `section { @apply foo; }`
330 // We'd need to generate all of these:
331 // - `.foo + .foo`
332 // - `.foo + section`
333 // - `section + .foo`
334 // - `section + section`
335 if (hasReplaced) {
336 return
337 }
338
339 // Since you can only `@apply` class names this is sufficient
340 // We want to replace the matched class name with the selector the user is using
341 // Ex: Replace `.text-blue-500` with `.foo.bar:is(.something-cool)`
342 node.replaceWith(...sel.nodes.map((node) => node.clone()))
343
344 // Record that we did something and we want to use this new selector
345 replaced.add(utilitySelector)
346
347 hasReplaced = true
348 })
349 })
350
351 // Sort tag names before class names (but only sort each group (separated by a combinator)
352 // separately and not in total)
353 // This happens when replacing `.bar` in `.foo.bar` with a tag like `section`
354 for (let sel of replaced) {
355 let groups = [[]]
356 for (let node of sel.nodes) {
357 if (node.type === 'combinator') {
358 groups.push(node)
359 groups.push([])
360 } else {
361 let last = groups[groups.length - 1]
362 last.push(node)
363 }
364 }
365
366 sel.nodes = []
367
368 for (let group of groups) {
369 if (Array.isArray(group)) {
370 group.sort((a, b) => {
371 if (a.type === 'tag' && b.type === 'class') {
372 return -1
373 } else if (a.type === 'class' && b.type === 'tag') {
374 return 1
375 } else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) {
376 return -1
377 } else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') {
378 return 1
379 }
380
381 return 0
382 })
383 }
384
385 sel.nodes = sel.nodes.concat(group)
386 }
387 }
388
389 sel.replaceWith(...replaced)
390 })
391
392 return selectorList.toString()
393 }
394
395 let perParentApplies = new Map()
396
397 // Collect all apply candidates and their rules
398 for (let apply of applies) {
399 let [candidates] = perParentApplies.get(apply.parent) || [[], apply.source]
400
401 perParentApplies.set(apply.parent, [candidates, apply.source])
402
403 let [applyCandidates, important] = extractApplyCandidates(apply.params)
404
405 if (apply.parent.type === 'atrule') {
406 if (apply.parent.name === 'screen') {
407 let screenType = apply.parent.params
408
409 throw apply.error(
410 `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates
411 .map((c) => `${screenType}:${c}`)
412 .join(' ')} instead.`
413 )
414 }
415
416 throw apply.error(
417 `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`
418 )
419 }
420
421 for (let applyCandidate of applyCandidates) {
422 if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
423 // TODO: Link to specific documentation page with error code.
424 throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
425 }
426
427 if (!applyClassCache.has(applyCandidate)) {
428 throw apply.error(
429 `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
430 )
431 }
432
433 let rules = applyClassCache.get(applyCandidate)
434
435 // Verify that we can apply the class
436 for (let [, rule] of rules) {
437 if (rule.type === 'atrule') {
438 continue
439 }
440
441 rule.walkRules(() => {
442 throw apply.error(
443 [
444 `The \`${applyCandidate}\` class cannot be used with \`@apply\` because \`@apply\` does not currently support nested CSS.`,
445 'Rewrite the selector without nesting or configure the `tailwindcss/nesting` plugin:',
446 'https://tailwindcss.com/docs/using-with-preprocessors#nesting',
447 ].join('\n')
448 )
449 })
450 }
451
452 candidates.push([applyCandidate, important, rules])
453 }
454 }
455
456 for (let [parent, [candidates, atApplySource]] of perParentApplies) {
457 let siblings = []
458
459 for (let [applyCandidate, important, rules] of candidates) {
460 let potentialApplyCandidates = [
461 applyCandidate,
462 ...extractBaseCandidates([applyCandidate], context.tailwindConfig.separator),
463 ]
464
465 for (let [meta, node] of rules) {
466 let parentClasses = extractClasses(parent)
467 let nodeClasses = extractClasses(node)
468
469 // When we encounter a rule like `.dark .a, .b { … }` we only want to be left with `[.dark, .a]` if the base applyCandidate is `.a` or with `[.b]` if the base applyCandidate is `.b`
470 // So we've split them into groups
471 nodeClasses = nodeClasses.groups
472 .filter((classList) =>
473 classList.some((className) => potentialApplyCandidates.includes(className))
474 )
475 .flat()
476
477 // Add base utility classes from the @apply node to the list of
478 // classes to check whether it intersects and therefore results in a
479 // circular dependency or not.
480 //
481 // E.g.:
482 // .foo {
483 // @apply hover:a; // This applies "a" but with a modifier
484 // }
485 //
486 // We only have to do that with base classes of the `node`, not of the `parent`
487 // E.g.:
488 // .hover\:foo {
489 // @apply bar;
490 // }
491 // .bar {
492 // @apply foo;
493 // }
494 //
495 // This should not result in a circular dependency because we are
496 // just applying `.foo` and the rule above is `.hover\:foo` which is
497 // unrelated. However, if we were to apply `hover:foo` then we _did_
498 // have to include this one.
499 nodeClasses = nodeClasses.concat(
500 extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)
501 )
502
503 let intersects = parentClasses.some((selector) => nodeClasses.includes(selector))
504 if (intersects) {
505 throw node.error(
506 `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`
507 )
508 }
509
510 let root = postcss.root({ nodes: [node.clone()] })
511
512 // Make sure every node in the entire tree points back at the @apply rule that generated it
513 root.walk((node) => {
514 node.source = atApplySource
515 })
516
517 let canRewriteSelector =
518 node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
519
520 if (canRewriteSelector) {
521 root.walkRules((rule) => {
522 // Let's imagine you have the following structure:
523 //
524 // .foo {
525 // @apply bar;
526 // }
527 //
528 // @supports (a: b) {
529 // .bar {
530 // color: blue
531 // }
532 //
533 // .something-unrelated {}
534 // }
535 //
536 // In this case we want to apply `.bar` but it happens to be in
537 // an atrule node. We clone that node instead of the nested one
538 // because we still want that @supports rule to be there once we
539 // applied everything.
540 //
541 // However it happens to be that the `.something-unrelated` is
542 // also in that same shared @supports atrule. This is not good,
543 // and this should not be there. The good part is that this is
544 // a clone already and it can be safely removed. The question is
545 // how do we know we can remove it. Basically what we can do is
546 // match it against the applyCandidate that you want to apply. If
547 // it doesn't match the we can safely delete it.
548 //
549 // If we didn't do this, then the `replaceSelector` function
550 // would have replaced this with something that didn't exist and
551 // therefore it removed the selector altogether. In this specific
552 // case it would result in `{}` instead of `.something-unrelated {}`
553 if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) {
554 rule.remove()
555 return
556 }
557
558 // Strip the important selector from the parent selector if at the beginning
559 let importantSelector =
560 typeof context.tailwindConfig.important === 'string'
561 ? context.tailwindConfig.important
562 : null
563
564 // We only want to move the "important" selector if this is a Tailwind-generated utility
565 // We do *not* want to do this for user CSS that happens to be structured the same
566 let isGenerated = parent.raws.tailwind !== undefined
567
568 let parentSelector =
569 isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0
570 ? parent.selector.slice(importantSelector.length)
571 : parent.selector
572
573 // If the selector becomes empty after replacing the important selector
574 // This means that it's the same as the parent selector and we don't want to replace it
575 // Otherwise we'll crash
576 if (parentSelector === '') {
577 parentSelector = parent.selector
578 }
579
580 rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate)
581
582 // And then re-add it if it was removed
583 if (importantSelector && parentSelector !== parent.selector) {
584 rule.selector = applyImportantSelector(rule.selector, importantSelector)
585 }
586
587 rule.walkDecls((d) => {
588 d.important = meta.important || important
589 })
590
591 // Move pseudo elements to the end of the selector (if necessary)
592 let selector = parser().astSync(rule.selector)
593 selector.each((sel) => movePseudos(sel))
594 rule.selector = selector.toString()
595 })
596 }
597
598 // It could be that the node we were inserted was removed because the class didn't match
599 // If that was the *only* rule in the parent, then we have nothing add so we skip it
600 if (!root.nodes[0]) {
601 continue
602 }
603
604 // Insert it
605 siblings.push([meta.sort, root.nodes[0]])
606 }
607 }
608
609 // Inject the rules, sorted, correctly
610 let nodes = context.offsets.sort(siblings).map((s) => s[1])
611
612 // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
613 parent.after(nodes)
614 }
615
616 for (let apply of applies) {
617 // If there are left-over declarations, just remove the @apply
618 if (apply.parent.nodes.length > 1) {
619 apply.remove()
620 } else {
621 // The node is empty, drop the full node
622 apply.parent.remove()
623 }
624 }
625
626 // Do it again, in case we have other `@apply` rules
627 processApply(root, context, localCache)
628}
629
630export default function expandApplyAtRules(context) {
631 return (root) => {
632 // Build a cache of the user's CSS so we can use it to resolve classes used by @apply
633 let localCache = lazyCache(() => buildLocalApplyCache(root, context))
634
635 processApply(root, context, localCache)
636 }
637}
Note: See TracBrowser for help on using the repository browser.