tailwindcss-patch 9.3.1 → 9.3.3

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.
@@ -39,7 +39,8 @@ const DEFAULT_BARE_ARBITRARY_VALUE_UNITS = [
39
39
  ]
40
40
 
41
41
  const NUMBER_RE = /^-?(?:\d+|\d*\.\d+)$/
42
- const FUNCTION_VALUE_RE = /^[a-zA-Z_-][a-zA-Z0-9_-]*\(/
42
+ const FUNCTION_VALUE_RE = /^[a-z_-][\w-]*\(/i
43
+ const HEX_ESCAPE_RE = /^[\da-f]$/i
43
44
 
44
45
  function splitVariantPrefix(candidate: string) {
45
46
  let depth = 0
@@ -133,8 +134,55 @@ function isBalancedFunctionValue(value: string) {
133
134
  return depth === 0 && quote === undefined
134
135
  }
135
136
 
137
+ function isEscapedAt(value: string, index: number) {
138
+ let slashCount = 0
139
+ for (let slashIndex = index - 1; slashIndex >= 0 && value[slashIndex] === '\\'; slashIndex--) {
140
+ slashCount++
141
+ }
142
+ return slashCount % 2 === 1
143
+ }
144
+
145
+ function isBalancedBareArbitraryBody(value: string) {
146
+ let depth = 0
147
+ let quote: string | undefined
148
+
149
+ for (let index = 0; index < value.length; index++) {
150
+ const character = value[index]
151
+
152
+ if (isEscapedAt(value, index)) {
153
+ continue
154
+ }
155
+
156
+ if (quote) {
157
+ if (character === quote) {
158
+ quote = undefined
159
+ }
160
+ continue
161
+ }
162
+
163
+ if (character === '"' || character === '\'') {
164
+ quote = character
165
+ continue
166
+ }
167
+
168
+ if (character === '(' || character === '{') {
169
+ depth++
170
+ continue
171
+ }
172
+
173
+ if (character === ')' || character === '}') {
174
+ depth--
175
+ if (depth < 0) {
176
+ return false
177
+ }
178
+ }
179
+ }
180
+
181
+ return depth === 0 && quote === undefined
182
+ }
183
+
136
184
  function isHexColorValue(value: string) {
137
- return /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6,8})$/.test(value)
185
+ return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6,8})$/i.test(value)
138
186
  }
139
187
 
140
188
  function isQuotedValue(value: string) {
@@ -173,12 +221,55 @@ function normalizeBareArbitraryValueOptions(options: boolean | BareArbitraryValu
173
221
  }
174
222
  }
175
223
 
