Skip to content

Commit 7347a2f

Browse files
Vite: Use Vite resolvers for CSS and JS files (#15173)
Closes #15159 This PR extends the `@tailwindcss/node` packages to be able to overwrite the CSS and JS resolvers. This is necessary as some bundlers, in particular Vite, have a custom module resolution system that can be individually configured. E.g. in Vite it is possible to add custom [resolver configs](https://vite.dev/config/shared-options.html#resolve-conditions) that is expected to be taken into account. With the new `customCssResolver` and `customJsResolver` option, we're able to use the Vite resolvers which take these configs into account. ## Test Plan Tested in the playground by configuring [resolver conditions](https://vite.dev/config/shared-options.html#resolve-conditions) (with Vite 5.4 and Vite 6 beta). An integration test was added for both the JS and CSS resolvers to ensure it keeps working as expected. --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent a1f78a2 commit 7347a2f

File tree

5 files changed

+231
-18
lines changed

5 files changed

+231
-18
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Ensure the Vite plugin resolves CSS and JS files according to the configured resolver conditions ([#15173])(https://github.com/tailwindlabs/tailwindcss/pull/15173)
1213
- _Upgrade (experimental)_: Migrate prefixes for `.group` and `.peer` classes ([#15208](https://github.com/tailwindlabs/tailwindcss/pull/15208))
1314

1415
### Fixed

integrations/vite/resolvers.test.ts

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, expect } from 'vitest'
2+
import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils'
3+
4+
for (let transformer of ['postcss', 'lightningcss']) {
5+
describe(transformer, () => {
6+
test(
7+
`resolves aliases in production build`,
8+
{
9+
fs: {
10+
'package.json': txt`
11+
{
12+
"type": "module",
13+
"dependencies": {
14+
"@tailwindcss/vite": "workspace:^",
15+
"tailwindcss": "workspace:^"
16+
},
17+
"devDependencies": {
18+
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
19+
"vite": "^5.3.5"
20+
}
21+
}
22+
`,
23+
'vite.config.ts': ts`
24+
import tailwindcss from '@tailwindcss/vite'
25+
import { defineConfig } from 'vite'
26+
import { fileURLToPath } from 'node:url'
27+
28+
export default defineConfig({
29+
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
30+
build: { cssMinify: false },
31+
plugins: [tailwindcss()],
32+
resolve: {
33+
alias: {
34+
'#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
35+
'#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
36+
},
37+
},
38+
})
39+
`,
40+
'index.html': html`
41+
<head>
42+
<link rel="stylesheet" href="./src/index.css" />
43+
</head>
44+
<body>
45+
<div class="underline custom-underline">Hello, world!</div>
46+
</body>
47+
`,
48+
'src/index.css': css`
49+
@import '#css-alias';
50+
@plugin '#js-alias';
51+
`,
52+
'src/alias.css': css`
53+
@import 'tailwindcss/theme' theme(reference);
54+
@import 'tailwindcss/utilities';
55+
`,
56+
'src/plugin.js': js`
57+
export default function ({ addUtilities }) {
58+
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
59+
}
60+
`,
61+
},
62+
},
63+
async ({ fs, exec }) => {
64+
await exec('pnpm vite build')
65+
66+
let files = await fs.glob('dist/**/*.css')
67+
expect(files).toHaveLength(1)
68+
let [filename] = files[0]
69+
70+
await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
71+
},
72+
)
73+
74+
test(
75+
`resolves aliases in dev mode`,
76+
{
77+
fs: {
78+
'package.json': txt`
79+
{
80+
"type": "module",
81+
"dependencies": {
82+
"@tailwindcss/vite": "workspace:^",
83+
"tailwindcss": "workspace:^"
84+
},
85+
"devDependencies": {
86+
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
87+
"vite": "^5.3.5"
88+
}
89+
}
90+
`,
91+
'vite.config.ts': ts`
92+
import tailwindcss from '@tailwindcss/vite'
93+
import { defineConfig } from 'vite'
94+
import { fileURLToPath } from 'node:url'
95+
96+
export default defineConfig({
97+
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
98+
build: { cssMinify: false },
99+
plugins: [tailwindcss()],
100+
resolve: {
101+
alias: {
102+
'#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
103+
'#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
104+
},
105+
},
106+
})
107+
`,
108+
'index.html': html`
109+
<head>
110+
<link rel="stylesheet" href="./src/index.css" />
111+
</head>
112+
<body>
113+
<div class="underline custom-underline">Hello, world!</div>
114+
</body>
115+
`,
116+
'src/index.css': css`
117+
@import '#css-alias';
118+
@plugin '#js-alias';
119+
`,
120+
'src/alias.css': css`
121+
@import 'tailwindcss/theme' theme(reference);
122+
@import 'tailwindcss/utilities';
123+
`,
124+
'src/plugin.js': js`
125+
export default function ({ addUtilities }) {
126+
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
127+
}
128+
`,
129+
},
130+
},
131+
async ({ root, spawn, getFreePort, fs }) => {
132+
let port = await getFreePort()
133+
await spawn(`pnpm vite dev --port ${port}`)
134+
135+
await retryAssertion(async () => {
136+
let styles = await fetchStyles(port, '/index.html')
137+
expect(styles).toContain(candidate`underline`)
138+
expect(styles).toContain(candidate`custom-underline`)
139+
})
140+
},
141+
)
142+
})
143+
}

packages/@tailwindcss-node/src/compile.ts

+50-9
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,33 @@ import {
1111
import { getModuleDependencies } from './get-module-dependencies'
1212
import { rewriteUrls } from './urls'
1313

14+
export type Resolver = (id: string, base: string) => Promise<string | false | undefined>
15+
1416
export async function compile(
1517
css: string,
1618
{
1719
base,
1820
onDependency,
1921
shouldRewriteUrls,
22+
23+
customCssResolver,
24+
customJsResolver,
2025
}: {
2126
base: string
2227
onDependency: (path: string) => void
2328
shouldRewriteUrls?: boolean
29+
30+
customCssResolver?: Resolver
31+
customJsResolver?: Resolver
2432
},
2533
) {
2634
let compiler = await _compile(css, {
2735
base,
2836
async loadModule(id, base) {
29-
return loadModule(id, base, onDependency)
37+
return loadModule(id, base, onDependency, customJsResolver)
3038
},
3139
async loadStylesheet(id, base) {
32-
let sheet = await loadStylesheet(id, base, onDependency)
40+
let sheet = await loadStylesheet(id, base, onDependency, customCssResolver)
3341

3442
if (shouldRewriteUrls) {
3543
sheet.content = await rewriteUrls({
@@ -80,9 +88,14 @@ export async function __unstable__loadDesignSystem(css: string, { base }: { base
8088
})
8189
}
8290

83-
export async function loadModule(id: string, base: string, onDependency: (path: string) => void) {
91+
export async function loadModule(
92+
id: string,
93+
base: string,
94+
onDependency: (path: string) => void,
95+
customJsResolver?: Resolver,
96+
) {
8497
if (id[0] !== '.') {
85-
let resolvedPath = await resolveJsId(id, base)
98+
let resolvedPath = await resolveJsId(id, base, customJsResolver)
8699
if (!resolvedPath) {
87100
throw new Error(`Could not resolve '${id}' from '${base}'`)
88101
}
@@ -94,7 +107,7 @@ export async function loadModule(id: string, base: string, onDependency: (path:
94107
}
95108
}
96109

97-
let resolvedPath = await resolveJsId(id, base)
110+
let resolvedPath = await resolveJsId(id, base, customJsResolver)
98111
if (!resolvedPath) {
99112
throw new Error(`Could not resolve '${id}' from '${base}'`)
100113
}
@@ -113,8 +126,13 @@ export async function loadModule(id: string, base: string, onDependency: (path:
113126
}
114127
}
115128

116-
async function loadStylesheet(id: string, base: string, onDependency: (path: string) => void) {
117-
let resolvedPath = await resolveCssId(id, base)
129+
async function loadStylesheet(
130+
id: string,
131+
base: string,
132+
onDependency: (path: string) => void,
133+
cssResolver?: Resolver,
134+
) {
135+
let resolvedPath = await resolveCssId(id, base, cssResolver)
118136
if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${base}'`)
119137

120138
onDependency(resolvedPath)
@@ -163,14 +181,25 @@ const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
163181
mainFields: ['style'],
164182
conditionNames: ['style'],
165183
})
166-
async function resolveCssId(id: string, base: string): Promise<string | false | undefined> {
184+
async function resolveCssId(
185+
id: string,
186+
base: string,
187+
customCssResolver?: Resolver,
188+
): Promise<string | false | undefined> {
167189
if (typeof globalThis.__tw_resolve === 'function') {
168190
let resolved = globalThis.__tw_resolve(id, base)
169191
if (resolved) {
170192
return Promise.resolve(resolved)
171193
}
172194
}
173195

196+
if (customCssResolver) {
197+
let customResolution = await customCssResolver(id, base)
198+
if (customResolution) {
199+
return customResolution
200+
}
201+
}
202+
174203
return runResolver(cssResolver, id, base)
175204
}
176205

@@ -188,13 +217,25 @@ const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
188217
conditionNames: ['node', 'require'],
189218
})
190219

191-
function resolveJsId(id: string, base: string): Promise<string | false | undefined> {
220+
async function resolveJsId(
221+
id: string,
222+
base: string,
223+
customJsResolver?: Resolver,
224+
): Promise<string | false | undefined> {
192225
if (typeof globalThis.__tw_resolve === 'function') {
193226
let resolved = globalThis.__tw_resolve(id, base)
194227
if (resolved) {
195228
return Promise.resolve(resolved)
196229
}
197230
}
231+
232+
if (customJsResolver) {
233+
let customResolution = await customJsResolver(id, base)
234+
if (customResolution) {
235+
return customResolution
236+
}
237+
}
238+
198239
return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
199240
}
200241

packages/@tailwindcss-vite/src/index.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,31 @@ export default function tailwindcss(): Plugin[] {
3535
let moduleGraphCandidates = new DefaultMap<string, Set<string>>(() => new Set<string>())
3636
let moduleGraphScanner = new Scanner({})
3737

38-
let roots: DefaultMap<string, Root> = new DefaultMap(
39-
(id) => new Root(id, () => moduleGraphCandidates, config!.base),
40-
)
38+
let roots: DefaultMap<string, Root> = new DefaultMap((id) => {
39+
let cssResolver = config!.createResolver({
40+
...config!.resolve,
41+
extensions: ['.css'],
42+
mainFields: ['style'],
43+
conditions: ['style', 'development|production'],
44+
tryIndex: false,
45+
preferRelative: true,
46+
})
47+
function customCssResolver(id: string, base: string) {
48+
return cssResolver(id, base, false, isSSR)
49+
}
50+
51+
let jsResolver = config!.createResolver(config!.resolve)
52+
function customJsResolver(id: string, base: string) {
53+
return jsResolver(id, base, true, isSSR)
54+
}
55+
return new Root(
56+
id,
57+
() => moduleGraphCandidates,
58+
config!.base,
59+
customCssResolver,
60+
customJsResolver,
61+
)
62+
})
4163

4264
function scanFile(id: string, content: string, extension: string, isSSR: boolean) {
4365
let updated = false
@@ -423,6 +445,9 @@ class Root {
423445
private id: string,
424446
private getSharedCandidates: () => Map<string, Set<string>>,
425447
private base: string,
448+
449+
private customCssResolver: (id: string, base: string) => Promise<string | false | undefined>,
450+
private customJsResolver: (id: string, base: string) => Promise<string | false | undefined>,
426451
) {}
427452

428453
// Generate the CSS for the root file. This can return false if the file is
@@ -448,6 +473,9 @@ class Root {
448473
addWatchFile(path)
449474
this.dependencies.add(path)
450475
},
476+
477+
customCssResolver: this.customCssResolver,
478+
customJsResolver: this.customJsResolver,
451479
})
452480
env.DEBUG && console.timeEnd('[@tailwindcss/vite] Setup compiler')
453481

pnpm-lock.yaml

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)