source: frontend/node_modules/tailwindcss/src/lib/expandTailwindAtRules.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: 8.0 KB
RevLine 
[9af201e]1import fs from 'fs'
2import LRU from '@alloc/quick-lru'
3import * as sharedState from './sharedState'
4import { generateRules } from './generateRules'
5import log from '../util/log'
6import cloneNodes from '../util/cloneNodes'
7import { defaultExtractor } from './defaultExtractor'
8
9let env = sharedState.env
10
11const builtInExtractors = {
12 DEFAULT: defaultExtractor,
13}
14
15const builtInTransformers = {
16 DEFAULT: (content) => content,
17 svelte: (content) => content.replace(/(?:^|\s)class:/g, ' '),
18}
19
20function getExtractor(context, fileExtension) {
21 let extractors = context.tailwindConfig.content.extract
22
23 return (
24 extractors[fileExtension] ||
25 extractors.DEFAULT ||
26 builtInExtractors[fileExtension] ||
27 builtInExtractors.DEFAULT(context)
28 )
29}
30
31function getTransformer(tailwindConfig, fileExtension) {
32 let transformers = tailwindConfig.content.transform
33
34 return (
35 transformers[fileExtension] ||
36 transformers.DEFAULT ||
37 builtInTransformers[fileExtension] ||
38 builtInTransformers.DEFAULT
39 )
40}
41
42let extractorCache = new WeakMap()
43
44// Scans template contents for possible classes. This is a hot path on initial build but
45// not too important for subsequent builds. The faster the better though — if we can speed
46// up these regexes by 50% that could cut initial build time by like 20%.
47function getClassCandidates(content, extractor, candidates, seen) {
48 if (!extractorCache.has(extractor)) {
49 extractorCache.set(extractor, new LRU({ maxSize: 25000 }))
50 }
51
52 for (let line of content.split('\n')) {
53 line = line.trim()
54
55 if (seen.has(line)) {
56 continue
57 }
58 seen.add(line)
59
60 if (extractorCache.get(extractor).has(line)) {
61 for (let match of extractorCache.get(extractor).get(line)) {
62 candidates.add(match)
63 }
64 } else {
65 let extractorMatches = extractor(line).filter((s) => s !== '!*')
66 let lineMatchesSet = new Set(extractorMatches)
67
68 for (let match of lineMatchesSet) {
69 candidates.add(match)
70 }
71
72 extractorCache.get(extractor).set(line, lineMatchesSet)
73 }
74 }
75}
76
77/**
78 *
79 * @param {[import('./offsets.js').RuleOffset, import('postcss').Node][]} rules
80 * @param {*} context
81 */
82function buildStylesheet(rules, context) {
83 let sortedRules = context.offsets.sort(rules)
84
85 let returnValue = {
86 base: new Set(),
87 defaults: new Set(),
88 components: new Set(),
89 utilities: new Set(),
90 variants: new Set(),
91 }
92
93 for (let [sort, rule] of sortedRules) {
94 returnValue[sort.layer].add(rule)
95 }
96
97 return returnValue
98}
99
100export default function expandTailwindAtRules(context) {
101 return async (root) => {
102 let layerNodes = {
103 base: null,
104 components: null,
105 utilities: null,
106 variants: null,
107 }
108
109 root.walkAtRules((rule) => {
110 // Make sure this file contains Tailwind directives. If not, we can save
111 // a lot of work and bail early. Also we don't have to register our touch
112 // file as a dependency since the output of this CSS does not depend on
113 // the source of any templates. Think Vue <style> blocks for example.
114 if (rule.name === 'tailwind') {
115 if (Object.keys(layerNodes).includes(rule.params)) {
116 layerNodes[rule.params] = rule
117 }
118 }
119 })
120
121 if (Object.values(layerNodes).every((n) => n === null)) {
122 return root
123 }
124
125 // ---
126
127 // Find potential rules in changed files
128 let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND])
129 let seen = new Set()
130
131 env.DEBUG && console.time('Reading changed files')
132
133 /** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */
134 let regexParserContent = []
135
136 for (let item of context.changedContent) {
137 let transformer = getTransformer(context.tailwindConfig, item.extension)
138 let extractor = getExtractor(context, item.extension)
139 regexParserContent.push([item, { transformer, extractor }])
140 }
141
142 const BATCH_SIZE = 500
143
144 for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) {
145 let batch = regexParserContent.slice(i, i + BATCH_SIZE)
146 await Promise.all(
147 batch.map(async ([{ file, content }, { transformer, extractor }]) => {
148 content = file ? await fs.promises.readFile(file, 'utf8') : content
149 getClassCandidates(transformer(content), extractor, candidates, seen)
150 })
151 )
152 }
153
154 env.DEBUG && console.timeEnd('Reading changed files')
155
156 // ---
157
158 // Generate the actual CSS
159 let classCacheCount = context.classCache.size
160
161 env.DEBUG && console.time('Generate rules')
162 env.DEBUG && console.time('Sorting candidates')
163 let sortedCandidates = new Set(
164 [...candidates].sort((a, z) => {
165 if (a === z) return 0
166 if (a < z) return -1
167 return 1
168 })
169 )
170 env.DEBUG && console.timeEnd('Sorting candidates')
171 generateRules(sortedCandidates, context)
172 env.DEBUG && console.timeEnd('Generate rules')
173
174 // We only ever add to the classCache, so if it didn't grow, there is nothing new.
175 env.DEBUG && console.time('Build stylesheet')
176 if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) {
177 context.stylesheetCache = buildStylesheet([...context.ruleCache], context)
178 }
179 env.DEBUG && console.timeEnd('Build stylesheet')
180
181 let {
182 defaults: defaultNodes,
183 base: baseNodes,
184 components: componentNodes,
185 utilities: utilityNodes,
186 variants: screenNodes,
187 } = context.stylesheetCache
188
189 // ---
190
191 // Replace any Tailwind directives with generated CSS
192
193 if (layerNodes.base) {
194 layerNodes.base.before(
195 cloneNodes([...defaultNodes, ...baseNodes], layerNodes.base.source, {
196 layer: 'base',
197 })
198 )
199 layerNodes.base.remove()
200 }
201
202 if (layerNodes.components) {
203 layerNodes.components.before(
204 cloneNodes([...componentNodes], layerNodes.components.source, {
205 layer: 'components',
206 })
207 )
208 layerNodes.components.remove()
209 }
210
211 if (layerNodes.utilities) {
212 layerNodes.utilities.before(
213 cloneNodes([...utilityNodes], layerNodes.utilities.source, {
214 layer: 'utilities',
215 })
216 )
217 layerNodes.utilities.remove()
218 }
219
220 // We do post-filtering to not alter the emitted order of the variants
221 const variantNodes = Array.from(screenNodes).filter((node) => {
222 const parentLayer = node.raws.tailwind?.parentLayer
223
224 if (parentLayer === 'components') {
225 return layerNodes.components !== null
226 }
227
228 if (parentLayer === 'utilities') {
229 return layerNodes.utilities !== null
230 }
231
232 return true
233 })
234
235 if (layerNodes.variants) {
236 layerNodes.variants.before(
237 cloneNodes(variantNodes, layerNodes.variants.source, {
238 layer: 'variants',
239 })
240 )
241 layerNodes.variants.remove()
242 } else if (variantNodes.length > 0) {
243 root.append(
244 cloneNodes(variantNodes, root.source, {
245 layer: 'variants',
246 })
247 )
248 }
249
250 // TODO: Why is the root node having no source location for `end` possible?
251 root.source.end = root.source.end ?? root.source.start
252
253 // If we've got a utility layer and no utilities are generated there's likely something wrong
254 const hasUtilityVariants = variantNodes.some(
255 (node) => node.raws.tailwind?.parentLayer === 'utilities'
256 )
257
258 if (layerNodes.utilities && utilityNodes.size === 0 && !hasUtilityVariants) {
259 log.warn('content-problems', [
260 'No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.',
261 'https://tailwindcss.com/docs/content-configuration',
262 ])
263 }
264
265 // ---
266
267 if (env.DEBUG) {
268 console.log('Potential classes: ', candidates.size)
269 console.log('Active contexts: ', sharedState.contextSourcesMap.size)
270 }
271
272 // Clear the cache for the changed files
273 context.changedContent = []
274
275 // Cleanup any leftover @layer atrules
276 root.walkAtRules('layer', (rule) => {
277 if (Object.keys(layerNodes).includes(rule.params)) {
278 rule.remove()
279 }
280 })
281 }
282}
Note: See TracBrowser for help on using the repository browser.