Skip to content

Commit e430655

Browse files
committed
feat(tailwindcss-patch): expose tailwind v4 engine api
1 parent 3d8ec06 commit e430655

11 files changed

Lines changed: 923 additions & 69 deletions

File tree

.changeset/tall-masks-breathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tailwindcss-patch": minor
3+
---
4+
5+
Expose a reusable Tailwind CSS v4 engine API for resolving CSS sources, validating candidates through the v4 design system, and generating CSS with Tailwind's compile/build pipeline.

packages/tailwindcss-patch/src/extraction/candidate-extractor.ts

Lines changed: 14 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,78 +8,21 @@ import type {
88
import { promises as fs } from 'node:fs'
99
import process from 'node:process'
1010
import path from 'pathe'
11+
import { resolveValidTailwindV4Candidates } from '../v4/candidates'
12+
import { getTailwindV4DesignSystemCacheKey, loadTailwindV4DesignSystem } from '../v4/node-adapter'
1113

12-
let nodeImportPromise: ReturnType<typeof importNode> | undefined
1314
let oxideImportPromise: ReturnType<typeof importOxide> | undefined
14-
const designSystemPromiseCache = new Map<string, Promise<any>>()
1515
const designSystemCandidateCache = new Map<string, Map<string, boolean>>()
1616

17-
async function importNode() {
18-
return import('@tailwindcss/node')
19-
}
20-
2117
async function importOxide() {
2218
return import('@tailwindcss/oxide')
2319
}
2420

25-
function getNodeModule() {
26-
nodeImportPromise ??= importNode()
27-
return nodeImportPromise
28-
}
29-
3021
function getOxideModule() {
3122
oxideImportPromise ??= importOxide()
3223
return oxideImportPromise
3324
}
3425

35-
function createDesignSystemCacheKey(css: string, bases: string[]) {
36-
return JSON.stringify({
37-
css,
38-
bases: Array.from(new Set(bases.filter(Boolean))),
39-
})
40-
}
41-
42-
async function loadDesignSystem(css: string, bases: string[]) {
43-
const uniqueBases = Array.from(new Set(bases.filter(Boolean)))
44-
if (uniqueBases.length === 0) {
45-
throw new Error('No base directories provided for Tailwind CSS design system.')
46-
}
47-
48-
const cacheKey = createDesignSystemCacheKey(css, uniqueBases)
49-
const cached = designSystemPromiseCache.get(cacheKey)
50-
if (cached) {
51-
return cached
52-
}
53-
54-
const promise = (async () => {
55-
const { __unstable__loadDesignSystem } = await getNodeModule()
56-
let lastError: unknown
57-
58-
for (const base of uniqueBases) {
59-
try {
60-
return await __unstable__loadDesignSystem(css, { base })
61-
}
62-
catch (error) {
63-
lastError = error
64-
}
65-
}
66-
67-
if (lastError instanceof Error) {
68-
throw lastError
69-
}
70-
throw new Error('Failed to load Tailwind CSS design system.')
71-
})()
72-
73-
designSystemPromiseCache.set(cacheKey, promise)
74-
promise.catch(() => {
75-
if (designSystemPromiseCache.get(cacheKey) === promise) {
76-
designSystemPromiseCache.delete(cacheKey)
77-
designSystemCandidateCache.delete(cacheKey)
78-
}
79-
})
80-
return promise
81-
}
82-
8326
export interface ExtractValidCandidatesOption {
8427
sources?: SourceEntry[]
8528
base?: string
@@ -131,8 +74,15 @@ export async function extractValidCandidates(options?: ExtractValidCandidatesOpt
13174
negated: source.negated,
13275
}))
13376

134-
const designSystemKey = createDesignSystemCacheKey(css, [base, ...baseFallbacks])
135-
const designSystem = await loadDesignSystem(css, [base, ...baseFallbacks])
77+
const source = {
78+
projectRoot: defaultCwd,
79+
base,
80+
baseFallbacks,
81+
css,
82+
dependencies: [],
83+
}
84+
const designSystemKey = getTailwindV4DesignSystemCacheKey(source)
85+
const designSystem = await loadTailwindV4DesignSystem(source)
13686
const candidateCache = designSystemCandidateCache.get(designSystemKey) ?? new Map<string, boolean>()
13787
designSystemCandidateCache.set(designSystemKey, candidateCache)
13888

