source: frontend/node_modules/tailwindcss/src/lib/setupContextUtils.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: 41.6 KB
Line 
1import fs from 'fs'
2import url from 'url'
3import postcss from 'postcss'
4import dlv from 'dlv'
5import selectorParser from 'postcss-selector-parser'
6
7import transformThemeValue from '../util/transformThemeValue'
8import parseObjectStyles from '../util/parseObjectStyles'
9import prefixSelector from '../util/prefixSelector'
10import isPlainObject from '../util/isPlainObject'
11import escapeClassName from '../util/escapeClassName'
12import nameClass, { formatClass } from '../util/nameClass'
13import { coerceValue } from '../util/pluginUtils'
14import { variantPlugins, corePlugins } from '../corePlugins'
15import * as sharedState from './sharedState'
16import { env } from './sharedState'
17import { toPath } from '../util/toPath'
18import log from '../util/log'
19import negateValue from '../util/negateValue'
20import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPropertyValue'
21import { generateRules, getClassNameFromSelector } from './generateRules'
22import { hasContentChanged } from './cacheInvalidation.js'
23import { Offsets } from './offsets.js'
24import { flagEnabled } from '../featureFlags.js'
25import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
26
27export const INTERNAL_FEATURES = Symbol()
28
29const VARIANT_TYPES = {
30 AddVariant: Symbol.for('ADD_VARIANT'),
31 MatchVariant: Symbol.for('MATCH_VARIANT'),
32}
33
34const VARIANT_INFO = {
35 Base: 1 << 0,
36 Dynamic: 1 << 1,
37}
38
39function prefix(context, selector) {
40 let prefix = context.tailwindConfig.prefix
41 return typeof prefix === 'function' ? prefix(selector) : prefix + selector
42}
43
44function normalizeOptionTypes({ type = 'any', ...options }) {
45 let types = [].concat(type)
46
47 return {
48 ...options,
49 types: types.map((type) => {
50 if (Array.isArray(type)) {
51 return { type: type[0], ...type[1] }
52 }
53 return { type, preferOnConflict: false }
54 }),
55 }
56}
57
58function parseVariantFormatString(input) {
59 /** @type {string[]} */
60 let parts = []
61
62 // When parsing whitespace around special characters are insignificant
63 // However, _inside_ of a variant they could be
64 // Because the selector could look like this
65 // @media { &[data-name="foo bar"] }
66 // This is why we do not skip whitespace
67
68 let current = ''
69 let depth = 0
70
71 for (let idx = 0; idx < input.length; idx++) {
72 let char = input[idx]
73
74 if (char === '\\') {
75 // Escaped characters are not special
76 current += '\\' + input[++idx]
77 } else if (char === '{') {
78 // Nested rule: start
79 ++depth
80 parts.push(current.trim())
81 current = ''
82 } else if (char === '}') {
83 // Nested rule: end
84 if (--depth < 0) {
85 throw new Error(`Your { and } are unbalanced.`)
86 }
87
88 parts.push(current.trim())
89 current = ''
90 } else {
91 // Normal character
92 current += char
93 }
94 }
95
96 if (current.length > 0) {
97 parts.push(current.trim())
98 }
99
100 parts = parts.filter((part) => part !== '')
101
102 return parts
103}
104
105function insertInto(list, value, { before = [] } = {}) {
106 before = [].concat(before)
107
108 if (before.length <= 0) {
109 list.push(value)
110 return
111 }
112
113 let idx = list.length - 1
114 for (let other of before) {
115 let iidx = list.indexOf(other)
116 if (iidx === -1) continue
117 idx = Math.min(idx, iidx)
118 }
119
120 list.splice(idx, 0, value)
121}
122
123function parseStyles(styles) {
124 if (!Array.isArray(styles)) {
125 return parseStyles([styles])
126 }
127
128 return styles.flatMap((style) => {
129 let isNode = !Array.isArray(style) && !isPlainObject(style)
130 return isNode ? style : parseObjectStyles(style)
131 })
132}
133
134function getClasses(selector, mutate) {
135 let parser = selectorParser((selectors) => {
136 let allClasses = []
137
138 if (mutate) {
139 mutate(selectors)
140 }
141
142 selectors.walkClasses((classNode) => {
143 allClasses.push(classNode.value)
144 })
145
146 return allClasses
147 })
148 return parser.transformSync(selector)
149}
150
151/**
152 * Ignore everything inside a :not(...). This allows you to write code like
153 * `div:not(.foo)`. If `.foo` is never found in your code, then we used to
154 * not generated it. But now we will ignore everything inside a `:not`, so
155 * that it still gets generated.
156 *
157 * @param {selectorParser.Root} selectors
158 */
159function ignoreNot(selectors) {
160 selectors.walkPseudos((pseudo) => {
161 if (pseudo.value === ':not') {
162 pseudo.remove()
163 }
164 })
165}
166
167function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
168 let classes = []
169 let selectors = []
170
171 if (node.type === 'rule') {
172 // Handle normal rules
173 selectors.push(...node.selectors)
174 } else if (node.type === 'atrule') {
175 // Handle at-rules (which contains nested rules)
176 node.walkRules((rule) => selectors.push(...rule.selectors))
177 }
178
179 for (let selector of selectors) {
180 let classCandidates = getClasses(selector, ignoreNot)
181
182 // At least one of the selectors contains non-"on-demandable" candidates.
183 if (classCandidates.length === 0) {
184 state.containsNonOnDemandable = true
185 }
186
187 for (let classCandidate of classCandidates) {
188 classes.push(classCandidate)
189 }
190 }
191
192 if (depth === 0) {
193 return [state.containsNonOnDemandable || classes.length === 0, classes]
194 }
195
196 return classes
197}
198
199function withIdentifiers(styles) {
200 return parseStyles(styles).flatMap((node) => {
201 let nodeMap = new Map()
202 let [containsNonOnDemandableSelectors, candidates] = extractCandidates(node)
203
204 // If this isn't "on-demandable", assign it a universal candidate to always include it.
205 if (containsNonOnDemandableSelectors) {
206 candidates.unshift(sharedState.NOT_ON_DEMAND)
207 }
208
209 // However, it could be that it also contains "on-demandable" candidates.
210 // E.g.: `span, .foo {}`, in that case it should still be possible to use
211 // `@apply foo` for example.
212 return candidates.map((c) => {
213 if (!nodeMap.has(node)) {
214 nodeMap.set(node, node)
215 }
216 return [c, nodeMap.get(node)]
217 })
218 })
219}
220
221export function isValidVariantFormatString(format) {
222 return format.startsWith('@') || format.includes('&')
223}
224
225export function parseVariant(variant) {
226 variant = variant
227 .replace(/\n+/g, '')
228 .replace(/\s{1,}/g, ' ')
229 .trim()
230
231 let fns = parseVariantFormatString(variant)
232 .map((str) => {
233 if (!str.startsWith('@')) {
234 return ({ format }) => format(str)
235 }
236
237 let [, name, params] = /@(\S*)( .+|[({].*)?/g.exec(str)
238 return ({ wrap }) => wrap(postcss.atRule({ name, params: params?.trim() ?? '' }))
239 })
240 .reverse()
241
242 return (api) => {
243 for (let fn of fns) {
244 fn(api)
245 }
246 }
247}
248
249/**
250 *
251 * @param {any} tailwindConfig
252 * @param {any} context
253 * @param {object} param2
254 * @param {Offsets} param2.offsets
255 */
256function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) {
257 function getConfigValue(path, defaultValue) {
258 return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig
259 }
260
261 function applyConfiguredPrefix(selector) {
262 return prefixSelector(tailwindConfig.prefix, selector)
263 }
264
265 function prefixIdentifier(identifier, options) {
266 if (identifier === sharedState.NOT_ON_DEMAND) {
267 return sharedState.NOT_ON_DEMAND
268 }
269
270 if (!options.respectPrefix) {
271 return identifier
272 }
273
274 return context.tailwindConfig.prefix + identifier
275 }
276
277 function resolveThemeValue(path, defaultValue, opts = {}) {
278 let parts = toPath(path)
279 let value = getConfigValue(['theme', ...parts], defaultValue)
280 return transformThemeValue(parts[0])(value, opts)
281 }
282
283 let variantIdentifier = 0
284 let api = {
285 postcss,
286 prefix: applyConfiguredPrefix,
287 e: escapeClassName,
288 config: getConfigValue,
289 theme: resolveThemeValue,
290 corePlugins: (path) => {
291 if (Array.isArray(tailwindConfig.corePlugins)) {
292 return tailwindConfig.corePlugins.includes(path)
293 }
294
295 return getConfigValue(['corePlugins', path], true)
296 },
297 variants: () => {
298 // Preserved for backwards compatibility but not used in v3.0+
299 return []
300 },
301 addBase(base) {
302 for (let [identifier, rule] of withIdentifiers(base)) {
303 let prefixedIdentifier = prefixIdentifier(identifier, {})
304 let offset = offsets.create('base')
305
306 if (!context.candidateRuleMap.has(prefixedIdentifier)) {
307 context.candidateRuleMap.set(prefixedIdentifier, [])
308 }
309
310 context.candidateRuleMap
311 .get(prefixedIdentifier)
312 .push([{ sort: offset, layer: 'base' }, rule])
313 }
314 },
315 /**
316 * @param {string} group
317 * @param {Record<string, string | string[]>} declarations
318 */
319 addDefaults(group, declarations) {
320 const groups = {
321 [`@defaults ${group}`]: declarations,
322 }
323
324 for (let [identifier, rule] of withIdentifiers(groups)) {
325 let prefixedIdentifier = prefixIdentifier(identifier, {})
326
327 if (!context.candidateRuleMap.has(prefixedIdentifier)) {
328 context.candidateRuleMap.set(prefixedIdentifier, [])
329 }
330
331 context.candidateRuleMap
332 .get(prefixedIdentifier)
333 .push([{ sort: offsets.create('defaults'), layer: 'defaults' }, rule])
334 }
335 },
336 addComponents(components, options) {
337 let defaultOptions = {
338 preserveSource: false,
339 respectPrefix: true,
340 respectImportant: false,
341 }
342
343 options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options)
344
345 for (let [identifier, rule] of withIdentifiers(components)) {
346 let prefixedIdentifier = prefixIdentifier(identifier, options)
347
348 classList.add(prefixedIdentifier)
349
350 if (!context.candidateRuleMap.has(prefixedIdentifier)) {
351 context.candidateRuleMap.set(prefixedIdentifier, [])
352 }
353
354 context.candidateRuleMap
355 .get(prefixedIdentifier)
356 .push([{ sort: offsets.create('components'), layer: 'components', options }, rule])
357 }
358 },
359 addUtilities(utilities, options) {
360 let defaultOptions = {
361 preserveSource: false,
362 respectPrefix: true,
363 respectImportant: true,
364 }
365
366 options = Object.assign({}, defaultOptions, Array.isArray(options) ? {} : options)
367
368 for (let [identifier, rule] of withIdentifiers(utilities)) {
369 let prefixedIdentifier = prefixIdentifier(identifier, options)
370
371 classList.add(prefixedIdentifier)
372
373 if (!context.candidateRuleMap.has(prefixedIdentifier)) {
374 context.candidateRuleMap.set(prefixedIdentifier, [])
375 }
376
377 context.candidateRuleMap
378 .get(prefixedIdentifier)
379 .push([{ sort: offsets.create('utilities'), layer: 'utilities', options }, rule])
380 }
381 },
382 matchUtilities: function (utilities, options) {
383 let defaultOptions = {
384 respectPrefix: true,
385 respectImportant: true,
386 modifiers: false,
387 }
388
389 options = normalizeOptionTypes({ ...defaultOptions, ...options })
390
391 let offset = offsets.create('utilities')
392
393 for (let identifier in utilities) {
394 let prefixedIdentifier = prefixIdentifier(identifier, options)
395 let rule = utilities[identifier]
396
397 classList.add([prefixedIdentifier, options])
398
399 function wrapped(modifier, { isOnlyPlugin }) {
400 let [value, coercedType, utilityModifier] = coerceValue(
401 options.types,
402 modifier,
403 options,
404 tailwindConfig
405 )
406
407 if (value === undefined) {
408 return []
409 }
410
411 if (!options.types.some(({ type }) => type === coercedType)) {
412 if (isOnlyPlugin) {
413 log.warn([
414 `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`,
415 `You can safely update it to \`${identifier}-${modifier.replace(
416 coercedType + ':',
417 ''
418 )}\`.`,
419 ])
420 } else {
421 return []
422 }
423 }
424
425 if (!isSyntacticallyValidPropertyValue(value)) {
426 return []
427 }
428
429 let extras = {
430 get modifier() {
431 if (!options.modifiers) {
432 log.warn(`modifier-used-without-options-for-${identifier}`, [
433 'Your plugin must set `modifiers: true` in its options to support modifiers.',
434 ])
435 }
436
437 return utilityModifier
438 },
439 }
440
441 let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
442
443 let ruleSets = []
444 .concat(modifiersEnabled ? rule(value, extras) : rule(value))
445 .filter(Boolean)
446 .map((declaration) => ({
447 [nameClass(identifier, modifier)]: declaration,
448 }))
449
450 return ruleSets
451 }
452
453 let withOffsets = [{ sort: offset, layer: 'utilities', options }, wrapped]
454
455 if (!context.candidateRuleMap.has(prefixedIdentifier)) {
456 context.candidateRuleMap.set(prefixedIdentifier, [])
457 }
458
459 context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets)
460 }
461 },
462 matchComponents: function (components, options) {
463 let defaultOptions = {
464 respectPrefix: true,
465 respectImportant: false,
466 modifiers: false,
467 }
468
469 options = normalizeOptionTypes({ ...defaultOptions, ...options })
470
471 let offset = offsets.create('components')
472
473 for (let identifier in components) {
474 let prefixedIdentifier = prefixIdentifier(identifier, options)
475 let rule = components[identifier]
476
477 classList.add([prefixedIdentifier, options])
478
479 function wrapped(modifier, { isOnlyPlugin }) {
480 let [value, coercedType, utilityModifier] = coerceValue(
481 options.types,
482 modifier,
483 options,
484 tailwindConfig
485 )
486
487 if (value === undefined) {
488 return []
489 }
490
491 if (!options.types.some(({ type }) => type === coercedType)) {
492 if (isOnlyPlugin) {
493 log.warn([
494 `Unnecessary typehint \`${coercedType}\` in \`${identifier}-${modifier}\`.`,
495 `You can safely update it to \`${identifier}-${modifier.replace(
496 coercedType + ':',
497 ''
498 )}\`.`,
499 ])
500 } else {
501 return []
502 }
503 }
504
505 if (!isSyntacticallyValidPropertyValue(value)) {
506 return []
507 }
508
509 let extras = {
510 get modifier() {
511 if (!options.modifiers) {
512 log.warn(`modifier-used-without-options-for-${identifier}`, [
513 'Your plugin must set `modifiers: true` in its options to support modifiers.',
514 ])
515 }
516
517 return utilityModifier
518 },
519 }
520
521 let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
522
523 let ruleSets = []
524 .concat(modifiersEnabled ? rule(value, extras) : rule(value))
525 .filter(Boolean)
526 .map((declaration) => ({
527 [nameClass(identifier, modifier)]: declaration,
528 }))
529
530 return ruleSets
531 }
532
533 let withOffsets = [{ sort: offset, layer: 'components', options }, wrapped]
534
535 if (!context.candidateRuleMap.has(prefixedIdentifier)) {
536 context.candidateRuleMap.set(prefixedIdentifier, [])
537 }
538
539 context.candidateRuleMap.get(prefixedIdentifier).push(withOffsets)
540 }
541 },
542 addVariant(variantName, variantFunctions, options = {}) {
543 variantFunctions = [].concat(variantFunctions).map((variantFunction) => {
544 if (typeof variantFunction !== 'string') {
545 // Safelist public API functions
546 return (api = {}) => {
547 let { args, modifySelectors, container, separator, wrap, format } = api
548 let result = variantFunction(
549 Object.assign(
550 { modifySelectors, container, separator },
551 options.type === VARIANT_TYPES.MatchVariant && { args, wrap, format }
552 )
553 )
554
555 if (typeof result === 'string' && !isValidVariantFormatString(result)) {
556 throw new Error(
557 `Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`
558 )
559 }
560
561 if (Array.isArray(result)) {
562 return result
563 .filter((variant) => typeof variant === 'string')
564 .map((variant) => parseVariant(variant))
565 }
566
567 // result may be undefined with legacy variants that use APIs like `modifySelectors`
568 // result may also be a postcss node if someone was returning the result from `modifySelectors`
569 return result && typeof result === 'string' && parseVariant(result)(api)
570 }
571 }
572
573 if (!isValidVariantFormatString(variantFunction)) {
574 throw new Error(
575 `Your custom variant \`${variantName}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`
576 )
577 }
578
579 return parseVariant(variantFunction)
580 })
581
582 insertInto(variantList, variantName, options)
583 variantMap.set(variantName, variantFunctions)
584 context.variantOptions.set(variantName, options)
585 },
586 matchVariant(variant, variantFn, options) {
587 // A unique identifier that "groups" these variants together.
588 // This is for internal use only which is why it is not present in the types
589 let id = options?.id ?? ++variantIdentifier
590 let isSpecial = variant === '@'
591
592 let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
593
594 for (let [key, value] of Object.entries(options?.values ?? {})) {
595 if (key === 'DEFAULT') continue
596
597 api.addVariant(
598 isSpecial ? `${variant}${key}` : `${variant}-${key}`,
599 ({ args, container }) => {
600 return variantFn(
601 value,
602 modifiersEnabled ? { modifier: args?.modifier, container } : { container }
603 )
604 },
605
606 {
607 ...options,
608 value,
609 id,
610 type: VARIANT_TYPES.MatchVariant,
611 variantInfo: VARIANT_INFO.Base,
612 }
613 )
614 }
615
616 let hasDefault = 'DEFAULT' in (options?.values ?? {})
617
618 api.addVariant(
619 variant,
620 ({ args, container }) => {
621 if (args?.value === sharedState.NONE && !hasDefault) {
622 return null
623 }
624
625 return variantFn(
626 args?.value === sharedState.NONE
627 ? options.values.DEFAULT
628 : // Falling back to args if it is a string, otherwise '' for older intellisense
629 // (JetBrains) plugins.
630 args?.value ?? (typeof args === 'string' ? args : ''),
631 modifiersEnabled ? { modifier: args?.modifier, container } : { container }
632 )
633 },
634 {
635 ...options,
636 id,
637 type: VARIANT_TYPES.MatchVariant,
638 variantInfo: VARIANT_INFO.Dynamic,
639 }
640 )
641 },
642 }
643
644 return api
645}
646
647let fileModifiedMapCache = new WeakMap()
648export function getFileModifiedMap(context) {
649 if (!fileModifiedMapCache.has(context)) {
650 fileModifiedMapCache.set(context, new Map())
651 }
652 return fileModifiedMapCache.get(context)
653}
654
655function trackModified(files, fileModifiedMap) {
656 let changed = false
657 let mtimesToCommit = new Map()
658
659 for (let file of files) {
660 if (!file) continue
661
662 let parsed = url.parse(file)
663 let pathname = parsed.hash ? parsed.href.replace(parsed.hash, '') : parsed.href
664 pathname = parsed.search ? pathname.replace(parsed.search, '') : pathname
665 let newModified = fs.statSync(decodeURIComponent(pathname), { throwIfNoEntry: false })?.mtimeMs
666 if (!newModified) {
667 // It could happen that a file is passed in that doesn't exist. E.g.:
668 // postcss-cli will provide you a fake path when reading from stdin. This
669 // path then looks like /path-to-your-project/stdin In that case we just
670 // want to ignore it and don't track changes at all.
671 continue
672 }
673
674 if (!fileModifiedMap.has(file) || newModified > fileModifiedMap.get(file)) {
675 changed = true
676 }
677
678 mtimesToCommit.set(file, newModified)
679 }
680
681 return [changed, mtimesToCommit]
682}
683
684function extractVariantAtRules(node) {
685 node.walkAtRules((atRule) => {
686 if (['responsive', 'variants'].includes(atRule.name)) {
687 extractVariantAtRules(atRule)
688 atRule.before(atRule.nodes)
689 atRule.remove()
690 }
691 })
692}
693
694function collectLayerPlugins(root) {
695 let layerPlugins = []
696
697 root.each((node) => {
698 if (node.type === 'atrule' && ['responsive', 'variants'].includes(node.name)) {
699 node.name = 'layer'
700 node.params = 'utilities'
701 }
702 })
703
704 // Walk @layer rules and treat them like plugins
705 root.walkAtRules('layer', (layerRule) => {
706 extractVariantAtRules(layerRule)
707
708 if (layerRule.params === 'base') {
709 for (let node of layerRule.nodes) {
710 layerPlugins.push(function ({ addBase }) {
711 addBase(node, { respectPrefix: false })
712 })
713 }
714 layerRule.remove()
715 } else if (layerRule.params === 'components') {
716 for (let node of layerRule.nodes) {
717 layerPlugins.push(function ({ addComponents }) {
718 addComponents(node, { respectPrefix: false, preserveSource: true })
719 })
720 }
721 layerRule.remove()
722 } else if (layerRule.params === 'utilities') {
723 for (let node of layerRule.nodes) {
724 layerPlugins.push(function ({ addUtilities }) {
725 addUtilities(node, { respectPrefix: false, preserveSource: true })
726 })
727 }
728 layerRule.remove()
729 }
730 })
731
732 return layerPlugins
733}
734
735function resolvePlugins(context, root) {
736 let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins })
737 .map(([name, plugin]) => {
738 if (!context.tailwindConfig.corePlugins.includes(name)) {
739 return null
740 }
741
742 return plugin
743 })
744 .filter(Boolean)
745
746 let userPlugins = context.tailwindConfig.plugins.map((plugin) => {
747 if (plugin.__isOptionsFunction) {
748 plugin = plugin()
749 }
750
751 return typeof plugin === 'function' ? plugin : plugin.handler
752 })
753
754 let layerPlugins = collectLayerPlugins(root)
755
756 // TODO: This is a workaround for backwards compatibility, since custom variants
757 // were historically sorted before screen/stackable variants.
758 let beforeVariants = [
759 variantPlugins['childVariant'],
760 variantPlugins['pseudoElementVariants'],
761 variantPlugins['pseudoClassVariants'],
762 variantPlugins['hasVariants'],
763 variantPlugins['ariaVariants'],
764 variantPlugins['dataVariants'],
765 ]
766 let afterVariants = [
767 variantPlugins['supportsVariants'],
768 variantPlugins['reducedMotionVariants'],
769 variantPlugins['prefersContrastVariants'],
770 variantPlugins['screenVariants'],
771 variantPlugins['orientationVariants'],
772 variantPlugins['directionVariants'],
773 variantPlugins['darkVariants'],
774 variantPlugins['forcedColorsVariants'],
775 variantPlugins['printVariant'],
776 ]
777
778 // This is a compatibility fix for the pre 3.4 dark mode behavior
779 // `class` retains the old behavior, but `selector` keeps the new behavior
780 let isLegacyDarkMode =
781 context.tailwindConfig.darkMode === 'class' ||
782 (Array.isArray(context.tailwindConfig.darkMode) &&
783 context.tailwindConfig.darkMode[0] === 'class')
784
785 if (isLegacyDarkMode) {
786 afterVariants = [
787 variantPlugins['supportsVariants'],
788 variantPlugins['reducedMotionVariants'],
789 variantPlugins['prefersContrastVariants'],
790 variantPlugins['darkVariants'],
791 variantPlugins['screenVariants'],
792 variantPlugins['orientationVariants'],
793 variantPlugins['directionVariants'],
794 variantPlugins['forcedColorsVariants'],
795 variantPlugins['printVariant'],
796 ]
797 }
798
799 return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
800}
801
802function registerPlugins(plugins, context) {
803 let variantList = []
804 let variantMap = new Map()
805 context.variantMap = variantMap
806
807 let offsets = new Offsets()
808 context.offsets = offsets
809
810 let classList = new Set()
811
812 let pluginApi = buildPluginApi(context.tailwindConfig, context, {
813 variantList,
814 variantMap,
815 offsets,
816 classList,
817 })
818
819 for (let plugin of plugins) {
820 if (Array.isArray(plugin)) {
821 for (let pluginItem of plugin) {
822 pluginItem(pluginApi)
823 }
824 } else {
825 plugin?.(pluginApi)
826 }
827 }
828
829 // Make sure to record bit masks for every variant
830 offsets.recordVariants(variantList, (variant) => variantMap.get(variant).length)
831
832 // Build variantMap
833 for (let [variantName, variantFunctions] of variantMap.entries()) {
834 context.variantMap.set(
835 variantName,
836 variantFunctions.map((variantFunction, idx) => [
837 offsets.forVariant(variantName, idx),
838 variantFunction,
839 ])
840 )
841 }
842
843 let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean)
844 if (safelist.length > 0) {
845 let checks = []
846
847 for (let value of safelist) {
848 if (typeof value === 'string') {
849 context.changedContent.push({ content: value, extension: 'html' })
850 continue
851 }
852
853 if (value instanceof RegExp) {
854 log.warn('root-regex', [
855 'Regular expressions in `safelist` work differently in Tailwind CSS v3.0.',
856 'Update your `safelist` configuration to eliminate this warning.',
857 'https://tailwindcss.com/docs/content-configuration#safelisting-classes',
858 ])
859 continue
860 }
861
862 checks.push(value)
863 }
864
865 if (checks.length > 0) {
866 let patternMatchingCount = new Map()
867 let prefixLength = context.tailwindConfig.prefix.length
868 let checkImportantUtils = checks.some((check) => check.pattern.source.includes('!'))
869
870 for (let util of classList) {
871 let utils = Array.isArray(util)
872 ? (() => {
873 let [utilName, options] = util
874 let values = Object.keys(options?.values ?? {})
875 let classes = values.map((value) => formatClass(utilName, value))
876
877 if (options?.supportsNegativeValues) {
878 // This is the normal negated version
879 // e.g. `-inset-1` or `-tw-inset-1`
880 classes = [...classes, ...classes.map((cls) => '-' + cls)]
881
882 // This is the negated version *after* the prefix
883 // e.g. `tw--inset-1`
884 // The prefix is already attached to util name
885 // So we add the negative after the prefix
886 classes = [
887 ...classes,
888 ...classes.map(
889 (cls) => cls.slice(0, prefixLength) + '-' + cls.slice(prefixLength)
890 ),
891 ]
892 }
893
894 if (options.types.some(({ type }) => type === 'color')) {
895 classes = [
896 ...classes,
897 ...classes.flatMap((cls) =>
898 Object.keys(context.tailwindConfig.theme.opacity).map(
899 (opacity) => `${cls}/${opacity}`
900 )
901 ),
902 ]
903 }
904
905 if (checkImportantUtils && options?.respectImportant) {
906 classes = [...classes, ...classes.map((cls) => '!' + cls)]
907 }
908
909 return classes
910 })()
911 : [util]
912
913 for (let util of utils) {
914 for (let { pattern, variants = [] } of checks) {
915 // RegExp with the /g flag are stateful, so let's reset the last
916 // index pointer to reset the state.
917 pattern.lastIndex = 0
918
919 if (!patternMatchingCount.has(pattern)) {
920 patternMatchingCount.set(pattern, 0)
921 }
922
923 if (!pattern.test(util)) continue
924
925 patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1)
926
927 context.changedContent.push({ content: util, extension: 'html' })
928 for (let variant of variants) {
929 context.changedContent.push({
930 content: variant + context.tailwindConfig.separator + util,
931 extension: 'html',
932 })
933 }
934 }
935 }
936 }
937
938 for (let [regex, count] of patternMatchingCount.entries()) {
939 if (count !== 0) continue
940
941 log.warn([
942 `The safelist pattern \`${regex}\` doesn't match any Tailwind CSS classes.`,
943 'Fix this pattern or remove it from your `safelist` configuration.',
944 'https://tailwindcss.com/docs/content-configuration#safelisting-classes',
945 ])
946 }
947 }
948 }
949
950 let darkClassName = [].concat(context.tailwindConfig.darkMode ?? 'media')[1] ?? 'dark'
951
952 // A list of utilities that are used by certain Tailwind CSS utilities but
953 // that don't exist on their own. This will result in them "not existing" and
954 // sorting could be weird since you still require them in order to make the
955 // host utilities work properly. (Thanks Biology)
956 let parasiteUtilities = [
957 prefix(context, darkClassName),
958 prefix(context, 'group'),
959 prefix(context, 'peer'),
960 ]
961 context.getClassOrder = function getClassOrder(classes) {
962 // Sort classes so they're ordered in a deterministic manner
963 let sorted = [...classes].sort((a, z) => {
964 if (a === z) return 0
965 if (a < z) return -1
966 return 1
967 })
968
969 // Non-util classes won't be generated, so we default them to null
970 let sortedClassNames = new Map(sorted.map((className) => [className, null]))
971
972 // Sort all classes in order
973 // Non-tailwind classes won't be generated and will be left as `null`
974 let rules = generateRules(new Set(sorted), context, true)
975 rules = context.offsets.sort(rules)
976
977 let idx = BigInt(parasiteUtilities.length)
978
979 for (const [, rule] of rules) {
980 let candidate = rule.raws.tailwind.candidate
981
982 // When multiple rules match a candidate
983 // always take the position of the first one
984 sortedClassNames.set(candidate, sortedClassNames.get(candidate) ?? idx++)
985 }
986
987 return classes.map((className) => {
988 let order = sortedClassNames.get(className) ?? null
989 let parasiteIndex = parasiteUtilities.indexOf(className)
990
991 if (order === null && parasiteIndex !== -1) {
992 // This will make sure that it is at the very beginning of the
993 // `components` layer which technically means 'before any
994 // components'.
995 order = BigInt(parasiteIndex)
996 }
997
998 return [className, order]
999 })
1000 }
1001
1002 // Generate a list of strings for autocompletion purposes, e.g.
1003 // ['uppercase', 'lowercase', ...]
1004 context.getClassList = function getClassList(options = {}) {
1005 let output = []
1006
1007 for (let util of classList) {
1008 if (Array.isArray(util)) {
1009 let [utilName, utilOptions] = util
1010 let negativeClasses = []
1011
1012 let modifiers = Object.keys(utilOptions?.modifiers ?? {})
1013
1014 if (utilOptions?.types?.some(({ type }) => type === 'color')) {
1015 modifiers.push(...Object.keys(context.tailwindConfig.theme.opacity ?? {}))
1016 }
1017
1018 let metadata = { modifiers }
1019 let includeMetadata = options.includeMetadata && modifiers.length > 0
1020
1021 for (let [key, value] of Object.entries(utilOptions?.values ?? {})) {
1022 // Ignore undefined and null values
1023 if (value == null) {
1024 continue
1025 }
1026
1027 let cls = formatClass(utilName, key)
1028 output.push(includeMetadata ? [cls, metadata] : cls)
1029
1030 if (utilOptions?.supportsNegativeValues && negateValue(value)) {
1031 let cls = formatClass(utilName, `-${key}`)
1032 negativeClasses.push(includeMetadata ? [cls, metadata] : cls)
1033 }
1034 }
1035
1036 output.push(...negativeClasses)
1037 } else {
1038 output.push(util)
1039 }
1040 }
1041
1042 return output
1043 }
1044
1045 // Generate a list of available variants with meta information of the type of variant.
1046 context.getVariants = function getVariants() {
1047 // We use a unique, random ID for candidate names to avoid conflicts
1048 // We can't use characters like `_`, `:`, `@` or `.` because they might
1049 // be used as a separator
1050 let id = Math.random().toString(36).substring(7).toUpperCase()
1051
1052 let result = []
1053 for (let [name, options] of context.variantOptions.entries()) {
1054 if (options.variantInfo === VARIANT_INFO.Base) continue
1055
1056 result.push({
1057 name,
1058 isArbitrary: options.type === Symbol.for('MATCH_VARIANT'),
1059 values: Object.keys(options.values ?? {}),
1060 hasDash: name !== '@',
1061 selectors({ modifier, value } = {}) {
1062 let candidate = `TAILWINDPLACEHOLDER${id}`
1063
1064 let rule = postcss.rule({ selector: `.${candidate}` })
1065 let container = postcss.root({ nodes: [rule.clone()] })
1066
1067 let before = container.toString()
1068
1069 let fns = (context.variantMap.get(name) ?? []).flatMap(([_, fn]) => fn)
1070 let formatStrings = []
1071 for (let fn of fns) {
1072 let localFormatStrings = []
1073
1074 let api = {
1075 args: { modifier, value: options.values?.[value] ?? value },
1076 separator: context.tailwindConfig.separator,
1077 modifySelectors(modifierFunction) {
1078 // Run the modifierFunction over each rule
1079 container.each((rule) => {
1080 if (rule.type !== 'rule') {
1081 return
1082 }
1083
1084 rule.selectors = rule.selectors.map((selector) => {
1085 return modifierFunction({
1086 get className() {
1087 return getClassNameFromSelector(selector)
1088 },
1089 selector,
1090 })
1091 })
1092 })
1093
1094 return container
1095 },
1096 format(str) {
1097 localFormatStrings.push(str)
1098 },
1099 wrap(wrapper) {
1100 localFormatStrings.push(`@${wrapper.name} ${wrapper.params} { & }`)
1101 },
1102 container,
1103 }
1104
1105 let ruleWithVariant = fn(api)
1106 if (localFormatStrings.length > 0) {
1107 formatStrings.push(localFormatStrings)
1108 }
1109
1110 if (Array.isArray(ruleWithVariant)) {
1111 for (let variantFunction of ruleWithVariant) {
1112 localFormatStrings = []
1113 variantFunction(api)
1114 formatStrings.push(localFormatStrings)
1115 }
1116 }
1117 }
1118
1119 // Reverse engineer the result of the `container`
1120 let manualFormatStrings = []
1121 let after = container.toString()
1122
1123 if (before !== after) {
1124 // Figure out all selectors
1125 container.walkRules((rule) => {
1126 let modified = rule.selector
1127
1128 // Rebuild the base selector, this is what plugin authors would do
1129 // as well. E.g.: `${variant}${separator}${className}`.
1130 // However, plugin authors probably also prepend or append certain
1131 // classes, pseudos, ids, ...
1132 let rebuiltBase = selectorParser((selectors) => {
1133 selectors.walkClasses((classNode) => {
1134 classNode.value = `${name}${context.tailwindConfig.separator}${classNode.value}`
1135 })
1136 }).processSync(modified)
1137
1138 // Now that we know the original selector, the new selector, and
1139 // the rebuild part in between, we can replace the part that plugin
1140 // authors need to rebuild with `&`, and eventually store it in the
1141 // collectedFormats. Similar to what `format('...')` would do.
1142 //
1143 // E.g.:
1144 // variant: foo
1145 // selector: .markdown > p
1146 // modified (by plugin): .foo .foo\\:markdown > p
1147 // rebuiltBase (internal): .foo\\:markdown > p
1148 // format: .foo &
1149 manualFormatStrings.push(modified.replace(rebuiltBase, '&').replace(candidate, '&'))
1150 })
1151
1152 // Figure out all atrules
1153 container.walkAtRules((atrule) => {
1154 manualFormatStrings.push(`@${atrule.name} (${atrule.params}) { & }`)
1155 })
1156 }
1157
1158 let isArbitraryVariant = !(value in (options.values ?? {}))
1159 let internalFeatures = options[INTERNAL_FEATURES] ?? {}
1160
1161 let respectPrefix = (() => {
1162 if (isArbitraryVariant) return false
1163 if (internalFeatures.respectPrefix === false) return false
1164 return true
1165 })()
1166
1167 formatStrings = formatStrings.map((format) =>
1168 format.map((str) => ({
1169 format: str,
1170 respectPrefix,
1171 }))
1172 )
1173
1174 manualFormatStrings = manualFormatStrings.map((format) => ({
1175 format,
1176 respectPrefix,
1177 }))
1178
1179 let opts = {
1180 candidate,
1181 context,
1182 }
1183
1184 let result = formatStrings.map((formats) =>
1185 finalizeSelector(`.${candidate}`, formatVariantSelector(formats, opts), opts)
1186 .replace(`.${candidate}`, '&')
1187 .replace('{ & }', '')
1188 .trim()
1189 )
1190
1191 if (manualFormatStrings.length > 0) {
1192 result.push(
1193 formatVariantSelector(manualFormatStrings, opts)
1194 .toString()
1195 .replace(`.${candidate}`, '&')
1196 )
1197 }
1198
1199 return result
1200 },
1201 })
1202 }
1203
1204 return result
1205 }
1206}
1207
1208/**
1209 * Mark as class as retroactively invalid
1210 *
1211 *
1212 * @param {string} candidate
1213 */
1214function markInvalidUtilityCandidate(context, candidate) {
1215 if (!context.classCache.has(candidate)) {
1216 return
1217 }
1218
1219 // Mark this as not being a real utility
1220 context.notClassCache.add(candidate)
1221
1222 // Remove it from any candidate-specific caches
1223 context.classCache.delete(candidate)
1224 context.applyClassCache.delete(candidate)
1225 context.candidateRuleMap.delete(candidate)
1226 context.candidateRuleCache.delete(candidate)
1227
1228 // Ensure the stylesheet gets rebuilt
1229 context.stylesheetCache = null
1230}
1231
1232/**
1233 * Mark as class as retroactively invalid
1234 *
1235 * @param {import('postcss').Node} node
1236 */
1237function markInvalidUtilityNode(context, node) {
1238 let candidate = node.raws.tailwind.candidate
1239
1240 if (!candidate) {
1241 return
1242 }
1243
1244 for (const entry of context.ruleCache) {
1245 if (entry[1].raws.tailwind.candidate === candidate) {
1246 context.ruleCache.delete(entry)
1247 // context.postCssNodeCache.delete(node)
1248 }
1249 }
1250
1251 markInvalidUtilityCandidate(context, candidate)
1252}
1253
1254export function createContext(tailwindConfig, changedContent = [], root = postcss.root()) {
1255 let context = {
1256 disposables: [],
1257 ruleCache: new Set(),
1258 candidateRuleCache: new Map(),
1259 classCache: new Map(),
1260 applyClassCache: new Map(),
1261 // Seed the not class cache with the blocklist (which is only strings)
1262 notClassCache: new Set(tailwindConfig.blocklist ?? []),
1263 postCssNodeCache: new Map(),
1264 candidateRuleMap: new Map(),
1265 tailwindConfig,
1266 changedContent: changedContent,
1267 variantMap: new Map(),
1268 stylesheetCache: null,
1269 variantOptions: new Map(),
1270
1271 markInvalidUtilityCandidate: (candidate) => markInvalidUtilityCandidate(context, candidate),
1272 markInvalidUtilityNode: (node) => markInvalidUtilityNode(context, node),
1273 }
1274
1275 let resolvedPlugins = resolvePlugins(context, root)
1276 registerPlugins(resolvedPlugins, context)
1277
1278 return context
1279}
1280
1281let contextMap = sharedState.contextMap
1282let configContextMap = sharedState.configContextMap
1283let contextSourcesMap = sharedState.contextSourcesMap
1284
1285export function getContext(
1286 root,
1287 result,
1288 tailwindConfig,
1289 userConfigPath,
1290 tailwindConfigHash,
1291 contextDependencies
1292) {
1293 let sourcePath = result.opts.from
1294 let isConfigFile = userConfigPath !== null
1295
1296 env.DEBUG && console.log('Source path:', sourcePath)
1297
1298 let existingContext
1299
1300 if (isConfigFile && contextMap.has(sourcePath)) {
1301 existingContext = contextMap.get(sourcePath)
1302 } else if (configContextMap.has(tailwindConfigHash)) {
1303 let context = configContextMap.get(tailwindConfigHash)
1304 contextSourcesMap.get(context).add(sourcePath)
1305 contextMap.set(sourcePath, context)
1306
1307 existingContext = context
1308 }
1309
1310 let cssDidChange = hasContentChanged(sourcePath, root)
1311
1312 // If there's already a context in the cache and we don't need to
1313 // reset the context, return the cached context.
1314 if (existingContext) {
1315 let [contextDependenciesChanged, mtimesToCommit] = trackModified(
1316 [...contextDependencies],
1317 getFileModifiedMap(existingContext)
1318 )
1319 if (!contextDependenciesChanged && !cssDidChange) {
1320 return [existingContext, false, mtimesToCommit]
1321 }
1322 }
1323
1324 // If this source is in the context map, get the old context.
1325 // Remove this source from the context sources for the old context,
1326 // and clean up that context if no one else is using it. This can be
1327 // called by many processes in rapid succession, so we check for presence
1328 // first because the first process to run this code will wipe it out first.
1329 if (contextMap.has(sourcePath)) {
1330 let oldContext = contextMap.get(sourcePath)
1331 if (contextSourcesMap.has(oldContext)) {
1332 contextSourcesMap.get(oldContext).delete(sourcePath)
1333 if (contextSourcesMap.get(oldContext).size === 0) {
1334 contextSourcesMap.delete(oldContext)
1335 for (let [tailwindConfigHash, context] of configContextMap) {
1336 if (context === oldContext) {
1337 configContextMap.delete(tailwindConfigHash)
1338 }
1339 }
1340 for (let disposable of oldContext.disposables.splice(0)) {
1341 disposable(oldContext)
1342 }
1343 }
1344 }
1345 }
1346
1347 env.DEBUG && console.log('Setting up new context...')
1348
1349 let context = createContext(tailwindConfig, [], root)
1350
1351 Object.assign(context, {
1352 userConfigPath,
1353 })
1354
1355 let [, mtimesToCommit] = trackModified([...contextDependencies], getFileModifiedMap(context))
1356
1357 // ---
1358
1359 // Update all context tracking state
1360
1361 configContextMap.set(tailwindConfigHash, context)
1362 contextMap.set(sourcePath, context)
1363
1364 if (!contextSourcesMap.has(context)) {
1365 contextSourcesMap.set(context, new Set())
1366 }
1367
1368 contextSourcesMap.get(context).add(sourcePath)
1369
1370 return [context, true, mtimesToCommit]
1371}
Note: See TracBrowser for help on using the repository browser.