tailwindcss-patch 9.4.4 → 9.5.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.
- package/dist/{cli-srv0kRlt.js → cli-CGyUnvFc.js} +3 -2
- package/dist/{cli-CLvyx3xl.mjs → cli-DRfALTSo.mjs} +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.mjs +2 -2
- package/dist/commands/cli-runtime.d.mts +1 -1
- package/dist/commands/cli-runtime.d.ts +1 -1
- package/dist/commands/cli-runtime.js +2 -2
- package/dist/commands/cli-runtime.mjs +2 -2
- package/dist/{dist-Dn7cMVhi.js → dist-DlC5vuI2.js} +1 -1
- package/dist/index.d.mts +7 -149
- package/dist/index.d.ts +8 -150
- package/dist/index.js +294 -521
- package/dist/index.mjs +6 -471
- package/dist/{validate-oAkURzUC.d.mts → validate-B5-08lrU.d.ts} +8 -252
- package/dist/{validate-BuqRodYI.d.ts → validate-CgrG4aAY.d.mts} +8 -252
- package/dist/{validate-CUNJFfHh.mjs → validate-Q00Ccqht.mjs} +5 -1402
- package/dist/{validate-RpdgpjgT.js → validate-XiYmTZcd.js} +79 -1705
- package/package.json +4 -6
- package/src/api/tailwindcss-patcher.ts +3 -2
- package/src/extraction/candidate-extractor.ts +17 -701
- package/src/extraction/split-candidate-tokens.ts +5 -101
- package/src/options/types.ts +1 -2
- package/src/style-candidates.ts +5 -35
- package/src/style-generator.ts +11 -80
- package/src/types.ts +21 -95
- package/src/v3/index.ts +2 -2
- package/src/v4/index.ts +104 -28
- package/src/v3/style-generator.ts +0 -384
- package/src/v4/bare-arbitrary-values.ts +0 -545
- package/src/v4/candidates.ts +0 -316
- package/src/v4/engine.ts +0 -112
- package/src/v4/node-adapter.ts +0 -207
- package/src/v4/source-scan.ts +0 -432
- package/src/v4/source.ts +0 -235
- package/src/v4/style-generator.ts +0 -44
- package/src/v4/types.ts +0 -103
|
@@ -1,701 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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'
|