tailwindcss-patch 9.3.6 → 9.4.0

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.
@@ -0,0 +1,432 @@
1
+ import type { SourceEntry } from '@tailwindcss/oxide'
2
+ import type { TailwindV4CompiledSourceRoot, TailwindV4SourcePattern } from './types'
3
+ import { realpathSync } from 'node:fs'
4
+ import { stat } from 'node:fs/promises'
5
+ import process from 'node:process'
6
+ import path from 'pathe'
7
+ import micromatch from 'micromatch'
8
+
9
+ export const TAILWIND_V4_IGNORED_CONTENT_DIRS = [
10
+ '.git',
11
+ '.hg',
12
+ '.jj',
13
+ '.next',
14
+ '.parcel-cache',
15
+ '.pnpm-store',
16
+ '.svelte-kit',
17
+ '.svn',
18
+ '.turbo',
19
+ '.venv',
20
+ '.vercel',
21
+ '.yarn',
22
+ '__pycache__',
23
+ 'node_modules',
24
+ 'venv',
25
+ ]
26
+
27
+ export const TAILWIND_V4_IGNORED_EXTENSIONS = [
28
+ 'less',
29
+ 'lock',
30
+ 'sass',
31
+ 'scss',
32
+ 'styl',
33
+ 'log',
34
+ ]
35
+
36
+ export const TAILWIND_V4_IGNORED_FILES = [
37
+ 'package-lock.json',
38
+ 'pnpm-lock.yaml',
39
+ 'bun.lockb',
40
+ '.gitignore',
41
+ '.env',
42
+ '.env.*',
43
+ ]
44
+
45
+ export const TAILWIND_V4_AUTO_SOURCE_SCAN_PATTERN = '**/*'
46
+
47
+ function uniqueResolvedPaths(values: Iterable<string | undefined>) {
48
+ const result: string[] = []
49
+ for (const value of values) {
50
+ if (!value) {
51
+ continue
52
+ }
53
+ const resolved = path.resolve(value)
54
+ if (!result.includes(resolved)) {
55
+ result.push(resolved)
56
+ }
57
+ }
58
+ return result
59
+ }
60
+
61
+ export function toPosixPath(value: string) {
62
+ return value.replaceAll(path.sep, '/')
63
+ }
64
+
65
+ export function resolveSourceScanPath(value: string) {
66
+ const resolved = path.resolve(value)
67
+ try {
68
+ return realpathSync.native(resolved)
69
+ }
70
+ catch {
71
+ return resolved
72
+ }
73
+ }
74
+
75
+ export function normalizeGlobPattern(pattern: string) {
76
+ return pattern.startsWith('./') ? pattern.slice(2) : pattern
77
+ }
78
+
79
+ function hasGlobMagic(value: string) {
80
+ return /[*?[\]{}()!+@]/.test(value)
81
+ }
82
+
83
+ function splitStaticGlobPrefix(pattern: string) {
84
+ const normalized = normalizeGlobPattern(pattern)
85
+ const segments = normalized.split(/[\\/]+/)
86
+ const prefix: string[] = []
87
+ const rest: string[] = []
88
+ let reachedGlob = false
89
+
90
+ for (const segment of segments) {
91
+ if (!reachedGlob && segment && !hasGlobMagic(segment)) {
92
+ prefix.push(segment)
93
+ continue
94
+ }
95
+ reachedGlob = true
96
+ rest.push(segment)
97
+ }
98
+
99
+ return {
100
+ prefix,
101
+ rest,
102
+ }
103
+ }
104
+
105
+ async function pathExistsAsDirectory(file: string) {
106
+ try {
107
+ return (await stat(file)).isDirectory()
108
+ }
109
+ catch {
110
+ return false
111
+ }
112
+ }
113
+
114
+ export function createTailwindV4DefaultIgnoreSources(base: string): TailwindV4SourcePattern[] {
115
+ return [
116
+ ...TAILWIND_V4_IGNORED_CONTENT_DIRS.map(pattern => ({
117
+ base,
118
+ pattern: `**/${pattern}/**`,
119
+ negated: true,
120
+ })),
121
+ ...TAILWIND_V4_IGNORED_EXTENSIONS.map(extension => ({
122
+ base,
123
+ pattern: `**/*.${extension}`,
124
+ negated: true,
125
+ })),
126
+ ...TAILWIND_V4_IGNORED_FILES.map(pattern => ({
127
+ base,
128
+ pattern: `**/${pattern}`,
129
+ negated: true,
130
+ })),
131
+ ]
132
+ }
133
+
134
+ export function createTailwindV4RootSources(
135
+ root: TailwindV4CompiledSourceRoot,
136
+ fallbackBase: string,
137
+ ): TailwindV4SourcePattern[] {
138
+ if (root === 'none') {
139
+ return []
140
+ }
141
+ if (root === null) {
142
+ return [{ base: fallbackBase, pattern: TAILWIND_V4_AUTO_SOURCE_SCAN_PATTERN, negated: false }]
143
+ }
144
+ return [{ ...root, negated: false }]
145
+ }
146
+
147
+ export function createTailwindV4CompiledSourceEntries(
148
+ root: TailwindV4CompiledSourceRoot,
149
+ sources: TailwindV4SourcePattern[],
150
+ fallbackBase: string,
151
+ ) {
152
+ return [
153
+ ...createTailwindV4RootSources(root, fallbackBase),
154
+ ...sources,
155
+ ]
156
+ }
157
+
158
+ export async function resolveTailwindV4SourceEntry(
159
+ sourcePath: string,
160
+ base: string,
161
+ negated: boolean,
162
+ defaultPattern = TAILWIND_V4_AUTO_SOURCE_SCAN_PATTERN,
163
+ ): Promise<TailwindV4SourcePattern> {
164
+ const absoluteSource = path.isAbsolute(sourcePath) ? path.resolve(sourcePath) : path.resolve(base, sourcePath)
165
+
166
+ if (await pathExistsAsDirectory(absoluteSource)) {
167
+ return {
168
+ base: absoluteSource,
169
+ negated,
170
+ pattern: normalizeGlobPattern(defaultPattern),
171
+ }
172
+ }
173
+
174
+ if (path.isAbsolute(sourcePath)) {
175
+ return {
176
+ base: path.dirname(absoluteSource),
177
+ negated,
178
+ pattern: normalizeGlobPattern(path.basename(absoluteSource)),
179
+ }
180
+ }
181
+
182
+ const { prefix, rest } = splitStaticGlobPrefix(sourcePath)
183
+ if (prefix.length > 0 && rest.length > 0) {
184
+ return {
185
+ base: path.resolve(base, ...prefix),
186
+ negated,
187
+ pattern: normalizeGlobPattern(rest.join('/')),
188
+ }
189
+ }
190
+
191
+ return {
192
+ base,
193
+ negated,
194
+ pattern: normalizeGlobPattern(sourcePath),
195
+ }
196
+ }
197
+
198
+ export async function normalizeTailwindV4SourceEntries(
199
+ sources: TailwindV4SourcePattern[],
200
+ options: {
201
+ cwd?: string
202
+ defaultPattern?: string
203
+ } = {},
204
+ ) {
205
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd()
206
+ return Promise.all(sources.map(source =>
207
+ resolveTailwindV4SourceEntry(
208
+ source.pattern,
209
+ source.base ? path.resolve(source.base) : cwd,
210
+ source.negated,
211
+ options.defaultPattern,
212
+ )))
213
+ }
214
+
215
+ function expandBracePattern(pattern: string): string[] {
216
+ const index = pattern.indexOf('{')
217
+ if (index === -1) {
218
+ return [pattern]
219
+ }
220
+
221
+ const rest = pattern.slice(index)
222
+ let depth = 0
223
+ let endIndex = -1
224
+ for (let i = 0; i < rest.length; i++) {
225
+ const char = rest[i]
226
+ if (char === '\\') {
227
+ i += 1
228
+ continue
229
+ }
230
+ if (char === '{') {
231
+ depth += 1
232
+ continue
233
+ }
234
+ if (char === '}') {
235
+ depth -= 1
236
+ if (depth === 0) {
237
+ endIndex = i
238
+ break
239
+ }
240
+ }
241
+ }
242
+ if (endIndex === -1) {
243
+ return [pattern]
244
+ }
245
+
246
+ const prefix = pattern.slice(0, index)
247
+ const inner = rest.slice(1, endIndex)
248
+ const suffix = rest.slice(endIndex + 1)
249
+ const parts: string[] = []
250
+ const stack: string[] = []
251
+ let lastPos = 0
252
+ for (let i = 0; i < inner.length; i++) {
253
+ const char = inner[i]
254
+ if (char === '\\') {
255
+ i += 1
256
+ continue
257
+ }
258
+ if (char === '{') {
259
+ stack.push('}')
260
+ continue
261
+ }
262
+ if (char === '}' && stack[stack.length - 1] === '}') {
263
+ stack.pop()
264
+ continue
265
+ }
266
+ if (char === ',' && stack.length === 0) {
267
+ parts.push(inner.slice(lastPos, i))
268
+ lastPos = i + 1
269
+ }
270
+ }
271
+ parts.push(inner.slice(lastPos))
272
+
273
+ return parts.flatMap(part =>
274
+ expandBracePattern(`${prefix}${part}${suffix}`))
275
+ }
276
+
277
+ export function expandTailwindV4SourceEntryBraces(sources: TailwindV4SourcePattern[]): TailwindV4SourcePattern[] {
278
+ return sources.flatMap((source) => {
279
+ const base = path.resolve(source.base)
280
+ return expandBracePattern(source.pattern).map(pattern => ({
281
+ base,
282
+ pattern,
283
+ negated: source.negated,
284
+ }))
285
+ })
286
+ }
287
+
288
+ export function normalizeTailwindV4ScannerSources(
289
+ sources: TailwindV4SourcePattern[] | undefined,
290
+ cwd: string,
291
+ ignoredSources: TailwindV4SourcePattern[] = [],
292
+ ): SourceEntry[] {
293
+ const baseSources = sources?.length
294
+ ? sources
295
+ : [
296
+ {
297
+ base: cwd,
298
+ pattern: TAILWIND_V4_AUTO_SOURCE_SCAN_PATTERN,
299
+ negated: false,
300
+ },
301
+ ]
302
+
303
+ return expandTailwindV4SourceEntryBraces([...baseSources, ...ignoredSources])
304
+ }
305
+
306
+ function normalizeEntryPattern(entry: TailwindV4SourcePattern) {
307
+ return path.isAbsolute(entry.pattern)
308
+ ? toPosixPath(path.relative(resolveSourceScanPath(entry.base), entry.pattern))
309
+ : normalizeGlobPattern(entry.pattern)
310
+ }
311
+
312
+ function isFileMatchedByTailwindV4SourceEntry(file: string, entry: TailwindV4SourcePattern) {
313
+ const relative = toPosixPath(path.relative(resolveSourceScanPath(entry.base), file))
314
+ return Boolean(relative)
315
+ && !relative.startsWith('../')
316
+ && !path.isAbsolute(relative)
317
+ && micromatch.isMatch(relative, normalizeEntryPattern(entry))
318
+ }
319
+
320
+ export function isFileExcludedByTailwindV4SourceEntries(
321
+ file: string,
322
+ entries: TailwindV4SourcePattern[] | undefined,
323
+ ) {
324
+ if (!entries?.length) {
325
+ return false
326
+ }
327
+ const resolvedFile = resolveSourceScanPath(file)
328
+ return entries.some(entry => entry.negated && isFileMatchedByTailwindV4SourceEntry(resolvedFile, entry))
329
+ }
330
+
331
+ export function isFileMatchedByTailwindV4SourceEntries(
332
+ file: string,
333
+ entries: TailwindV4SourcePattern[] | undefined,
334
+ ) {
335
+ if (!entries?.length) {
336
+ return true
337
+ }
338
+
339
+ const positiveEntries = entries.filter(entry => !entry.negated)
340
+ const negativeEntries = entries.filter(entry => entry.negated)
341
+ if (positiveEntries.length === 0) {
342
+ return false
343
+ }
344
+
345
+ const resolvedFile = resolveSourceScanPath(file)
346
+ const matchesPositive = positiveEntries.some(entry => isFileMatchedByTailwindV4SourceEntry(resolvedFile, entry))
347
+ if (!matchesPositive) {
348
+ return false
349
+ }
350
+
351
+ return !negativeEntries.some(entry => isFileMatchedByTailwindV4SourceEntry(resolvedFile, entry))
352
+ }
353
+
354
+ export function createTailwindV4SourceEntryMatcher(entries: TailwindV4SourcePattern[] | undefined) {
355
+ if (!entries?.length) {
356
+ return undefined
357
+ }
358
+ return (file: string) => isFileMatchedByTailwindV4SourceEntries(file, entries)
359
+ }
360
+
361
+ export function createTailwindV4SourceExclusionMatcher(entries: TailwindV4SourcePattern[] | undefined) {
362
+ if (!entries?.length) {
363
+ return undefined
364
+ }
365
+ return (file: string) => isFileExcludedByTailwindV4SourceEntries(file, entries)
366
+ }
367
+
368
+ export function groupTailwindV4SourceEntriesByBase(entries: TailwindV4SourcePattern[]) {
369
+ const entriesByBase = new Map<string, TailwindV4SourcePattern[]>()
370
+ for (const entry of entries) {
371
+ const base = path.resolve(entry.base)
372
+ const group = entriesByBase.get(base) ?? []
373
+ group.push({
374
+ ...entry,
375
+ base,
376
+ pattern: normalizeGlobPattern(entry.pattern),
377
+ })
378
+ entriesByBase.set(base, group)
379
+ }
380
+ return entriesByBase
381
+ }
382
+
383
+ export async function expandTailwindV4SourceEntries(
384
+ entries: TailwindV4SourcePattern[],
385
+ resolveFiles: (options: { cwd: string, sources: TailwindV4SourcePattern[] }) => Promise<string[]>,
386
+ ) {
387
+ if (entries.length === 0) {
388
+ return []
389
+ }
390
+
391
+ const files = new Set<string>()
392
+ await Promise.all([...groupTailwindV4SourceEntriesByBase(entries).entries()].map(async ([base, group]) => {
393
+ const matched = await resolveFiles({
394
+ cwd: base,
395
+ sources: group,
396
+ })
397
+ for (const file of matched) {
398
+ files.add(path.resolve(file))
399
+ }
400
+ }))
401
+
402
+ return [...files].filter(file => !isFileExcludedByTailwindV4SourceEntries(file, entries))
403
+ }
404
+
405
+ export function mergeTailwindV4SourceEntries(...entries: Array<TailwindV4SourcePattern[] | undefined>) {
406
+ const result: TailwindV4SourcePattern[] = []
407
+ const seen = new Set<string>()
408
+ for (const group of entries) {
409
+ for (const entry of group ?? []) {
410
+ const normalized = {
411
+ base: path.resolve(entry.base),
412
+ pattern: normalizeGlobPattern(entry.pattern),
413
+ negated: entry.negated,
414
+ }
415
+ const key = JSON.stringify(normalized)
416
+ if (seen.has(key)) {
417
+ continue
418
+ }
419
+ seen.add(key)
420
+ result.push(normalized)
421
+ }
422
+ }
423
+ return result
424
+ }
425
+
426
+ export function resolveTailwindV4SourceBaseCandidates(
427
+ projectRoot: string,
428
+ base: string,
429
+ baseFallbacks: string[],
430
+ ) {
431
+ return uniqueResolvedPaths([base, projectRoot, ...baseFallbacks])
432
+ }
package/src/v4/types.ts CHANGED
@@ -47,6 +47,11 @@ export interface TailwindV4GenerateOptions {
47
47
  scanSources?: boolean | TailwindV4SourcePattern[]
48
48
  }
49
49
 
50
+ export type TailwindV4CompiledSourceRoot = null | 'none' | {
51
+ base: string
52
+ pattern: string
53
+ }
54
+
50
55
  export interface TailwindV4SourcePattern {
51
56
  base: string
52
57
  pattern: string
@@ -59,10 +64,7 @@ export interface TailwindV4GenerateResult {
59
64
  rawCandidates: Set<string>
60
65
  dependencies: string[]
61
66
  sources: TailwindV4SourcePattern[]
62
- root: null | 'none' | {
63
- base: string
64
- pattern: string
65
- }
67
+ root: TailwindV4CompiledSourceRoot
66
68
  }
67
69
 
68
70
  export interface TailwindV4DesignSystem {