tailwindcss-patch 9.4.4 → 9.5.1

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.
Files changed (36) hide show
  1. package/dist/{cli-CLvyx3xl.mjs → cli-Bv15iTiT.mjs} +1 -1
  2. package/dist/{cli-srv0kRlt.js → cli-Db2YAbkN.js} +3 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/cli.mjs +2 -2
  5. package/dist/commands/cli-runtime.d.mts +1 -1
  6. package/dist/commands/cli-runtime.d.ts +1 -1
  7. package/dist/commands/cli-runtime.js +2 -2
  8. package/dist/commands/cli-runtime.mjs +2 -2
  9. package/dist/{dist-Dn7cMVhi.js → dist-wp0o36Ns.js} +1 -1
  10. package/dist/index.d.mts +7 -149
  11. package/dist/index.d.ts +8 -150
  12. package/dist/index.js +294 -521
  13. package/dist/index.mjs +6 -471
  14. package/dist/{validate-oAkURzUC.d.mts → validate-B5-08lrU.d.ts} +8 -252
  15. package/dist/{validate-RpdgpjgT.js → validate-C8oLv32F.js} +79 -1705
  16. package/dist/{validate-BuqRodYI.d.ts → validate-CgrG4aAY.d.mts} +8 -252
  17. package/dist/{validate-CUNJFfHh.mjs → validate-Cu1G06lO.mjs} +5 -1402
  18. package/package.json +5 -7
  19. package/src/api/tailwindcss-patcher.ts +3 -2
  20. package/src/extraction/candidate-extractor.ts +17 -701
  21. package/src/extraction/split-candidate-tokens.ts +5 -101
  22. package/src/options/types.ts +1 -2
  23. package/src/style-candidates.ts +5 -35
  24. package/src/style-generator.ts +11 -80
  25. package/src/types.ts +21 -95
  26. package/src/v3/index.ts +2 -2
  27. package/src/v4/index.ts +104 -28
  28. package/src/v3/style-generator.ts +0 -384
  29. package/src/v4/bare-arbitrary-values.ts +0 -545
  30. package/src/v4/candidates.ts +0 -316
  31. package/src/v4/engine.ts +0 -112
  32. package/src/v4/node-adapter.ts +0 -207
  33. package/src/v4/source-scan.ts +0 -432
  34. package/src/v4/source.ts +0 -235
  35. package/src/v4/style-generator.ts +0 -44
  36. package/src/v4/types.ts +0 -103
