tailwindcss-patch 9.3.2 → 9.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.
@@ -55,6 +55,190 @@ export interface ExtractValidCandidatesOption {
55
55
  bareArbitraryValues?: boolean | BareArbitraryValueOptions
56
56
  }
57
57
 
58
+ export interface ExtractSourceCandidate {
59
+ rawCandidate: string
60
+ start: number
61
+ end: number
62
+ }
63
+
64
+ interface ExtractSourceCandidateWithContext extends ExtractSourceCandidate {
65
+ content: string
66
+ extension: string
67
+ localStart: number
68
+ }
69
+
70
+ const HTML_ATTRIBUTE_NAME_CANDIDATE_RE = /^(?:class|className|hover-class|hoverClass)$/
71
+ const CSS_DIRECTIVE_CANDIDATE_RE = /^@(?:apply|tailwind|source|config|plugin|theme|utility|custom-variant|variant)$/
72
+ const CSS_APPLY_IMPORTANT = '!important'
73
+ const JS_LIKE_SOURCE_EXTENSION_RE = /^(?:[cm]?[jt]sx?)$/
74
+ const MIXED_TEMPLATE_SOURCE_EXTENSION_RE = /^(?:vue|uvue|nvue|svelte|mpx)$/
75
+ const CSS_LIKE_SOURCE_EXTENSION_RE = /^(?:css|wxss|acss|jxss|ttss|qss|tyss|scss|sass|less|styl|stylus)$/
76
+ const SFC_SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
77
+
78
+ function isWhitespace(value: string | undefined) {
79
+ return value === ' ' || value === '\n' || value === '\r' || value === '\t' || value === '\f'
80
+ }
81
+
82
+ function isHtmlAttributeNameCandidate(content: string, candidate: ExtractSourceCandidate) {
83
+ if (!HTML_ATTRIBUTE_NAME_CANDIDATE_RE.test(candidate.rawCandidate)) {
84
+ return false
85
+ }
86
+ let index = candidate.end
87
+ while (isWhitespace(content[index])) {
88
+ index++
89
+ }
90
+ return content[index] === '='
91
+ }
92
+
93
+ function isInsideHtmlTagText(content: string, candidate: ExtractSourceCandidate) {
94
+ const open = content.lastIndexOf('<', candidate.start)
95
+ const close = content.lastIndexOf('>', candidate.start)
96
+ if (open > close) {
97
+ return false
98
+ }
99
+ const nextOpen = content.indexOf('<', candidate.end)
100
+ return nextOpen !== -1 && (nextOpen < content.indexOf('>', candidate.end) || content.indexOf('>', candidate.end) === -1)
101
+ }
102
+
103
+ function isCssDirectiveCandidate(candidate: string) {
104
+ return candidate === CSS_APPLY_IMPORTANT || CSS_DIRECTIVE_CANDIDATE_RE.test(candidate)
105
+ }
106
+
107
+ function isCandidateInCssApplyParams(content: string, candidate: ExtractSourceCandidate) {
108
+ const apply = content.lastIndexOf('@apply', candidate.start)
109
+ if (apply === -1) {
110
+ return false
111
+ }
112
+ const boundary = content.slice(apply + '@apply'.length, candidate.start)
113
+ return !/[;{}]/.test(boundary)
114
+ }
115
+
116
+ function isCandidateInsideJsStringStaticContent(content: string, start: number) {
117
+ let quote: '"' | '\'' | '`' | undefined
118
+ let templateExpressionDepth = 0
119
+ for (let index = 0; index < start; index++) {
120
+ const char = content[index]
121
+ const next = content[index + 1]
122
+
123
+ if (quote && char === '\\') {
124
+ index++
125
+ continue
126
+ }
127
+
128
+ if (quote === '`' && templateExpressionDepth > 0) {
129
+ if (char === '"' || char === '\'') {
130
+ const nestedQuote = char
131
+ index++
132
+ while (index < start) {
133
+ const nestedChar = content[index]
134
+ if (nestedChar === '\\') {
135
+ index += 2
136
+ continue
137
+ }
138
+ if (nestedChar === nestedQuote) {
139
+ break
140
+ }
141
+ index++
142
+ }
143
+ continue
144
+ }
145
+ if (char === '`') {
146
+ index++
147
+ while (index < start) {
148
+ const nestedChar = content[index]
149
+ if (nestedChar === '\\') {
150
+ index += 2
151
+ continue
152
+ }
153
+ if (nestedChar === '`') {
154
+ break
155
+ }
156
+ index++
157
+ }
158
+ continue
159
+ }
160
+ if (char === '{') {
161
+ templateExpressionDepth++
162
+ continue
163
+ }
164
+ if (char === '}') {
165
+ templateExpressionDepth--
166
+ continue
167
+ }
168
+ continue
169
+ }
170
+
171
+ if (quote) {
172
+ if (quote === '`' && char === '$' && next === '{') {
173
+ templateExpressionDepth = 1
174
+ index++
175
+ continue
176
+ }
177
+ if (char === quote) {
178
+ quote = undefined
179
+ }
180
+ continue
181
+ }
182
+
183
+ if (char === '"' || char === '\'' || char === '`') {
184
+ quote = char
185
+ }
186
+ }
187
+
188
+ return quote !== undefined && templateExpressionDepth === 0
189
+ }
190
+
191
+ function shouldKeepSourceCandidate(content: string, extension: string, candidate: ExtractSourceCandidate) {
192
+ if (!candidate.rawCandidate || isCssDirectiveCandidate(candidate.rawCandidate)) {
193
+ return false
194
+ }
195
+ if (CSS_LIKE_SOURCE_EXTENSION_RE.test(extension) && !isCandidateInCssApplyParams(content, candidate)) {
196
+ return false
197
+ }
198
+ if (isHtmlAttributeNameCandidate(content, candidate)) {
199
+ return false
200
+ }
201
+ if (isInsideHtmlTagText(content, candidate)) {
202
+ return false
203
+ }
204
+ if (
205
+ JS_LIKE_SOURCE_EXTENSION_RE.test(extension)
206
+ && !isCandidateInsideJsStringStaticContent(content, candidate.start)
207
+ ) {
208
+ return false
209
+ }
210
+ return true
211
+ }
212
+
213
+ function createLocalCandidate(candidate: ExtractSourceCandidateWithContext): ExtractSourceCandidate {
214
+ return {
215
+ rawCandidate: candidate.rawCandidate,
216
+ start: candidate.localStart,
217
+ end: candidate.localStart + candidate.rawCandidate.length,
218
+ }
219
+ }
220
+
221
+ async function extractMixedSourceScriptCandidates(content: string) {
222
+ const candidates: ExtractSourceCandidateWithContext[] = []
223
+ SFC_SCRIPT_BLOCK_RE.lastIndex = 0
224
+ let match = SFC_SCRIPT_BLOCK_RE.exec(content)
225
+ while (match !== null) {
226
+ const scriptContent = match[1] ?? ''
227
+ const scriptStart = match.index + match[0].indexOf(scriptContent)
228
+ const scriptCandidates = await extractRawCandidatesWithPositions(scriptContent, 'js')
229
+ candidates.push(...scriptCandidates.map(candidate => ({
230
+ content: scriptContent,
231
+ extension: 'js',
232
+ localStart: candidate.start,
233
+ rawCandidate: candidate.rawCandidate,
234
+ start: candidate.start + scriptStart,
235
+ end: candidate.end + scriptStart,
236
+ })))
237
+ match = SFC_SCRIPT_BLOCK_RE.exec(content)
238
+ }
239
+ return candidates
240
+ }
241
+
58
242
  function createCandidateCacheKey(
59
243
  designSystemKey: string,
60
244
  options: Pick<ExtractValidCandidatesOption, 'bareArbitraryValues'>,
@@ -68,7 +252,7 @@ function createCandidateCacheKey(
68
252
  export async function extractRawCandidatesWithPositions(
69
253
  content: string,
70
254
  extension: string = 'html',
71
- ): Promise<{ rawCandidate: string, start: number, end: number }[]> {
255
+ ): Promise<ExtractSourceCandidate[]> {
72
256
  const { Scanner } = await getOxideModule()
73
257
  const scanner = new Scanner({})
74
258
  const result = scanner.getCandidatesWithPositions({ content, extension })
@@ -80,6 +264,43 @@ export async function extractRawCandidatesWithPositions(
80
264
  }))
81
265
  }