224
+ function normalizeEscapedValue(value: string) {
225
+ let result = ''
226
+ for (let index = 0; index < value.length; index++) {
227
+ const character = value[index]
228
+ if (character !== '\\') {
229
+ result += character
230
+ continue
231
+ }
232
+
233
+ const nextCharacter = value[index + 1]
234
+ if (nextCharacter === undefined) {
235
+ result += character
236
+ continue
237
+ }
238
+
239
+ if (HEX_ESCAPE_RE.test(nextCharacter)) {
240
+ let hex = ''
241
+ let nextIndex = index + 1
242
+ while (nextIndex < value.length && hex.length < 6) {
243
+ const hexCharacter = value[nextIndex]
244
+ if (hexCharacter === undefined || !HEX_ESCAPE_RE.test(hexCharacter)) {
245
+ break
246
+ }
247
+ hex += hexCharacter
248
+ nextIndex++
249
+ }
250
+ if (/[\t\n\f\r ]/.test(value[nextIndex] ?? '')) {
251
+ nextIndex++
252
+ }
253
+
254
+ const decoded = String.fromCodePoint(Number.parseInt(hex, 16))
255
+ result += decoded === '_' ? '\\_' : decoded
256
+ index = nextIndex - 1
257
+ continue
258
+ }
259
+
260
+ result += nextCharacter === '_' ? '\\_' : nextCharacter
261
+ index++
262
+ }
263
+ return result
264
+ }
265
+
176
266
  function resolveValueWithUnit(body: string, units: string[]) {
267
+ const value = normalizeEscapedValue(body)
177
268
  for (const unit of units) {
178
- if (!body.endsWith(unit)) {
269
+ if (!value.endsWith(unit)) {
179
270
  continue
180
271
  }
181
- const numberPart = body.slice(0, -unit.length)
272
+ const numberPart = value.slice(0, -unit.length)
182
273
  if (NUMBER_RE.test(numberPart)) {
183
274
  return `${numberPart}${unit}`
184
275
  }
@@ -186,21 +277,22 @@ function resolveValueWithUnit(body: string, units: string[]) {
186
277
  }
187
278
 
188
279
  function resolveArbitraryValue(body: string, units: string[]) {
189
- const withUnit = resolveValueWithUnit(body, units)
280
+ const value = normalizeEscapedValue(body)
281
+ const withUnit = resolveValueWithUnit(value, units)
190
282
  if (withUnit) {
191
283
  return withUnit
192
284
  }
193
285
 
194
- if (isHexColorValue(body)) {
195
- return body
286
+ if (isHexColorValue(value)) {
287
+ return value
196
288
  }
197
289
 
198
- if (isQuotedValue(body)) {
199
- return body
290
+ if (isQuotedValue(value)) {
291
+ return value
200
292
  }
201
293
 
202
- if (FUNCTION_VALUE_RE.test(body) && body.endsWith(')') && isBalancedFunctionValue(body)) {
203
- return body
294
+ if (FUNCTION_VALUE_RE.test(value) && value.endsWith(')') && isBalancedFunctionValue(value)) {
295
+ return value
204
296
  }
205
297
  }
206
298
 
@@ -208,11 +300,10 @@ function resolveUtilityAndValue(body: string, units: string[]) {
208
300
  let depth = 0
209
301
  let quote: string | undefined
210
302
 
211
- for (let index = 1; index < body.length; index++) {
303
+ for (let index = body.length - 1; index > 0; index--) {
212
304
  const character = body[index]
213
305
 
214
- if (character === '\\') {
215
- index++
306
+ if (isEscapedAt(body, index)) {
216
307
  continue
217
308
  }
218
309
 
@@ -228,12 +319,12 @@ function resolveUtilityAndValue(body: string, units: string[]) {
228
319
  continue
229
320
  }
230
321
 
231
- if (character === '(' || character === '{') {
322
+ if (character === ')' || character === '}') {
232
323
  depth++
233
324
  continue
234
325
  }
235
326
 
236
- if (character === ')' || character === '}') {
327
+ if (character === '(' || character === '{') {
237
328
  depth = Math.max(0, depth - 1)
238
329
  continue
239
330
  }
@@ -274,6 +365,9 @@ export function resolveBareArbitraryValueCandidate(
274
365
  if (negative) {
275
366
  normalizedBody = normalizedBody.slice(1)
276
367
  }
368
+ if (!isBalancedBareArbitraryBody(normalizedBody)) {
369
+ return
370
+ }
277
371
 
278
372
  const resolved = resolveUtilityAndValue(normalizedBody, normalizedOptions.units)
279
373
  if (!resolved) {
@@ -1,10 +1,7 @@
1
+ import type { BareArbitraryValueOptions } from './bare-arbitrary-values'
1
2
  import type { TailwindV4DesignSystem } from './types'
2
3
  import postcss from 'postcss'
3
- import {
4
- escapeCssClassName,
5
- type BareArbitraryValueOptions,
6
- resolveBareArbitraryValueCandidate,
7
- } from './bare-arbitrary-values'
4
+ import { escapeCssClassName, resolveBareArbitraryValueCandidate } from './bare-arbitrary-values'
8
5
 
9
6
  export function resolveValidTailwindV4Candidates(
10
7
  designSystem: TailwindV4DesignSystem,
@@ -15,7 +12,7 @@ export function resolveValidTailwindV4Candidates(
15
12
  ): Set<string> {
16
13
  const validCandidates = new Set<string>()
17
14
  const parsedCandidates: string[] = []
18
- const originalCandidateByCanonical = new Map<string, string>()
15
+ const originalCandidatesByCanonical = new Map<string, Set<string>>()
19
16
 
20
17
  for (const candidate of candidates) {
21
18
  if (!candidate) {
@@ -25,15 +22,19 @@ export function resolveValidTailwindV4Candidates(
25
22
  const bareArbitrary = resolveBareArbitraryValueCandidate(candidate, options?.bareArbitraryValues)
26
23
  const candidateToCheck = bareArbitrary?.canonicalCandidate ?? candidate
27
24
 
28
- if (parsedCandidates.includes(candidateToCheck)) {
25
+ if (bareArbitrary) {
26
+ const originalCandidates = originalCandidatesByCanonical.get(candidateToCheck) ?? new Set<string>()
27
+ originalCandidates.add(candidate)
28
+ originalCandidatesByCanonical.set(candidateToCheck, originalCandidates)
29
+ }
30
+
31
+ const alreadyParsed = parsedCandidates.includes(candidateToCheck)
32
+ if (alreadyParsed) {
29
33
  continue
30
34
  }
31
35
 
32
36
  if (designSystem.parseCandidate(candidateToCheck).length > 0) {
33
37
  parsedCandidates.push(candidateToCheck)
34
- if (bareArbitrary) {
35
- originalCandidateByCanonical.set(candidateToCheck, candidate)
36
- }
37
38
  }
38
39
  }
39
40
 
@@ -46,7 +47,14 @@ export function resolveValidTailwindV4Candidates(
46
47
  const candidate = parsedCandidates[index]
47
48
  const candidateCss = cssByCandidate[index]
48
49
  if (candidate && typeof candidateCss === 'string' && candidateCss.trim().length > 0) {
49
- validCandidates.add(originalCandidateByCanonical.get(candidate) ?? candidate)
50
+ const originalCandidates = originalCandidatesByCanonical.get(candidate)
51
+ if (originalCandidates) {
52
+ for (const originalCandidate of originalCandidates) {
53
+ validCandidates.add(originalCandidate)
54
+ }
55
+ continue
56
+ }
57
+ validCandidates.add(candidate)
50
58
  }
51
59
  }
52
60
 
@@ -57,16 +65,16 @@ function createSelectorAliasMap(
57
65
  candidates: Iterable<string>,
58
66
  options?: boolean | BareArbitraryValueOptions,
59
67
  ) {
60
- const aliases = new Map<string, string>()
68
+ const aliases = new Map<string, Set<string>>()
61
69
  for (const candidate of candidates) {
62
70
  const bareArbitrary = resolveBareArbitraryValueCandidate(candidate, options)
63
71
  if (!bareArbitrary) {
64
72
  continue
65
73
  }
66
- aliases.set(
67
- escapeCssClassName(bareArbitrary.canonicalCandidate),
68
- escapeCssClassName(bareArbitrary.candidate),
69
- )
74
+ const canonicalSelector = escapeCssClassName(bareArbitrary.canonicalCandidate)
75
+ const bareSelectors = aliases.get(canonicalSelector) ?? new Set<string>()
76
+ bareSelectors.add(escapeCssClassName(bareArbitrary.candidate))
77
+ aliases.set(canonicalSelector, bareSelectors)
70
78
  }
71
79
  return aliases
72
80
  }
@@ -81,11 +89,31 @@ export function replaceBareArbitraryValueSelectors(
81
89
  return css
82
90
  }
83
91
 
84
- let result = css
85
- for (const [canonicalSelector, bareSelector] of aliases) {
86
- result = result.replaceAll(canonicalSelector, bareSelector)
92
+ if (Array.from(aliases.values()).every(bareSelectors => bareSelectors.size === 1)) {
93
+ let result = css
94
+ for (const [canonicalSelector, bareSelectors] of aliases) {
95
+ const bareSelector = Array.from(bareSelectors)[0]
96
+ if (bareSelector !== undefined) {
97
+ result = result.replaceAll(canonicalSelector, bareSelector)
98
+ }
99
+ }
100
+ return result
87
101
  }
88
- return result
102
+
103
+ const root = postcss.parse(css)
104
+ root.walkRules((rule) => {
105
+ let selectors = rule.selectors
106
+ for (const [canonicalSelector, bareSelectors] of aliases) {
107
+ selectors = selectors.flatMap((selector) => {
108
+ if (!selector.includes(canonicalSelector)) {
109
+ return selector
110
+ }
111
+ return Array.from(bareSelectors, bareSelector => selector.replaceAll(canonicalSelector, bareSelector))
112
+ })
113
+ }
114
+ rule.selectors = selectors
115
+ })
116
+ return root.toString()
89
117
  }
90
118
 
91
119
  export function canonicalizeBareArbitraryValueCandidates(
package/src/v4/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  } from './source'
16
16
  export type {
17
17
  TailwindV4CandidateSource,
18
+ TailwindV4CssSource,
18
19
  TailwindV4DesignSystem,
19
20
  TailwindV4Engine,
20
21
  TailwindV4GenerateOptions,
package/src/v4/source.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { NormalizedTailwindCssPatchOptions, TailwindCssPatchOptions } from '../config'
2
- import type { TailwindV4ResolvedSource, TailwindV4SourceOptions } from './types'
2
+ import type { TailwindV4CssSource, TailwindV4ResolvedSource, TailwindV4SourceOptions } from './types'
3
3
  import { promises as fs } from 'node:fs'
4
4
  import process from 'node:process'
5
5
  import path from 'pathe'
@@ -87,6 +87,35 @@ async function resolveCssEntries(entries: string[], projectRoot: string, base: s
87
87
  }
88
88
  }
89
89
 
90
+ function resolveCssSources(sources: TailwindV4CssSource[], projectRoot: string, base: string | undefined) {
91
+ const resolvedSources = sources.map(source => ({
92
+ ...source,
93
+ base: source.base === undefined ? undefined : resolveBase(source.base, projectRoot),
94
+ file: source.file === undefined
95
+ ? undefined
96
+ : path.isAbsolute(source.file)
97
+ ? path.resolve(source.file)
98
+ : path.resolve(projectRoot, source.file),
99
+ dependencies: source.dependencies?.map(dependency =>
100
+ path.isAbsolute(dependency) ? path.resolve(dependency) : path.resolve(projectRoot, dependency),
101
+ ) ?? [],
102
+ }))
103
+ const firstSource = resolvedSources[0]
104
+ const resolvedBase = base
105
+ ?? firstSource?.base
106
+ ?? (firstSource?.file ? path.dirname(firstSource.file) : projectRoot)
107
+ const dependencies = resolvedSources.flatMap(source => [
108
+ source.file,
109
+ ...source.dependencies,
110
+ ]).filter((dependency): dependency is string => Boolean(dependency))
111
+
112
+ return {
113
+ base: resolvedBase,
114
+ css: resolvedSources.map(source => source.css).join('\n'),
115
+ dependencies,
116
+ }
117
+ }
118
+
90
119
  function normalizeResolvedSource(
91
120
  source: {
92
121
  projectRoot: string
@@ -129,15 +158,27 @@ export async function resolveTailwindV4Source(options: TailwindV4SourceOptions =
129
158
  })
130
159
  }
131
160
 
132
- if (options.cssEntries?.length) {
133
- const entries = await resolveCssEntries(options.cssEntries, projectRoot, configuredBase)
161
+ if (options.cssEntries?.length || options.cssSources?.length) {
162
+ const entries = options.cssEntries?.length
163
+ ? await resolveCssEntries(options.cssEntries, projectRoot, configuredBase)
164
+ : undefined
165
+ const sources = options.cssSources?.length
166
+ ? resolveCssSources(options.cssSources, projectRoot, configuredBase)
167
+ : undefined
168
+ const css = [
169
+ entries?.css,
170
+ sources?.css,
171
+ ].filter(Boolean).join('\n')
134
172
  return normalizeResolvedSource({
135
173
  projectRoot,
136
174
  cwd,
137
- base: entries.base,
175
+ base: configuredBase ?? entries?.base ?? sources?.base ?? cwd,
138
176
  baseFallbacks,
139
- css: entries.css,
140
- dependencies: entries.dependencies,
177
+ css,
178
+ dependencies: [
179
+ ...(entries?.dependencies ?? []),
180
+ ...(sources?.dependencies ?? []),
181
+ ],
141
182
  })
142
183
  }
143
184
 
@@ -177,6 +218,7 @@ function createSourceOptionsFromNormalizedPatchOptions(
177
218
  ...(v4?.configuredBase === undefined ? {} : { base: v4.configuredBase }),
178
219
  baseFallbacks,
179
220
  ...(v4?.css === undefined ? {} : { css: v4.css }),
221
+ ...(v4?.cssSources === undefined ? {} : { cssSources: v4.cssSources }),
180
222
  ...(v4?.cssEntries === undefined ? {} : { cssEntries: v4.cssEntries }),
181
223
  packageName: options.tailwind.packageName,
182
224
  }
package/src/v4/types.ts CHANGED
@@ -4,10 +4,18 @@ export interface TailwindV4SourceOptions {
4
4
  base?: string
5
5
  baseFallbacks?: string[]
6
6
  css?: string
7
+ cssSources?: TailwindV4CssSource[]
7
8
  cssEntries?: string[]
8
9
  packageName?: string
9
10
  }
10
11
 
12
+ export interface TailwindV4CssSource {
13
+ css: string
14
+ base?: string
15
+ file?: string
16
+ dependencies?: string[]
17
+ }
18
+
11
19
  export interface TailwindV4ResolvedSource {
12
20
  projectRoot: string
13
21
  base: string