Skip to content

Commit 390e2d3

Browse files
Allow @plugin and @config to point to TS files (#14317)
Tailwind V3 used [jiti](https://github.com/unjs/jiti/) to allow importing of TypeScript files for the config and plugins. This PR adds the new Jiti V2 beta to our `@tailwindcss/node` and uses it if a native `import()` fails. I added a new integration test to the CLI config setup, to ensure it still works with our module cache cleanup.
1 parent 191c544 commit 390e2d3

File tree

5 files changed

+134
-5
lines changed

5 files changed

+134
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Support TypeScript for `@plugin` and `@config` files ([#14317](https://github.com/tailwindlabs/tailwindcss/pull/14317))
13+
1014
### Fixed
1115

1216
- Ensure content globs defined in `@config` files are relative to that file ([#14314](https://github.com/tailwindlabs/tailwindcss/pull/14314))

integrations/cli/config.test.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,48 @@ test(
8484
},
8585
)
8686

87+
test(
88+
'Config files (TS)',
89+
{
90+
fs: {
91+
'package.json': json`
92+
{
93+
"dependencies": {
94+
"tailwindcss": "workspace:^",
95+
"@tailwindcss/cli": "workspace:^"
96+
}
97+
}
98+
`,
99+
'index.html': html`
100+
<div class="text-primary"></div>
101+
`,
102+
'tailwind.config.ts': js`
103+
export default {
104+
theme: {
105+
extend: {
106+
colors: {
107+
primary: 'blue',
108+
},
109+
},
110+
},
111+
} satisfies { theme: { extend: { colors: { primary: string } } } }
112+
`,
113+
'src/index.css': css`
114+
@import 'tailwindcss';
115+
@config '../tailwind.config.ts';
116+
`,
117+
},
118+
},
119+
async ({ fs, exec }) => {
120+
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
121+
122+
await fs.expectFileToContain('dist/out.css', [
123+
//
124+
candidate`text-primary`,
125+
])
126+
},
127+
)
128+
87129
test(
88130
'Config files (CJS, watch mode)',
89131
{
@@ -138,7 +180,7 @@ test(
138180
)
139181

140182
test(
141-
'Config files (MJS, watch mode)',
183+
'Config files (ESM, watch mode)',
142184
{
143185
fs: {
144186
'package.json': json`
@@ -189,3 +231,56 @@ test(
189231
])
190232
},
191233
)
234+
235+
test(
236+
'Config files (TS, watch mode)',
237+
{
238+
fs: {
239+
'package.json': json`
240+
{
241+
"dependencies": {
242+
"tailwindcss": "workspace:^",
243+
"@tailwindcss/cli": "workspace:^"
244+
}
245+
}
246+
`,
247+
'index.html': html`
248+
<div class="text-primary"></div>
249+
`,
250+
'tailwind.config.ts': js`
251+
import myColor from './my-color.ts'
252+
export default {
253+
theme: {
254+
extend: {
255+
colors: {
256+
primary: myColor,
257+
},
258+
},
259+
},
260+
}
261+
`,
262+
'my-color.ts': js`export default 'blue'`,
263+
'src/index.css': css`
264+
@import 'tailwindcss';
265+
@config '../tailwind.config.ts';
266+
`,
267+
},
268+
},
269+
async ({ fs, spawn }) => {
270+
await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
271+
272+
await fs.expectFileToContain('dist/out.css', [
273+
//
274+
candidate`text-primary`,
275+
'color: blue',
276+
])
277+
278+
await fs.write('my-color.ts', js`export default 'red'`)
279+
280+
await fs.expectFileToContain('dist/out.css', [
281+
//
282+
candidate`text-primary`,
283+
'color: red',
284+
])
285+
},
286+
)

packages/@tailwindcss-node/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,8 @@
3838
},
3939
"devDependencies": {
4040
"tailwindcss": "workspace:^"
41+
},
42+
"dependencies": {
43+
"jiti": "^2.0.0-beta.3"
4144
}
4245
}

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createJiti, type Jiti } from 'jiti'
12
import path from 'node:path'
23
import { pathToFileURL } from 'node:url'
34
import { compile as _compile } from 'tailwindcss'
@@ -10,12 +11,12 @@ export async function compile(
1011
return await _compile(css, {
1112
loadPlugin: async (pluginPath) => {
1213
if (pluginPath[0] !== '.') {
13-
return import(pluginPath).then((m) => m.default ?? m)
14+
return importModule(pluginPath).then((m) => m.default ?? m)
1415
}
1516

1617
let resolvedPath = path.resolve(base, pluginPath)
1718
let [module, moduleDependencies] = await Promise.all([
18-
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
19+
importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
1920
getModuleDependencies(resolvedPath),
2021
])
2122

@@ -28,12 +29,12 @@ export async function compile(
2829

2930
loadConfig: async (configPath) => {
3031
if (configPath[0] !== '.') {
31-
return import(configPath).then((m) => m.default ?? m)
32+
return importModule(configPath).then((m) => m.default ?? m)
3233
}
3334

3435
let resolvedPath = path.resolve(base, configPath)
3536
let [module, moduleDependencies] = await Promise.all([
36-
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
37+
importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
3738
getModuleDependencies(resolvedPath),
3839
])
3940

@@ -45,3 +46,19 @@ export async function compile(
4546
},
4647
})
4748
}
49+
50+
// Attempts to import the module using the native `import()` function. If this
51+
// fails, it sets up `jiti` and attempts to import this way so that `.ts` files
52+
// can be resolved properly.
53+
let jiti: null | Jiti = null
54+
async function importModule(path: string): Promise<any> {
55+
try {
56+
return await import(path)
57+
} catch (error) {
58+
try {
59+
jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false })
60+
return await jiti.import(path)
61+
} catch {}
62+
throw error
63+
}
64+
}

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)