tailwindcss 3.3.3 → 3.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -164,7 +164,7 @@ export function createWatcher(args, { state, rebuild }) {
164
164
  // This is very likely a chokidar bug but it's one we need to work around
165
165
  // We treat this as a change event and rebuild the CSS
166
166
  watcher.on('raw', (evt, filePath, meta) => {
167
- if (evt !== 'rename') {
167
+ if (evt !== 'rename' || filePath === null) {
168
168
  return
169
169
  }
170
170
 
@@ -22,7 +22,6 @@ let featureFlags = {
22
22
  experimental: [
23
23
  'optimizeUniversalDefaults',
24
24
  'generalizedModifiers',
25
- // 'variantGrouping',
26
25
  ],
27
26
  }
28
27
 
@@ -12,16 +12,17 @@ export function defaultExtractor(context) {
12
12
  let results = []
13
13
 
14
14
  for (let pattern of patterns) {
15
- results = [...results, ...(content.match(pattern) ?? [])]
15
+ for (let result of content.match(pattern) ?? []) {
16
+ results.push(clipAtBalancedParens(result))
17
+ }
16
18
  }
17
19
 
18
- return results.filter((v) => v !== undefined).map(clipAtBalancedParens)
20
+ return results
19
21
  }
20
22
  }
21
23
 
22
24
  function* buildRegExps(context) {
23
25
  let separator = context.tailwindConfig.separator
24
- let variantGroupingEnabled = flagEnabled(context.tailwindConfig, 'variantGrouping')
25
26
  let prefix =
26
27
  context.tailwindConfig.prefix !== ''
27
28
  ? regex.optional(regex.pattern([/-?/, regex.escape(context.tailwindConfig.prefix)]))
@@ -35,7 +36,7 @@ function* buildRegExps(context) {
35
36
  // This is a targeted fix to continue to allow theme()
36
37
  // with square brackets to work in arbitrary properties
37
38
  // while fixing a problem with the regex matching too much
38
- /\[[^\s:'"`]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,
39
+ /\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,
39
40
 
40
41
  // Utilities
41
42
  regex.pattern([
@@ -80,12 +81,18 @@ function* buildRegExps(context) {
80
81
  // This is here to provide special support for the `@` variant
81
82
  regex.pattern([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/, separator]),
82
83
 
84
+ // With variant modifier (e.g.: group-[..]/modifier)
85
+ regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/\w+/, separator]),
86
+
83
87
  regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/, separator]),
84
88
  regex.pattern([/[^\s"'`\[\\]+/, separator]),
85
89
  ]),
86
90
 
87
91
  // With quotes allowed
88
92
  regex.any([
93
+ // With variant modifier (e.g.: group-[..]/modifier)
94
+ regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/\w+/, separator]),
95
+
89
96
  regex.pattern([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/, separator]),
90
97
  regex.pattern([/[^\s`\[\\]+/, separator]),
91
98
  ]),
@@ -103,15 +110,7 @@ function* buildRegExps(context) {
103
110
 
104
111
  prefix,
105
112
 
106
- variantGroupingEnabled
107
- ? regex.any([
108
- // Or any of those things but grouped separated by commas
109
- regex.pattern([/\(/, utility, regex.zeroOrMore([/,/, utility]), /\)/]),
110
-
111
- // Arbitrary properties, constrained utilities, arbitrary values, etc…
112
- utility,
113
- ])
114
- : utility,
113
+ utility,
115
114
  ])
116
115
  }
117
116
 
@@ -553,6 +553,13 @@ function processApply(root, context, localCache) {
553
553
  ? parent.selector.slice(importantSelector.length)
554
554
  : parent.selector
555
555
 
556
+ // If the selector becomes empty after replacing the important selector
557
+ // This means that it's the same as the parent selector and we don't want to replace it
558
+ // Otherwise we'll crash
559
+ if (parentSelector === '') {
560
+ parentSelector = parent.selector
561
+ }
562
+
556
563
  rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate)
557
564
 
558
565
  // And then re-add it if it was removed
@@ -145,14 +145,26 @@ export default function expandTailwindAtRules(context) {
145
145
  // getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
146
146
  // }
147
147
  } else {
148
- await Promise.all(
149
- context.changedContent.map(async ({ file, content, extension }) => {
150
- let transformer = getTransformer(context.tailwindConfig, extension)
151
- let extractor = getExtractor(context, extension)
152
- content = file ? await fs.promises.readFile(file, 'utf8') : content
153
- getClassCandidates(transformer(content), extractor, candidates, seen)
154
- })
155
- )
148
+ /** @type {[item: {file?: string, content?: string}, meta: {transformer: any, extractor: any}][]} */
149
+ let regexParserContent = []
150
+
151
+ for (let item of context.changedContent) {
152
+ let transformer = getTransformer(context.tailwindConfig, item.extension)
153
+ let extractor = getExtractor(context, item.extension)
154
+ regexParserContent.push([item, { transformer, extractor }])
155
+ }
156
+
157
+ const BATCH_SIZE = 500
158
+
159
+ for (let i = 0; i < regexParserContent.length; i += BATCH_SIZE) {
160
+ let batch = regexParserContent.slice(i, i + BATCH_SIZE)
161
+ await Promise.all(
162
+ batch.map(async ([{ file, content }, { transformer, extractor }]) => {
163
+ content = file ? await fs.promises.readFile(file, 'utf8') : content
164
+ getClassCandidates(transformer(content), extractor, candidates, seen)
165
+ })
166
+ )
167
+ }
156
168
  }
157
169
 
158
170
  env.DEBUG && console.timeEnd('Reading changed files')
@@ -496,7 +496,7 @@ function extractArbitraryProperty(classCandidate, context) {
496
496
  return null
497
497
  }
498
498
 
499
- let normalized = normalize(value)
499
+ let normalized = normalize(value, { property })
500
500
 
501
501
  if (!isParsableCssValue(property, normalized)) {
502
502
  return null
@@ -573,7 +573,7 @@ function* recordCandidates(matches, classCandidate) {
573
573
  }
574
574
  }
575
575
 
576
- function* resolveMatches(candidate, context, original = candidate) {
576
+ function* resolveMatches(candidate, context) {
577
577
  let separator = context.tailwindConfig.separator
578
578
  let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
579
579
  let important = false
@@ -583,15 +583,6 @@ function* resolveMatches(candidate, context, original = candidate) {
583
583
  classCandidate = classCandidate.slice(1)
584
584
  }
585
585
 
586
- if (flagEnabled(context.tailwindConfig, 'variantGrouping')) {
587
- if (classCandidate.startsWith('(') && classCandidate.endsWith(')')) {
588
- let base = variants.slice().reverse().join(separator)
589
- for (let part of splitAtTopLevelOnly(classCandidate.slice(1, -1), ',')) {
590
- yield* resolveMatches(base + separator + part, context, original)
591
- }
592
- }
593
- }
594
-
595
586
  // TODO: Reintroduce this in ways that doesn't break on false positives
596
587
  // function sortAgainst(toSort, against) {
597
588
  // return toSort.slice().sort((a, z) => {
@@ -780,7 +771,7 @@ function* resolveMatches(candidate, context, original = candidate) {
780
771
  match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
781
772
 
782
773
  // Apply final format selector
783
- match = applyFinalFormat(match, { context, candidate, original })
774
+ match = applyFinalFormat(match, { context, candidate })
784
775
 
785
776
  // Skip rules with invalid selectors
786
777
  // This will cause the candidate to be added to the "not class"
@@ -794,7 +785,7 @@ function* resolveMatches(candidate, context, original = candidate) {
794
785
  }
795
786
  }
796
787
 
797
- function applyFinalFormat(match, { context, candidate, original }) {
788
+ function applyFinalFormat(match, { context, candidate }) {
798
789
  if (!match[0].collectedFormats) {
799
790
  return match
800
791
  }
@@ -829,10 +820,19 @@ function applyFinalFormat(match, { context, candidate, original }) {
829
820
  }
830
821
 
831
822
  try {
832
- rule.selector = finalizeSelector(rule.selector, finalFormat, {
833
- candidate: original,
823
+ let selector = finalizeSelector(rule.selector, finalFormat, {
824
+ candidate,
834
825
  context,
835
826
  })
827
+
828
+ // Finalize Selector determined that this candidate is irrelevant
829
+ // TODO: This elimination should happen earlier so this never happens
830
+ if (selector === null) {
831
+ rule.remove()
832
+ return
833
+ }
834
+
835
+ rule.selector = selector
836
836
  } catch {
837
837
  // If this selector is invalid we also want to skip it
838
838
  // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
@@ -882,7 +882,7 @@ function getImportantStrategy(important) {
882
882
  }
883
883
  }
884
884
 
885
- function generateRules(candidates, context) {
885
+ function generateRules(candidates, context, isSorting = false) {
886
886
  let allRules = []
887
887
  let strategy = getImportantStrategy(context.tailwindConfig.important)
888
888
 
@@ -917,7 +917,9 @@ function generateRules(candidates, context) {
917
917
  rule = container.nodes[0]
918
918
  }
919
919
 
920
- let newEntry = [sort, rule]
920
+ // Note: We have to clone rules during sorting
921
+ // so we eliminate some shared mutable state
922
+ let newEntry = [sort, isSorting ? rule.clone() : rule]
921
923
  rules.add(newEntry)
922
924
  context.ruleCache.add(newEntry)
923
925
  allRules.push(newEntry)
@@ -148,43 +148,45 @@ function getClasses(selector, mutate) {
148
148
  return parser.transformSync(selector)
149
149
  }
150
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
+ */
159
+ function ignoreNot(selectors) {
160
+ selectors.walkPseudos((pseudo) => {
161
+ if (pseudo.value === ':not') {
162
+ pseudo.remove()
163
+ }
164
+ })
165
+ }
166
+
151
167
  function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
152
168
  let classes = []
169
+ let selectors = []
153
170
 
154
- // Handle normal rules
155
171
  if (node.type === 'rule') {
156
- // Ignore everything inside a :not(...). This allows you to write code like
157
- // `div:not(.foo)`. If `.foo` is never found in your code, then we used to
158
- // not generated it. But now we will ignore everything inside a `:not`, so
159
- // that it still gets generated.
160
- function ignoreNot(selectors) {
161
- selectors.walkPseudos((pseudo) => {
162
- if (pseudo.value === ':not') {
163
- pseudo.remove()
164
- }
165
- })
166
- }
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
+ }
167
178
 
168
- for (let selector of node.selectors) {
169
- let classCandidates = getClasses(selector, ignoreNot)
170
- // At least one of the selectors contains non-"on-demandable" candidates.
171
- if (classCandidates.length === 0) {
172
- state.containsNonOnDemandable = true
173
- }
179
+ for (let selector of selectors) {
180
+ let classCandidates = getClasses(selector, ignoreNot)
174
181
 
175
- for (let classCandidate of classCandidates) {
176
- classes.push(classCandidate)
177
- }
182
+ // At least one of the selectors contains non-"on-demandable" candidates.
183
+ if (classCandidates.length === 0) {
184
+ state.containsNonOnDemandable = true
178
185
  }
179
- }
180
186
 
181
- // Handle at-rules (which contains nested rules)
182
- else if (node.type === 'atrule') {
183
- node.walkRules((rule) => {
184
- for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) {
185
- classes.push(classCandidate)
186
- }
187
- })
187
+ for (let classCandidate of classCandidates) {
188
+ classes.push(classCandidate)
189
+ }
188
190
  }
189
191
 
190
192
  if (depth === 0) {
@@ -945,7 +947,7 @@ function registerPlugins(plugins, context) {
945
947
 
946
948
  // Sort all classes in order
947
949
  // Non-tailwind classes won't be generated and will be left as `null`
948
- let rules = generateRules(new Set(sorted), context)
950
+ let rules = generateRules(new Set(sorted), context, true)
949
951
  rules = context.offsets.sort(rules)
950
952
 
951
953
  let idx = BigInt(parasiteUtilities.length)
@@ -277,9 +277,9 @@ export async function createProcessor(args, cliConfigPath) {
277
277
  let tailwindPlugin = () => {
278
278
  return {
279
279
  postcssPlugin: 'tailwindcss',
280
- Once(root, { result }) {
280
+ async Once(root, { result }) {
281
281
  env.DEBUG && console.time('Compiling CSS')
282
- tailwind(({ createContext }) => {
282
+ await tailwind(({ createContext }) => {
283
283
  console.error()
284
284
  console.error('Rebuilding...')
285
285
 
package/src/util/color.js CHANGED
@@ -5,7 +5,7 @@ let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i
5
5
  let VALUE = /(?:\d+|\d*\.\d+)%?/
6
6
  let SEP = /(?:\s*,\s*|\s+)/
7
7
  let ALPHA_SEP = /\s*[,/]\s*/
8
- let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)\)/
8
+ let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/
9
9
 
10
10
  let RGB = new RegExp(
11
11
  `^(rgba?)\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
@@ -10,13 +10,34 @@ function isCSSFunction(value) {
10
10
  return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value))
11
11
  }
12
12
 
13
- const placeholder = '--tw-placeholder'
14
- const placeholderRe = new RegExp(placeholder, 'g')
13
+ // These properties accept a `<dashed-ident>` as one of the values. This means that you can use them
14
+ // as: `timeline-scope: --tl;`
15
+ //
16
+ // Without the `var(--tl)`, in these cases we don't want to normalize the value, and you should add
17
+ // the `var()` yourself.
18
+ //
19
+ // More info:
20
+ // - https://drafts.csswg.org/scroll-animations/#propdef-timeline-scope
21
+ // - https://developer.mozilla.org/en-US/docs/Web/CSS/timeline-scope#dashed-ident
22
+ //
23
+ const AUTO_VAR_INJECTION_EXCEPTIONS = new Set([
24
+ // Concrete properties
25
+ 'scroll-timeline-name',
26
+ 'timeline-scope',
27
+ 'view-timeline-name',
28
+ 'font-palette',
29
+
30
+ // Shorthand properties
31
+ 'scroll-timeline',
32
+ 'animation-timeline',
33
+ 'view-timeline',
34
+ ])
15
35
 
16
36
  // This is not a data type, but rather a function that can normalize the
17
37
  // correct values.
18
- export function normalize(value, isRoot = true) {
19
- if (value.startsWith('--')) {
38
+ export function normalize(value, context = null, isRoot = true) {
39
+ let isVarException = context && AUTO_VAR_INJECTION_EXCEPTIONS.has(context.property)
40
+ if (value.startsWith('--') && !isVarException) {
20
41
  return `var(${value})`
21
42
  }
22
43
 
@@ -30,7 +51,7 @@ export function normalize(value, isRoot = true) {
30
51
  return part
31
52
  }
32
53
 
33
- return normalize(part, false)
54
+ return normalize(part, context, false)
34
55
  })
35
56
  .join('')
36
57
  }
@@ -62,16 +83,67 @@ export function normalize(value, isRoot = true) {
62
83
  * @returns {string}
63
84
  */
64
85
  function normalizeMathOperatorSpacing(value) {
86
+ let preventFormattingInFunctions = ['theme']
87
+
65
88
  return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
66
- let vars = []
89
+ let result = ''
67
90
 
68
- return match
69
- .replace(/var\((--.+?)[,)]/g, (match, g1) => {
70
- vars.push(g1)
71
- return match.replace(g1, placeholder)
72
- })
73
- .replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ')
74
- .replace(placeholderRe, () => vars.shift())
91
+ function lastChar() {
92
+ let char = result.trimEnd()
93
+ return char[char.length - 1]
94
+ }
95
+
96
+ for (let i = 0; i < match.length; i++) {
97
+ function peek(word) {
98
+ return word.split('').every((char, j) => match[i + j] === char)
99
+ }
100
+
101
+ function consumeUntil(chars) {
102
+ let minIndex = Infinity
103
+ for (let char of chars) {
104
+ let index = match.indexOf(char, i)
105
+ if (index !== -1 && index < minIndex) {
106
+ minIndex = index
107
+ }
108
+ }
109
+
110
+ let result = match.slice(i, minIndex)
111
+ i += result.length - 1
112
+ return result
113
+ }
114
+
115
+ let char = match[i]
116
+
117
+ // Handle `var(--variable)`
118
+ if (peek('var')) {
119
+ // When we consume until `)`, then we are dealing with this scenario:
120
+ // `var(--example)`
121
+ //
122
+ // When we consume until `,`, then we are dealing with this scenario:
123
+ // `var(--example, 1rem)`
124
+ //
125
+ // In this case we do want to "format", the default value as well
126
+ result += consumeUntil([')', ','])
127
+ }
128
+
129
+ // Skip formatting inside known functions
130
+ else if (preventFormattingInFunctions.some((fn) => peek(fn))) {
131
+ result += consumeUntil([')'])
132
+ }
133
+
134
+ // Handle operators
135
+ else if (
136
+ ['+', '-', '*', '/'].includes(char) &&
137
+ !['(', '+', '-', '*', '/'].includes(lastChar())
138
+ ) {
139
+ result += ` ${char} `
140
+ } else {
141
+ result += char
142
+ }
143
+ }
144
+
145
+ // Simplify multiple spaces
146
+ return result.replace(/\s+/g, ' ')
75
147
  })
76
148
  }
77
149
 
@@ -3,6 +3,7 @@ import unescape from 'postcss-selector-parser/dist/util/unesc'
3
3
  import escapeClassName from '../util/escapeClassName'
4
4
  import prefixSelector from '../util/prefixSelector'
5
5
  import { movePseudos } from './pseudoElements'
6
+ import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
6
7
 
7
8
  /** @typedef {import('postcss-selector-parser').Root} Root */
8
9
  /** @typedef {import('postcss-selector-parser').Selector} Selector */
@@ -160,7 +161,7 @@ export function finalizeSelector(current, formats, { context, candidate, base })
160
161
  // │ │ │ ╰── We will not split here
161
162
  // ╰──┴─────┴─────────────── We will split here
162
163
  //
163
- base = base ?? candidate.split(new RegExp(`\\${separator}(?![^[]*\\])`)).pop()
164
+ base = base ?? splitAtTopLevelOnly(candidate, separator).pop()
164
165
 
165
166
  // Parse the selector into an AST
166
167
  let selector = selectorParser().astSync(current)
@@ -185,6 +186,13 @@ export function finalizeSelector(current, formats, { context, candidate, base })
185
186
  // Remove extraneous selectors that do not include the base candidate
186
187
  selector.each((sel) => eliminateIrrelevantSelectors(sel, base))
187
188
 
189
+ // If ffter eliminating irrelevant selectors, we end up with nothing
190
+ // Then the whole "rule" this is associated with does not need to exist
191
+ // We use `null` as a marker value for that case
192
+ if (selector.length === 0) {
193
+ return null
194
+ }
195
+
188
196
  // If there are no formats that means there were no variants added to the candidate
189
197
  // so we can just return the selector as-is
190
198
  let formatAst = Array.isArray(formats)
@@ -4,5 +4,5 @@ export default function isPlainObject(value) {
4
4
  }
5
5
 
6
6
  const prototype = Object.getPrototypeOf(value)
7
- return prototype === null || prototype === Object.prototype
7
+ return prototype === null || Object.getPrototypeOf(prototype) === null
8
8
  }
package/types/config.d.ts CHANGED
@@ -46,13 +46,13 @@ type PrefixConfig = string
46
46
  type SeparatorConfig = string
47
47
 
48
48
  // Safelist related config
49
- type SafelistConfig = (string | { pattern: RegExp; variants?: string[] })[]
49
+ type SafelistConfig = string | { pattern: RegExp; variants?: string[] }
50
50
 
51
51
  // Blocklist related config
52
- type BlocklistConfig = string[]
52
+ type BlocklistConfig = string
53
53
 
54
54
  // Presets related config
55
- type PresetsConfig = Config[]
55
+ type PresetsConfig = Partial<Config>
56
56
 
57
57
  // Future related config
58
58
  type FutureConfigValues =
@@ -352,9 +352,9 @@ interface OptionalConfig {
352
352
  important: Partial<ImportantConfig>
353
353
  prefix: Partial<PrefixConfig>
354
354
  separator: Partial<SeparatorConfig>
355
- safelist: Partial<SafelistConfig>
356
- blocklist: Partial<BlocklistConfig>
357
- presets: Partial<PresetsConfig>
355
+ safelist: Array<SafelistConfig>
356
+ blocklist: Array<BlocklistConfig>
357
+ presets: Array<PresetsConfig>
358
358
  future: Partial<FutureConfig>
359
359
  experimental: Partial<ExperimentalConfig>
360
360
  darkMode: Partial<DarkModeConfig>
package/types/index.d.ts CHANGED
@@ -1,7 +1,11 @@
1
- import { PluginCreator } from 'postcss'
1
+ import type { PluginCreator } from 'postcss'
2
2
  import type { Config } from './config.d'
3
3
 
4
4
  declare const plugin: PluginCreator<string | Config | { config: string | Config }>
5
5
 
6
- export { Config }
7
- export default plugin
6
+ declare type _Config = Config
7
+ declare namespace plugin {
8
+ export type { _Config as Config }
9
+ }
10
+
11
+ export = plugin