Skip to content

Commit 1c0405c

Browse files
committed
chore: prefer CSS entry directories when resolving Tailwind v4 @config
1 parent d44eb0c commit 1c0405c

File tree

4 files changed

+91
-5
lines changed

4 files changed

+91
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tailwindcss-patch": patch
3+
---
4+
5+
Prefer CSS entry directories when resolving Tailwind v4 `@config` so relative configs are found even when a base directory is provided.

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,34 @@ async function importOxide() {
1717
return import('@tailwindcss/oxide')
1818
}
1919

20+
async function loadDesignSystem(css: string, bases: string[]) {
21+
const uniqueBases = Array.from(new Set(bases.filter(Boolean)))
22+
if (uniqueBases.length === 0) {
23+
throw new Error('No base directories provided for Tailwind CSS design system.')
24+
}
25+
26+
const { __unstable__loadDesignSystem } = await importNode()
27+
let lastError: unknown
28+
29+
for (const base of uniqueBases) {
30+
try {
31+
return await __unstable__loadDesignSystem(css, { base })
32+
}
33+
catch (error) {
34+
lastError = error
35+
}
36+
}
37+
38+
if (lastError instanceof Error) {
39+
throw lastError
40+
}
41+
throw new Error('Failed to load Tailwind CSS design system.')
42+
}
43+
2044
export interface ExtractValidCandidatesOption {
2145
sources?: SourceEntry[]
2246
base?: string
47+
baseFallbacks?: string[]
2348
css?: string
2449
cwd?: string
2550
}
@@ -55,6 +80,7 @@ export async function extractValidCandidates(options?: ExtractValidCandidatesOpt
5580
const defaultCwd = providedOptions.cwd ?? process.cwd()
5681

5782
const base = providedOptions.base ?? defaultCwd
83+
const baseFallbacks = providedOptions.baseFallbacks ?? []
5884
const css = providedOptions.css ?? '@import "tailwindcss";'
5985
const sources = (providedOptions.sources ?? [
6086
{
@@ -68,8 +94,7 @@ export async function extractValidCandidates(options?: ExtractValidCandidatesOpt
6894
negated: source.negated,
6995
}))
7096

71-
const { __unstable__loadDesignSystem } = await importNode()
72-
const designSystem = await __unstable__loadDesignSystem(css, { base })
97+
const designSystem = await loadDesignSystem(css, [base, ...baseFallbacks])
7398

7499
const candidates = await extractRawCandidates(sources)
75100
const parsedCandidates = candidates.filter(

packages/tailwindcss-patch/src/runtime/class-collector.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,15 @@ export async function collectClassesFromTailwindV4(
6363
}
6464
const css = await fs.readFile(filePath, 'utf8')
6565
const entryDir = path.dirname(filePath)
66-
const baseForEntry = resolvedConfiguredBase ?? entryDir
67-
const sources = resolveSources(baseForEntry)
66+
const designSystemBases = resolvedConfiguredBase && resolvedConfiguredBase !== entryDir
67+
? [entryDir, resolvedConfiguredBase]
68+
: [entryDir]
69+
const sourcesBase = resolvedConfiguredBase ?? entryDir
70+
const sources = resolveSources(sourcesBase)
6871
const candidates = await extractValidCandidates({
6972
cwd: options.projectRoot,
70-
base: baseForEntry,
73+
base: designSystemBases[0],
74+
baseFallbacks: designSystemBases.slice(1),
7175
css,
7276
sources,
7377
})

packages/tailwindcss-patch/test/api.tailwindcss-patcher.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,58 @@ describe('TailwindcssPatcher', () => {
4141
expect(await fs.pathExists(outputFile)).toBe(true)
4242
})
4343

44+
it('resolves @config relative to the CSS entry directory even when base is set', async () => {
45+
const workspaceRoot = await fs.mkdtemp(path.join(__dirname, 'tmp-entry-base-'))
46+
try {
47+
const cssDir = path.join(workspaceRoot, 'src')
48+
await fs.ensureDir(cssDir)
49+
50+
const configPath = path.join(workspaceRoot, 'tailwind.config.js')
51+
await fs.writeFile(
52+
configPath,
53+
[
54+
'module.exports = {',
55+
' content: ["./src/**/*.{html,css}"],',
56+
' theme: {},',
57+
'};',
58+
].join('\n'),
59+
'utf8',
60+
)
61+
62+
const cssEntry = path.join(cssDir, 'app.css')
63+
await fs.writeFile(
64+
cssEntry,
65+
['@import "tailwindcss";', '@config "../tailwind.config.js";'].join('\n'),
66+
'utf8',
67+
)
68+
69+
const usageFile = path.join(cssDir, 'page.html')
70+
await fs.writeFile(usageFile, '<div class="px-[48rpx]"></div>', 'utf8')
71+
72+
const patcher = new TailwindcssPatcher({
73+
cwd: workspaceRoot,
74+
overwrite: false,
75+
cache: false,
76+
output: {
77+
enabled: false,
78+
},
79+
tailwind: {
80+
version: 4,
81+
v4: {
82+
base: workspaceRoot,
83+
cssEntries: [cssEntry],
84+
},
85+
},
86+
})
87+
88+
const result = await patcher.extract({ write: false })
89+
expect(result.classSet?.has('px-[48rpx]')).toBe(true)
90+
}
91+
finally {
92+
await fs.remove(workspaceRoot)
93+
}
94+
})
95+
4496
it('falls back to workspace sources when cssEntries directory is empty', async () => {
4597
const workspaceRoot = await fs.mkdtemp(path.join(__dirname, 'tmp-workspace-'))
4698
try {

0 commit comments

Comments
 (0)