82
266
 
267
+ export async function extractSourceCandidatesWithPositions(
268
+ content: string,
269
+ extension: string = 'html',
270
+ ): Promise<ExtractSourceCandidate[]> {
271
+ const normalizedExtension = extension.replace(/^\./, '')
272
+ const candidates: ExtractSourceCandidateWithContext[] = (await extractRawCandidatesWithPositions(content, normalizedExtension))
273
+ .map(candidate => ({
274
+ ...candidate,
275
+ content,
276
+ extension: normalizedExtension,
277
+ localStart: candidate.start,
278
+ }))
279
+ if (MIXED_TEMPLATE_SOURCE_EXTENSION_RE.test(normalizedExtension)) {
280
+ candidates.push(...await extractMixedSourceScriptCandidates(content))
281
+ }
282
+ const seen = new Set<string>()
283
+ return candidates.filter((candidate) => {
284
+ if (!shouldKeepSourceCandidate(candidate.content, candidate.extension, createLocalCandidate(candidate))) {
285
+ return false
286
+ }
287
+ const key = `${candidate.start}:${candidate.end}:${candidate.rawCandidate}`
288
+ if (seen.has(key)) {
289
+ return false
290
+ }
291
+ seen.add(key)
292
+ return true
293
+ }).map(({ rawCandidate, start, end }) => ({ rawCandidate, start, end }))
294
+ }
295
+
296
+ export async function extractSourceCandidates(
297
+ content: string,
298
+ extension: string = 'html',
299
+ ): Promise<string[]> {
300
+ const candidates = await extractSourceCandidatesWithPositions(content, extension)
301
+ return [...new Set(candidates.map(candidate => candidate.rawCandidate))]
302
+ }
303
+
83
304
  export async function extractRawCandidates(
84
305
  sources?: SourceEntry[],
85
306
  ): Promise<string[]> {
@@ -22,6 +22,8 @@ import {
22
22
  extractProjectCandidatesWithPositions,
23
23
  extractRawCandidates,
24
24
  extractRawCandidatesWithPositions,
25
+ extractSourceCandidates,
26
+ extractSourceCandidatesWithPositions,
25
27
  extractValidCandidates,
26
28
  groupTokensByFile,
27
29
  } from './extraction/candidate-extractor'
@@ -57,6 +59,8 @@ export {
57
59
  extractProjectCandidatesWithPositions,
58
60
  extractRawCandidates,
59
61
  extractRawCandidatesWithPositions,
62
+ extractSourceCandidates,
63
+ extractSourceCandidatesWithPositions,
60
64
  extractValidCandidates,
61
65
  getPatchStatusReport,
62
66
  groupTokensByFile,
@@ -105,6 +109,7 @@ export type { TailwindCssPatchOptions } from './config'
105
109
  export * from './types'
106
110
  export type {
107
111
  TailwindV4CandidateSource,
112
+ TailwindV4CssSource,
108
113
  TailwindV4DesignSystem,
109
114
  TailwindV4Engine,
110
115
  TailwindV4GenerateOptions,
package/src/index.ts CHANGED
@@ -39,8 +39,11 @@ export {
39
39
  extractProjectCandidatesWithPositions,
40
40
  extractRawCandidates,
41
41
  extractRawCandidatesWithPositions,
42
+ extractSourceCandidates,
43
+ extractSourceCandidatesWithPositions,
42
44
  extractValidCandidates,
43
45
  groupTokensByFile,
46
+ type ExtractSourceCandidate,
44
47
  } from './extraction/candidate-extractor'
45
48
  export {
46
49
  collectClassesFromContexts,
@@ -60,6 +63,7 @@ export {
60
63
  } from './v4'
61
64
  export type {
62
65
  TailwindV4CandidateSource,
66
+ TailwindV4CssSource,
63
67
  TailwindV4DesignSystem,
64
68
  TailwindV4Engine,
65
69
  TailwindV4GenerateOptions,
@@ -148,6 +148,19 @@ function normalizeTailwindV4Options(
148
148
  ): NormalizedTailwindV4Options {
149
149
  const configuredBase = v4?.base ? path.resolve(v4.base) : undefined
150
150
  const base = configuredBase ?? fallbackBase
151
+ const resolveV4Path = (value: string) => path.isAbsolute(value) ? path.resolve(value) : path.resolve(fallbackBase, value)
152
+ const cssSources = Array.isArray(v4?.cssSources)
153
+ ? v4!.cssSources
154
+ .filter(source => typeof source?.css === 'string')
155
+ .map(source => ({
156
+ css: source.css,
157
+ ...(source.base === undefined ? {} : { base: resolveV4Path(source.base) }),
158
+ ...(source.file === undefined ? {} : { file: resolveV4Path(source.file) }),
159
+ ...(source.dependencies === undefined
160
+ ? {}
161
+ : { dependencies: source.dependencies.filter(Boolean).map(resolveV4Path) }),
162
+ }))
163
+ : []
151
164
  const cssEntries = Array.isArray(v4?.cssEntries)
152
165
  ? v4!.cssEntries.filter((entry): entry is string => Boolean(entry)).map(entry => path.resolve(entry))
153
166
  : []
@@ -167,6 +180,7 @@ function normalizeTailwindV4Options(
167
180
  base,
168
181
  ...(configuredBase === undefined ? {} : { configuredBase }),
169
182
  ...(v4?.css === undefined ? {} : { css: v4.css }),
183
+ cssSources,
170
184
  cssEntries,
171
185
  sources,
172
186
  hasUserDefinedSources,
@@ -1,6 +1,7 @@
1
1
  import type { SourceEntry } from '@tailwindcss/oxide'
2
2
  import type { PackageResolvingOptions } from 'local-pkg'
3
3
  import type { ILengthUnitsPatchOptions } from '../types'
4
+ import type { TailwindV4CssSource } from '../v4/types'
4
5
 
5
6
  export type CacheStrategy = 'merge' | 'overwrite'
6
7
  export type CacheDriver = 'file' | 'memory' | 'noop'
@@ -97,6 +98,8 @@ export interface TailwindV4Options {
97
98
  base?: string
98
99
  /** Raw CSS passed directly to the v4 design system. */
99
100
  css?: string
101
+ /** 构建器在 CSS 落盘前捕获的内存 CSS 入口。 */
102
+ cssSources?: TailwindV4CssSource[]
100
103
  /** Set of CSS entry files that should be scanned for `@config` directives. */
101
104
  cssEntries?: string[]
102
105
  /** Overrides the content sources scanned by the oxide scanner. */
@@ -187,6 +190,7 @@ export interface NormalizedTailwindV4Options {
187
190
  base: string
188
191
  configuredBase?: string
189
192
  css?: string
193
+ cssSources: TailwindV4CssSource[]
190
194
  cssEntries: string[]
191
195
  sources: SourceEntry[]
192
196
  hasUserDefinedSources: boolean
@@ -1,3 +1,4 @@
1
+ import type { ExtractValidCandidatesOption } from '../extraction/candidate-extractor'
1
2
  import type { NormalizedTailwindCssPatchOptions } from '../options/types'
2
3
  import type { TailwindcssRuntimeContext } from '../types'
3
4
  import process from 'node:process'
@@ -63,6 +64,35 @@ export async function collectClassesFromTailwindV4(
63
64
  negated: source.negated,
64
65
  }))
65
66
  }
67
+ const addCandidates = async (extractOptions: ExtractValidCandidatesOption) => {
68
+ const candidates = await extractValidCandidates(extractOptions)
69
+ for (const candidate of candidates) {
70
+ if (options.filter(candidate)) {
71
+ set.add(candidate)
72
+ }
73
+ }
74
+ }
75
+
76
+ if (v4Options.cssSources.length > 0) {
77
+ for (const source of v4Options.cssSources) {
78
+ const sourceFile = toAbsolute(source.file)
79
+ const sourceBase = toAbsolute(source.base)
80
+ ?? (sourceFile ? path.dirname(sourceFile) : resolvedDefaultBase)
81
+ const designSystemBases = resolvedConfiguredBase && resolvedConfiguredBase !== sourceBase
82
+ ? [sourceBase, resolvedConfiguredBase]
83
+ : [sourceBase]
84
+ const sources = resolveSources(sourceBase)
85
+ const firstBase = designSystemBases[0] ?? sourceBase
86
+ await addCandidates({
87
+ cwd: options.projectRoot,
88
+ base: firstBase,
89
+ baseFallbacks: designSystemBases.slice(1),
90
+ css: source.css,
91
+ ...(v4Options.bareArbitraryValues === undefined ? {} : { bareArbitraryValues: v4Options.bareArbitraryValues }),
92
+ ...(sources === undefined ? {} : { sources }),
93
+ })
94
+ }
95
+ }
66
96
 
67
97
  if (v4Options.cssEntries.length > 0) {
68
98
  for (const entry of v4Options.cssEntries) {
@@ -86,15 +116,10 @@ export async function collectClassesFromTailwindV4(
86
116
  ...(v4Options.bareArbitraryValues === undefined ? {} : { bareArbitraryValues: v4Options.bareArbitraryValues }),
87
117
  ...(sources === undefined ? {} : { sources }),
88
118
  }
89
- const candidates = await extractValidCandidates(extractOptions)
90
- for (const candidate of candidates) {
91
- if (options.filter(candidate)) {
92
- set.add(candidate)
93
- }
94
- }
119
+ await addCandidates(extractOptions)
95
120
  }
96
121
  }
97
- else {
122
+ else if (v4Options.cssSources.length === 0) {
98
123
  const baseForCss = resolvedConfiguredBase ?? resolvedDefaultBase
99
124
  const sources = resolveSources(baseForCss)
100
125
  const extractOptions = {
@@ -104,12 +129,7 @@ export async function collectClassesFromTailwindV4(
104
129
  ...(v4Options.css === undefined ? {} : { css: v4Options.css }),
105
130
  ...(sources === undefined ? {} : { sources }),
106
131
  }
107
- const candidates = await extractValidCandidates(extractOptions)
108
- for (const candidate of candidates) {
109
- if (options.filter(candidate)) {
110
- set.add(candidate)
111
- }
112
- }
132
+ await addCandidates(extractOptions)
113
133
  }
114
134
 
115
135
  return set
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