@@ -1,701 +1,17 @@
1
- import type { SourceEntry } from '@tailwindcss/oxide'
2
- import type {
3
- TailwindTokenByFileMap,
4
- TailwindTokenFileKey,
5
- TailwindTokenLocation,
6
- TailwindTokenReport,
7
- } from '../types'
8
- import type { BareArbitraryValueOptions } from '../v4/bare-arbitrary-values'
9
- import { promises as fs } from 'node:fs'
10
- import process from 'node:process'
11
- import path from 'pathe'
12
- import {
13
- extractBareArbitraryValueSourceCandidatesWithPositions,
14
- resolveBareArbitraryValueCandidate,
15
- } from '../v4/bare-arbitrary-values'
16
- import { extractTailwindV4InlineSourceCandidates, resolveValidTailwindV4Candidates } from '../v4/candidates'
17
- import { compileTailwindV4Source, getTailwindV4DesignSystemCacheKey, loadTailwindV4DesignSystem } from '../v4/node-adapter'
18
- import {
19
- createTailwindV4CompiledSourceEntries,
20
- normalizeTailwindV4ScannerSources,
21
- } from '../v4/source-scan'
22
-
23
- let oxideImportPromise: ReturnType<typeof importOxide> | undefined
24
- const designSystemCandidateCache = new Map<string, Map<string, boolean>>()
25
-
26
- function createOxideRuntimeDependencyError(cause: unknown) {
27
- return new Error(
28
- [
29
- 'tailwindcss-patch could not load @tailwindcss/oxide, which is required for source candidate scanning.',
30
- 'This dependency should be installed automatically by tailwindcss-patch.',
31
- 'Reinstall dependencies without disabling optional dependencies, or install @tailwindcss/oxide@^4.2.4 manually if your package manager omitted it.',
32
- ].join(' '),
33
- { cause },
34
- )
35
- }
36
-
37
- async function importOxide() {
38
- try {
39
- return await import('@tailwindcss/oxide')
40
- }
41
- catch (error) {
42
- throw createOxideRuntimeDependencyError(error)
43
- }
44
- }
45
-
46
- function getOxideModule() {
47
- oxideImportPromise ??= importOxide()
48
- oxideImportPromise.catch(() => {
49
- oxideImportPromise = undefined
50
- })
51
- return oxideImportPromise
52
- }
53
-
54
- export interface ExtractValidCandidatesOption {
55
- sources?: SourceEntry[]
56
- base?: string
57
- baseFallbacks?: string[]
58
- css?: string
59
- cwd?: string
60
- bareArbitraryValues?: boolean | BareArbitraryValueOptions
61
- }
62
-
63
- export interface ExtractCandidateOptions {
64
- bareArbitraryValues?: boolean | BareArbitraryValueOptions
65
- }
66
-
67
- export interface ExtractSourceCandidate {
68
- rawCandidate: string
69
- start: number
70
- end: number
71
- }
72
-
73
- interface ExtractSourceCandidateWithContext extends ExtractSourceCandidate {
74
- content: string
75
- extension: string
76
- localStart: number
77
- }
78
-
79
- const HTML_ATTRIBUTE_NAME_CANDIDATE_RE = /^(?:class|className|hover-class|hoverClass)$/
80
- const CSS_DIRECTIVE_CANDIDATE_RE = /^@(?:apply|tailwind|source|config|plugin|theme|utility|custom-variant|variant)$/
81
- const CSS_APPLY_IMPORTANT = '!important'
82
- const CSS_APPLY_RE = /@apply\s+([^;{}]+)/g
83
- const JS_LIKE_SOURCE_EXTENSION_RE = /^[cm]?[jt]sx?$/
84
- const MIXED_TEMPLATE_SOURCE_EXTENSION_RE = /^(?:vue|uvue|nvue|svelte|mpx)$/
85
- const CSS_LIKE_SOURCE_EXTENSION_RE = /^(?:css|wxss|acss|jxss|ttss|qss|tyss|scss|sass|less|styl|stylus)$/
86
- const SFC_SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
87
-
88
- function isWhitespace(value: string | undefined) {
89
- return value === ' ' || value === '\n' || value === '\r' || value === '\t' || value === '\f'
90
- }
91
-
92
- function isHtmlAttributeNameCandidate(content: string, candidate: ExtractSourceCandidate) {
93
- if (!HTML_ATTRIBUTE_NAME_CANDIDATE_RE.test(candidate.rawCandidate)) {
94
- return false
95
- }
96
- let index = candidate.end
97
- while (isWhitespace(content[index])) {
98
- index++
99
- }
100
- return content[index] === '='
101
- }
102
-
103
- function isInsideHtmlTagText(content: string, candidate: ExtractSourceCandidate) {
104
- const open = content.lastIndexOf('<', candidate.start)
105
- const close = content.lastIndexOf('>', candidate.start)
106
- if (open > close) {
107
- return false
108
- }
109
- const nextOpen = content.indexOf('<', candidate.end)
110
- return nextOpen !== -1 && (nextOpen < content.indexOf('>', candidate.end) || !content.includes('>', candidate.end))
111
- }
112
-
113
- function isCssDirectiveCandidate(candidate: string) {
114
- return candidate === CSS_APPLY_IMPORTANT || CSS_DIRECTIVE_CANDIDATE_RE.test(candidate)
115
- }
116
-
117
- function isCandidateInCssApplyParams(content: string, candidate: ExtractSourceCandidate) {
118
- const apply = content.lastIndexOf('@apply', candidate.start)
119
- if (apply === -1) {
120
- return false
121
- }
122
- const boundary = content.slice(apply + '@apply'.length, candidate.start)
123
- return !/[;{}]/.test(boundary)
124
- }
125
-
126
- function isCandidateInsideJsStringStaticContent(content: string, start: number) {
127
- let quote: '"' | '\'' | '`' | undefined
128
- let templateExpressionDepth = 0
129
- for (let index = 0; index < start; index++) {
130
- const char = content[index]
131
- const next = content[index + 1]
132
-
133
- if (quote && char === '\\') {
134
- index++
135
- continue
136
- }
137
-
138
- if (quote === '`' && templateExpressionDepth > 0) {
139
- if (char === '"' || char === '\'') {
140
- const nestedQuote = char
141
- index++
142
- while (index < start) {
143
- const nestedChar = content[index]
144
- if (nestedChar === '\\') {
145
- index += 2
146
- continue
147
- }
148
- if (nestedChar === nestedQuote) {
149
- break
150
- }
151
- index++
152
- }
153
- continue
154
- }
155
- if (char === '`') {
156
- index++
157
- while (index < start) {
158
- const nestedChar = content[index]
159
- if (nestedChar === '\\') {
160
- index += 2
161
- continue
162
- }
163
- if (nestedChar === '`') {
164
- break
165
- }
166
- index++
167
- }
168
- continue
169
- }
170
- if (char === '{') {
171
- templateExpressionDepth++
172
- continue
173
- }
174
- if (char === '}') {
175
- templateExpressionDepth--
176
- continue
177
- }
178
- continue
179
- }
180
-
181
- if (quote) {
182
- if (quote === '`' && char === '$' && next === '{') {
183
- templateExpressionDepth = 1
184
- index++
185
- continue
186
- }
187
- if (char === quote) {
188
- quote = undefined
189
- }
190
- continue
191
- }
192
-
193
- if (char === '"' || char === '\'' || char === '`') {
194
- quote = char
195
- }
196
- }
197
-
198
- return quote !== undefined && templateExpressionDepth === 0
199
- }
200
-
201
- function shouldKeepSourceCandidate(content: string, extension: string, candidate: ExtractSourceCandidate) {
202
- if (!candidate.rawCandidate || isCssDirectiveCandidate(candidate.rawCandidate)) {
203
- return false
204
- }
205
- if (CSS_LIKE_SOURCE_EXTENSION_RE.test(extension) && !isCandidateInCssApplyParams(content, candidate)) {
206
- return false
207
- }
208
- if (isHtmlAttributeNameCandidate(content, candidate)) {
209
- return false
210
- }
211
- if (isInsideHtmlTagText(content, candidate)) {
212
- return false
213
- }
214
- if (
215
- JS_LIKE_SOURCE_EXTENSION_RE.test(extension)
216
- && !isCandidateInsideJsStringStaticContent(content, candidate.start)
217
- ) {
218
- return false
219
- }
220
- return true
221
- }
222
-
223
- function createLocalCandidate(candidate: ExtractSourceCandidateWithContext): ExtractSourceCandidate {
224
- return {
225
- rawCandidate: candidate.rawCandidate,
226
- start: candidate.localStart,
227
- end: candidate.localStart + candidate.rawCandidate.length,
228
- }
229
- }
230
-
231
- function dedupeCandidatesWithPositions(candidates: ExtractSourceCandidate[]) {
232
- const seen = new Set<string>()
233
- return candidates.filter((candidate) => {
234
- const key = `${candidate.start}:${candidate.end}:${candidate.rawCandidate}`
235
- if (seen.has(key)) {
236
- return false
237
- }
238
- seen.add(key)
239
- return true
240
- })
241
- }
242
-
243
- function createBareArbitraryValueCandidateContexts(
244
- content: string,
245
- extension: string,
246
- offset: number,
247
- options?: ExtractCandidateOptions,
248
- ): ExtractSourceCandidateWithContext[] {
249
- return extractBareArbitraryValueSourceCandidatesWithPositions(content, options?.bareArbitraryValues)
250
- .map(candidate => ({
251
- content,
252
- extension,
253
- localStart: candidate.start,
254
- rawCandidate: candidate.rawCandidate,
255
- start: candidate.start + offset,
256
- end: candidate.end + offset,
257
- }))
258
- }
259
-
260
- async function extractCssApplyCandidates(content: string, extension: string, options?: ExtractCandidateOptions) {
261
- const candidates: ExtractSourceCandidateWithContext[] = []
262
- CSS_APPLY_RE.lastIndex = 0
263
- let match = CSS_APPLY_RE.exec(content)
264
- while (match !== null) {
265
- const applyParams = match[1] ?? ''
266
- const applyParamsStart = match.index + match[0].indexOf(applyParams)
267
- const applyCandidates = await extractRawCandidatesWithPositions(applyParams, extension)
268
- candidates.push(...applyCandidates.map(candidate => ({
269
- content: applyParams,
270
- extension: 'html',
271
- localStart: candidate.start,
272
- rawCandidate: candidate.rawCandidate,
273
- start: candidate.start + applyParamsStart,
274
- end: candidate.end + applyParamsStart,
275
- })))
276
- candidates.push(...createBareArbitraryValueCandidateContexts(applyParams, 'html', applyParamsStart, options))
277
- match = CSS_APPLY_RE.exec(content)
278
- }
279
- return candidates
280
- }
281
-
282
- async function extractMixedSourceScriptCandidates(content: string, options?: ExtractCandidateOptions) {
283
- const candidates: ExtractSourceCandidateWithContext[] = []
284
- SFC_SCRIPT_BLOCK_RE.lastIndex = 0
285
- let match = SFC_SCRIPT_BLOCK_RE.exec(content)
286
- while (match !== null) {
287
- const scriptContent = match[1] ?? ''
288
- const scriptStart = match.index + match[0].indexOf(scriptContent)
289
- const scriptCandidates = await extractRawCandidatesWithPositions(scriptContent, 'js')
290
- candidates.push(...scriptCandidates.map(candidate => ({
291
- content: scriptContent,
292
- extension: 'js',
293
- localStart: candidate.start,
294
- rawCandidate: candidate.rawCandidate,
295
- start: candidate.start + scriptStart,
296
- end: candidate.end + scriptStart,
297
- })))
298
- candidates.push(...createBareArbitraryValueCandidateContexts(scriptContent, 'js', scriptStart, options))
299
- match = SFC_SCRIPT_BLOCK_RE.exec(content)
300
- }
301
- return candidates
302
- }
303
-
304
- function createCandidateCacheKey(
305
- designSystemKey: string,
306
- options: Pick<ExtractValidCandidatesOption, 'bareArbitraryValues'>,
307
- ) {
308
- if (options.bareArbitraryValues == null || options.bareArbitraryValues === false) {
309
- return designSystemKey
310
- }
311
- return `${designSystemKey}:bare-arbitrary:${JSON.stringify(options.bareArbitraryValues)}`
312
- }
313
-
314
- export async function extractRawCandidatesWithPositions(
315
- content: string,
316
- extension: string = 'html',
317
- options?: ExtractCandidateOptions,
318
- ): Promise<ExtractSourceCandidate[]> {
319
- const { Scanner } = await getOxideModule()
320
- const scanner = new Scanner({})
321
- const result = scanner.getCandidatesWithPositions({ content, extension })
322
-
323
- const candidates = result.map(({ candidate, position }) => ({
324
- rawCandidate: candidate,
325
- start: position,
326
- end: position + candidate.length,
327
- }))
328
- candidates.push(...extractBareArbitraryValueSourceCandidatesWithPositions(content, options?.bareArbitraryValues))
329
- return dedupeCandidatesWithPositions(candidates)
330
- }
331
-
332
- export async function extractSourceCandidatesWithPositions(
333
- content: string,
334
- extension: string = 'html',
335
- options?: ExtractCandidateOptions,
336
- ): Promise<ExtractSourceCandidate[]> {
337
- const normalizedExtension = extension.replace(/^\./, '')
338
- const candidates: ExtractSourceCandidateWithContext[] = CSS_LIKE_SOURCE_EXTENSION_RE.test(normalizedExtension)
339
- ? await extractCssApplyCandidates(content, normalizedExtension, options)
340
- : (await extractRawCandidatesWithPositions(content, normalizedExtension, options))
341
- .map(candidate => ({
342
- ...candidate,
343
- content,
344
- extension: normalizedExtension,
345
- localStart: candidate.start,
346
- }))
347
- if (MIXED_TEMPLATE_SOURCE_EXTENSION_RE.test(normalizedExtension)) {
348
- candidates.push(...await extractMixedSourceScriptCandidates(content, options))
349
- }
350
- const seen = new Set<string>()
351
- return candidates.filter((candidate) => {
352
- if (!shouldKeepSourceCandidate(candidate.content, candidate.extension, createLocalCandidate(candidate))) {
353
- return false
354
- }
355
- const key = `${candidate.start}:${candidate.end}:${candidate.rawCandidate}`
356
- if (seen.has(key)) {
357
- return false
358
- }
359
- seen.add(key)
360
- return true
361
- }).map(({ rawCandidate, start, end }) => ({ rawCandidate, start, end }))
362
- }
363
-
364
- export async function extractSourceCandidates(
365
- content: string,
366
- extension: string = 'html',
367
- options?: ExtractCandidateOptions,
368
- ): Promise<string[]> {
369
- const candidates = await extractSourceCandidatesWithPositions(content, extension, options)
370
- return [...new Set(candidates.map(candidate => candidate.rawCandidate))]
371
- }
372
-
373
- export async function extractRawCandidates(
374
- sources?: SourceEntry[],
375
- options?: ExtractCandidateOptions,
376
- ): Promise<string[]> {
377
- const { Scanner } = await getOxideModule()
378
- const scanner = new Scanner(sources === undefined ? {} : { sources })
379
-
380
- const candidates = new Set(scanner.scan())
381
- if (options?.bareArbitraryValues !== undefined && options.bareArbitraryValues !== false) {
382
- await Promise.all((scanner.files ?? []).map(async (file) => {
383
- try {
384
- const content = await fs.readFile(file, 'utf8')
385
- const extension = toExtension(file)
386
- for (const candidate of extractBareArbitraryValueSourceCandidatesWithPositions(content, options.bareArbitraryValues)) {
387
- if (shouldKeepSourceCandidate(content, extension, candidate)) {
388
- candidates.add(candidate.rawCandidate)
389
- }
390
- }
391
- }
392
- catch {
393
- // 文件可能在扫描和读取之间被移除,保持与 Tailwind 原扫描结果一致。
394
- }
395
- }))
396
- }
397
- return [...candidates]
398
- }
399
-
400
- export async function extractValidCandidates(options?: ExtractValidCandidatesOption) {
401
- const providedOptions = options ?? {}
402
- const defaultCwd = providedOptions.cwd ?? process.cwd()
403
-
404
- const base = providedOptions.base ?? defaultCwd
405
- const baseFallbacks = providedOptions.baseFallbacks ?? []
406
- const css = providedOptions.css ?? '@import "tailwindcss";'
407
- const sources = (providedOptions.sources ?? [
408
- {
409
- base: defaultCwd,
410
- pattern: '**/*',
411
- negated: false,
412
- },
413
- ]).map(source => ({
414
- base: source.base ?? defaultCwd,
415
- pattern: source.pattern,
416
- negated: source.negated,
417
- }))
418
-
419
- const source = {
420
- projectRoot: defaultCwd,
421
- base,
422
- baseFallbacks,
423
- css,
424
- dependencies: [],
425
- }
426
- const designSystemKey = getTailwindV4DesignSystemCacheKey(source)
427
- const designSystem = await loadTailwindV4DesignSystem(source)
428
- const candidateCacheKey = createCandidateCacheKey(designSystemKey, providedOptions)
429
- const candidateCache = designSystemCandidateCache.get(candidateCacheKey) ?? new Map<string, boolean>()
430
- designSystemCandidateCache.set(candidateCacheKey, candidateCache)
431
-
432
- const candidates = await extractRawCandidates(
433
- sources,
434
- providedOptions.bareArbitraryValues === undefined
435
- ? undefined
436
- : { bareArbitraryValues: providedOptions.bareArbitraryValues },
437
- )
438
- const inlineSources = extractTailwindV4InlineSourceCandidates(css)
439
- for (const candidate of inlineSources.included) {
440
- candidates.push(candidate)
441
- }
442
- for (const candidate of inlineSources.excluded) {
443
- let index = candidates.indexOf(candidate)
444
- while (index !== -1) {
445
- candidates.splice(index, 1)
446
- index = candidates.indexOf(candidate)
447
- }
448
- }
449
- const validCandidates: string[] = []
450
- const uncachedCandidates: string[] = []
451
-
452
- for (const rawCandidate of candidates) {
453
- const cached = candidateCache.get(rawCandidate)
454
- if (cached === true) {
455
- validCandidates.push(rawCandidate)
456
- continue
457
- }
458
-
459
- if (cached === false) {
460
- continue
461
- }
462
-
463
- const bareArbitrary = resolveBareArbitraryValueCandidate(rawCandidate, providedOptions.bareArbitraryValues)
464
- if (
465
- designSystem.parseCandidate(rawCandidate).length > 0
466
- || (bareArbitrary && designSystem.parseCandidate(bareArbitrary.canonicalCandidate).length > 0)
467
- ) {
468
- uncachedCandidates.push(rawCandidate)
469
- continue
470
- }
471
-
472
- candidateCache.set(rawCandidate, false)
473
- }
474
-
475
- if (uncachedCandidates.length === 0) {
476
- return validCandidates
477
- }
478
-
479
- const validUncachedCandidates = resolveValidTailwindV4Candidates(
480
- designSystem,
481
- uncachedCandidates,
482
- providedOptions.bareArbitraryValues === undefined
483
- ? undefined
484
- : { bareArbitraryValues: providedOptions.bareArbitraryValues },
485
- )
486
-
487
- for (const candidate of uncachedCandidates) {
488
- const isValid = validUncachedCandidates.has(candidate)
489
- candidateCache.set(candidate, isValid)
490
- if (!isValid) {
491
- continue
492
- }
493
- validCandidates.push(candidate)
494
- }
495
-
496
- return validCandidates
497
- }
498
-
499
- function buildLineOffsets(content: string) {
500
- const offsets: number[] = [0]
501
- for (let i = 0; i < content.length; i++) {
502
- if (content[i] === '\n') {
503
- offsets.push(i + 1)
504
- }
505
- }
506
- // Push a sentinel to simplify bounds checks during binary search.
507
- if (offsets[offsets.length - 1] !== content.length) {
508
- offsets.push(content.length)
509
- }
510
- return offsets
511
- }
512
-
513
- function resolveLineMeta(content: string, offsets: number[], index: number) {
514
- let low = 0
515
- let high = offsets.length - 1
516
- while (low <= high) {
517
- const mid = Math.floor((low + high) / 2)
518
- const start = offsets[mid]
519
- if (start === undefined) {
520
- break
521
- }
522
- const nextStart = offsets[mid + 1] ?? content.length
523
-
524
- if (index < start) {
525
- high = mid - 1
526
- continue
527
- }
528
-
529
- if (index >= nextStart) {
530
- low = mid + 1
531
- continue
532
- }
533
-
534
- const line = mid + 1
535
- const column = index - start + 1
536
- const lineEnd = content.indexOf('\n', start)
537
- const lineText = content.slice(start, lineEnd === -1 ? content.length : lineEnd)
538
-
539
- return { line, column, lineText }
540
- }
541
-
542
- const lastStart = offsets[offsets.length - 2] ?? 0
543
- return {
544
- line: offsets.length - 1,
545
- column: index - lastStart + 1,
546
- lineText: content.slice(lastStart),
547
- }
548
- }
549
-
550
- function toExtension(filename: string) {
551
- const ext = path.extname(filename).replace(/^\./, '')
552
- return ext || 'txt'
553
- }
554
-
555
- function toRelativeFile(cwd: string, filename: string) {
556
- const relative = path.relative(cwd, filename)
557
- return relative === '' ? path.basename(filename) : relative
558
- }
559
-
560
- export interface ExtractProjectCandidatesOptions {
561
- cwd?: string
562
- sources?: SourceEntry[]
563
- base?: string
564
- baseFallbacks?: string[]
565
- css?: string
566
- }
567
-
568
- export interface ResolveProjectSourceFilesOptions {
569
- cwd?: string
570
- sources?: SourceEntry[]
571
- ignoredSources?: SourceEntry[]
572
- base?: string
573
- baseFallbacks?: string[]
574
- css?: string
575
- filter?: (file: string) => boolean
576
- }
577
-
578
- async function resolveScannerSources(options?: ResolveProjectSourceFilesOptions) {
579
- const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd()
580
- if (options?.sources?.length || options?.css === undefined) {
581
- return {
582
- cwd,
583
- sources: normalizeTailwindV4ScannerSources(options?.sources, cwd, options?.ignoredSources),
584
- }
585
- }
586
-
587
- const base = options.base ? path.resolve(options.base) : cwd
588
- const { compiled } = await compileTailwindV4Source({
589
- projectRoot: cwd,
590
- base,
591
- baseFallbacks: options.baseFallbacks?.map(baseFallback => path.resolve(baseFallback)) ?? [],
592
- css: options.css,
593
- dependencies: [],
594
- })
595
- return {
596
- cwd,
597
- sources: normalizeTailwindV4ScannerSources(
598
- createTailwindV4CompiledSourceEntries(compiled.root, compiled.sources, base),
599
- cwd,
600
- options.ignoredSources,
601
- ),
602
- }
603
- }
604
-
605
- export async function resolveProjectSourceFiles(options?: ResolveProjectSourceFilesOptions): Promise<string[]> {
606
- const { sources } = await resolveScannerSources(options)
607
- const { Scanner } = await getOxideModule()
608
- const scanner = new Scanner({
609
- sources,
610
- })
611
- const files = scanner.files ?? []
612
- return options?.filter
613
- ? files.filter(options.filter)
614
- : files
615
- }
616
-
617
- export async function extractProjectCandidatesWithPositions(
618
- options?: ExtractProjectCandidatesOptions,
619
- ): Promise<TailwindTokenReport> {
620
- const { cwd, sources } = await resolveScannerSources(options)
621
- const { Scanner } = await getOxideModule()
622
- const scanner = new Scanner({
623
- sources,
624
- })
625
-
626
- const files = scanner.files ?? []
627
- const entries: TailwindTokenLocation[] = []
628
- const skipped: TailwindTokenReport['skippedFiles'] = []
629
-
630
- for (const file of files) {
631
- let content: string
632
- try {
633
- content = await fs.readFile(file, 'utf8')
634
- }
635
- catch (error) {
636
- skipped.push({
637
- file,
638
- reason: error instanceof Error ? error.message : 'Unknown error',
639
- })
640
- continue
641
- }
642
-
643
- const extension = toExtension(file)
644
- const matches = scanner.getCandidatesWithPositions({
645
- file,
646
- content,
647
- extension,
648
- })
649
-
650
- if (!matches.length) {
651
- continue
652
- }
653
-
654
- const offsets = buildLineOffsets(content)
655
- const relativeFile = toRelativeFile(cwd, file)
656
-
657
- for (const match of matches) {
658
- const info = resolveLineMeta(content, offsets, match.position)
659
- entries.push({
660
- rawCandidate: match.candidate,
661
- file,
662
- relativeFile,
663
- extension,
664
- start: match.position,
665
- end: match.position + match.candidate.length,
666
- length: match.candidate.length,
667
- line: info.line,
668
- column: info.column,
669
- lineText: info.lineText,
670
- })
671
- }
672
- }
673
-
674
- return {
675
- entries,
676
- filesScanned: files.length,
677
- skippedFiles: skipped,
678
- sources,
679
- }
680
- }
681
-
682
- export function groupTokensByFile(
683
- report: TailwindTokenReport,
684
- options?: { key?: TailwindTokenFileKey, stripAbsolutePaths?: boolean },
685
- ): TailwindTokenByFileMap {
686
- const key = options?.key ?? 'relative'
687
- const stripAbsolute = options?.stripAbsolutePaths ?? key !== 'absolute'
688
-
689
- return report.entries.reduce<TailwindTokenByFileMap>((acc, entry) => {
690
- const bucketKey = key === 'absolute' ? entry.file : entry.relativeFile
691
- const bucket = acc[bucketKey] ?? (acc[bucketKey] = [])
692
- const value = stripAbsolute
693
- ? {
694
- ...entry,
695
- file: entry.relativeFile,
696
- }
697
- : entry
698
- bucket.push(value)
699
- return acc
700
- }, {})
701
- }
1
+ export {
2
+ extractProjectCandidatesWithPositions,
3
+ extractRawCandidates,
4
+ extractRawCandidatesWithPositions,
5
+ extractSourceCandidates,
6
+ extractSourceCandidatesWithPositions,
7
+ extractValidCandidates,
8
+ groupTokensByFile,
9
+ resolveProjectSourceFiles,
10
+ } from '@tailwindcss-mangle/engine'
11
+ export type {
12
+ ExtractCandidateOptions,
13
+ ExtractProjectCandidatesOptions,
14
+ ExtractSourceCandidate,
15
+ ExtractValidCandidatesOption,
16
+ ResolveProjectSourceFilesOptions,
17
+ } from '@tailwindcss-mangle/engine'