diff --git a/CHANGELOG.md b/CHANGELOG.md index d55a21cdd..446e054e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/integrations/cli/config.test.ts b/integrations/cli/config.test.ts index 54ca8c0ca..6945079cf 100644 --- a/integrations/cli/config.test.ts +++ b/integrations/cli/config.test.ts @@ -84,6 +84,48 @@ test( }, ) +test( + 'Config files (TS)', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^" + } + } + `, + 'index.html': html` +
+ `, + '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` + + `, + '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', + ]) + }, +) diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 3517b6b24..fbfeb1157 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -38,5 +38,8 @@ }, "devDependencies": { "tailwindcss": "workspace:^" + }, + "dependencies": { + "jiti": "^2.0.0-beta.3" } } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index a5e4eb0d8..78c68d9ae 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -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