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.
This commit is contained in:
Philipp Spiess 2024-09-03 18:35:39 +02:00 committed by GitHub
parent 191c544c67
commit 390e2d3e8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 134 additions and 5 deletions

View File

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Support TypeScript for `@plugin` and `@config` files ([#14317](https://github.com/tailwindlabs/tailwindcss/pull/14317))
### Fixed
- Ensure content globs defined in `@config` files are relative to that file ([#14314](https://github.com/tailwindlabs/tailwindcss/pull/14314))

View File

@ -84,6 +84,48 @@ test(
},
)
test(
'Config files (TS)',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div class="text-primary"></div>
`,
'tailwind.config.ts': js`
export default {
theme: {
extend: {
colors: {
primary: 'blue',
},
},
},
} satisfies { theme: { extend: { colors: { primary: string } } } }
`,
'src/index.css': css`
@import 'tailwindcss';
@config '../tailwind.config.ts';
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css')
await fs.expectFileToContain('dist/out.css', [
//
candidate`text-primary`,
])
},
)
test(
'Config files (CJS, watch mode)',
{
@ -138,7 +180,7 @@ test(
)
test(
'Config files (MJS, watch mode)',
'Config files (ESM, watch mode)',
{
fs: {
'package.json': json`
@ -189,3 +231,56 @@ test(
])
},
)
test(
'Config files (TS, watch mode)',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div class="text-primary"></div>
`,
'tailwind.config.ts': js`
import myColor from './my-color.ts'
export default {
theme: {
extend: {
colors: {
primary: myColor,
},
},
},
}
`,
'my-color.ts': js`export default 'blue'`,
'src/index.css': css`
@import 'tailwindcss';
@config '../tailwind.config.ts';
`,
},
},
async ({ fs, spawn }) => {
await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
await fs.expectFileToContain('dist/out.css', [
//
candidate`text-primary`,
'color: blue',
])
await fs.write('my-color.ts', js`export default 'red'`)
await fs.expectFileToContain('dist/out.css', [
//
candidate`text-primary`,
'color: red',
])
},
)

View File

@ -38,5 +38,8 @@
},
"devDependencies": {
"tailwindcss": "workspace:^"
},
"dependencies": {
"jiti": "^2.0.0-beta.3"
}
}

View File

@ -1,3 +1,4 @@
import { createJiti, type Jiti } from 'jiti'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import { compile as _compile } from 'tailwindcss'
@ -10,12 +11,12 @@ export async function compile(
return await _compile(css, {
loadPlugin: async (pluginPath) => {
if (pluginPath[0] !== '.') {
return import(pluginPath).then((m) => m.default ?? m)
return importModule(pluginPath).then((m) => m.default ?? m)
}
let resolvedPath = path.resolve(base, pluginPath)
let [module, moduleDependencies] = await Promise.all([
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
getModuleDependencies(resolvedPath),
])
@ -28,12 +29,12 @@ export async function compile(
loadConfig: async (configPath) => {
if (configPath[0] !== '.') {
return import(configPath).then((m) => m.default ?? m)
return importModule(configPath).then((m) => m.default ?? m)
}
let resolvedPath = path.resolve(base, configPath)
let [module, moduleDependencies] = await Promise.all([
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
getModuleDependencies(resolvedPath),
])
@ -45,3 +46,19 @@ export async function compile(
},
})
}
// Attempts to import the module using the native `import()` function. If this
// fails, it sets up `jiti` and attempts to import this way so that `.ts` files
// can be resolved properly.
let jiti: null | Jiti = null
async function importModule(path: string): Promise<any> {
try {
return await import(path)
} catch (error) {
try {
jiti ??= createJiti(import.meta.url, { moduleCache: false, fsCache: false })
return await jiti.import(path)
} catch {}
throw error
}
}

10
pnpm-lock.yaml generated
View File

@ -174,6 +174,10 @@ importers:
version: link:../internal-postcss-fix-relative-paths
packages/@tailwindcss-node:
dependencies:
jiti:
specifier: ^2.0.0-beta.3
version: 2.0.0-beta.3
devDependencies:
tailwindcss:
specifier: workspace:^
@ -2127,6 +2131,10 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
jiti@2.0.0-beta.3:
resolution: {integrity: sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ==}
hasBin: true
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@ -4892,6 +4900,8 @@ snapshots:
jiti@1.21.6:
optional: true
jiti@2.0.0-beta.3: {}
joycon@3.1.1: {}
js-tokens@4.0.0: {}