From 390e2d3e8da5e242c7342be4915d9c6eceabf5d5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 3 Sep 2024 18:35:39 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 4 + integrations/cli/config.test.ts | 97 ++++++++++++++++++++++- packages/@tailwindcss-node/package.json | 3 + packages/@tailwindcss-node/src/compile.ts | 25 +++++- pnpm-lock.yaml | 10 +++ 5 files changed, 134 insertions(+), 5 deletions(-) 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 { + 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 + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b55fb9616..db4a9a9a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}