@@ -163,15 +113,10 @@ export async function extractValidCandidates(options?: ExtractValidCandidatesOpt
163113
return validCandidates
164114
}
165115

166-
const cssByCandidate = designSystem.candidatesToCss(uncachedCandidates)
116+
const validUncachedCandidates = resolveValidTailwindV4Candidates(designSystem, uncachedCandidates)
167117

168-
for (let index = 0; index < uncachedCandidates.length; index++) {
169-
const candidate = uncachedCandidates[index]
170-
if (candidate === undefined) {
171-
continue
172-
}
173-
const candidateCss = cssByCandidate[index]
174-
const isValid = typeof candidateCss === 'string' && candidateCss.trim().length > 0
118+
for (const candidate of uncachedCandidates) {
119+
const isValid = validUncachedCandidates.has(candidate)
175120
candidateCache.set(candidate, isValid)
176121
if (!isValid) {
177122
continue

packages/tailwindcss-patch/src/index.bundle.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ import {
3333
runTailwindBuild,
3434
} from './install'
3535
import logger from './logger'
36+
import {
37+
createTailwindV4Engine,
38+
loadTailwindV4DesignSystem,
39+
resolveTailwindV4Source,
40+
resolveTailwindV4SourceFromPatchOptions,
41+
resolveValidTailwindV4Candidates,
42+
} from './v4'
3643

3744
const require = createRequire(import.meta.url)
3845

@@ -46,18 +53,23 @@ export {
4653
CacheStore,
4754
collectClassesFromContexts,
4855
collectClassesFromTailwindV4,
56+
createTailwindV4Engine,
4957
extractProjectCandidatesWithPositions,
5058
extractRawCandidates,
5159
extractRawCandidatesWithPositions,
5260
extractValidCandidates,
5361
getPatchStatusReport,
5462
groupTokensByFile,
5563
loadRuntimeContexts,
64+
loadTailwindV4DesignSystem,
5665
logger,
5766
migrateConfigFiles,
5867
MIGRATION_REPORT_KIND,
5968
MIGRATION_REPORT_SCHEMA_VERSION,
6069
normalizeOptions,
70+
resolveTailwindV4Source,
71+
resolveTailwindV4SourceFromPatchOptions,
72+
resolveValidTailwindV4Candidates,
6173
restoreConfigFiles,
6274
runTailwindBuild,
6375
tailwindcssPatchCommands,
@@ -91,6 +103,15 @@ export type {
91103
} from './commands/validate'
92104
export type { TailwindCssPatchOptions } from './config'
93105
export * from './types'
106+
export type {
107+
TailwindV4CandidateSource,
108+
TailwindV4DesignSystem,
109+
TailwindV4Engine,
110+
TailwindV4GenerateOptions,
111+
TailwindV4GenerateResult,
112+
TailwindV4ResolvedSource,
113+
TailwindV4SourceOptions,
114+
} from './v4'
94115

95116
export function mountTailwindcssPatchCommands(cli: CAC, options: TailwindcssPatchCliMountOptions = {}) {
96117
return loadCliModule().mountTailwindcssPatchCommands(cli, options)

packages/tailwindcss-patch/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ export {
5151
} from './install'
5252
export { default as logger } from './logger'
5353
export * from './types'
54+
export {
55+
createTailwindV4Engine,
56+
loadTailwindV4DesignSystem,
57+
resolveTailwindV4Source,
58+
resolveTailwindV4SourceFromPatchOptions,
59+
resolveValidTailwindV4Candidates,
60+
} from './v4'
61+
export type {
62+
TailwindV4CandidateSource,
63+
TailwindV4DesignSystem,
64+
TailwindV4Engine,
65+
TailwindV4GenerateOptions,
66+
TailwindV4GenerateResult,
67+
TailwindV4ResolvedSource,
68+
TailwindV4SourceOptions,
69+
} from './v4'
5470

5571
export function defineConfig<T extends TailwindcssMangleConfig>(config: T): T {
5672
return config

0 commit comments

Comments
 (0)