diff --git a/integrations/cli/config.test.ts b/integrations/cli/config.test.ts
index 2780e9aca..54ca8c0ca 100644
--- a/integrations/cli/config.test.ts
+++ b/integrations/cli/config.test.ts
@@ -83,3 +83,109 @@ test(
])
},
)
+
+test(
+ 'Config files (CJS, watch mode)',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'index.html': html`
+
+ `,
+ 'tailwind.config.js': js`
+ const myColor = require('./my-color')
+ module.exports = {
+ theme: {
+ extend: {
+ colors: {
+ primary: myColor,
+ },
+ },
+ },
+ }
+ `,
+ 'my-color.js': js`module.exports = 'blue'`,
+ 'src/index.css': css`
+ @import 'tailwindcss';
+ @config '../tailwind.config.js';
+ `,
+ },
+ },
+ 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.js', js`module.exports = 'red'`)
+
+ await fs.expectFileToContain('dist/out.css', [
+ //
+ candidate`text-primary`,
+ 'color: red',
+ ])
+ },
+)
+
+test(
+ 'Config files (MJS, watch mode)',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'index.html': html`
+
+ `,
+ 'tailwind.config.mjs': js`
+ import myColor from './my-color.mjs'
+ export default {
+ theme: {
+ extend: {
+ colors: {
+ primary: myColor,
+ },
+ },
+ },
+ }
+ `,
+ 'my-color.mjs': js`export default 'blue'`,
+ 'src/index.css': css`
+ @import 'tailwindcss';
+ @config '../tailwind.config.mjs';
+ `,
+ },
+ },
+ 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.mjs', js`export default 'red'`)
+
+ await fs.expectFileToContain('dist/out.css', [
+ //
+ candidate`text-primary`,
+ 'color: red',
+ ])
+ },
+)
diff --git a/packages/@tailwindcss-cli/hook/esm.hook.mjs b/packages/@tailwindcss-cli/hook/esm.hook.mjs
index 6f693b817..bde8538b0 100644
--- a/packages/@tailwindcss-cli/hook/esm.hook.mjs
+++ b/packages/@tailwindcss-cli/hook/esm.hook.mjs
@@ -1,3 +1,3 @@
import { register } from 'node:module'
-register(import.meta.resolve('./esm.loader.mjs'))
+register(import.meta.resolve('@tailwindcss/cli/esm-loader'))
diff --git a/packages/@tailwindcss-cli/hook/esm.loader.mjs b/packages/@tailwindcss-cli/hook/esm.loader.mjs
index b21920a52..b7508c778 100644
--- a/packages/@tailwindcss-cli/hook/esm.loader.mjs
+++ b/packages/@tailwindcss-cli/hook/esm.loader.mjs
@@ -13,9 +13,10 @@ export async function resolve(specifier, context, nextResolve) {
if (id === null) return result
let url = new URL(result.url)
-
url.searchParams.set('id', id)
+ console.log({ url })
+
return {
...result,
url: `${url}`,
diff --git a/packages/@tailwindcss-cli/hook/require-cache.cjs b/packages/@tailwindcss-cli/hook/require-cache.cjs
new file mode 100644
index 000000000..d073d8701
--- /dev/null
+++ b/packages/@tailwindcss-cli/hook/require-cache.cjs
@@ -0,0 +1,5 @@
+module.exports = function clearRequireCache() {
+ for (const key in require.cache) {
+ delete require.cache[key]
+ }
+}
diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json
index 4a5310492..944fdb2ca 100644
--- a/packages/@tailwindcss-cli/package.json
+++ b/packages/@tailwindcss-cli/package.json
@@ -19,10 +19,14 @@
"tailwindcss": "./dist/index.mjs"
},
"exports": {
- "./package.json": "./package.json"
+ "./package.json": "./package.json",
+ "./esm-hook": "./hook/esm.hook.mjs",
+ "./esm-loader": "./hook/esm.loader.mjs",
+ "./require-cache": "./hook/require-cache.cjs"
},
"files": [
- "dist"
+ "dist",
+ "hook"
],
"publishConfig": {
"provenance": true,
diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts
index 84e17ee71..c5db54c46 100644
--- a/packages/@tailwindcss-cli/src/commands/build/index.ts
+++ b/packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -11,6 +11,7 @@ import atImport from 'postcss-import'
import * as tailwindcss from 'tailwindcss'
import type { Arg, Result } from '../../utils/args'
import { Disposables } from '../../utils/disposables'
+import { getModuleDependencies } from '../../utils/get-module-dependencies'
import {
eprintln,
formatDuration,
@@ -22,6 +23,10 @@ import {
import { resolveCssId } from '../../utils/resolve'
import { drainStdin, outputFile } from './utils'
+import '@tailwindcss/cli/esm-hook'
+// @ts-ignore
+import clearRequireCache from '@tailwindcss/cli/require-cache'
+
const css = String.raw
export function options() {
@@ -128,27 +133,35 @@ export async function handle(args: Result>) {
let inputFile = args['--input'] && args['--input'] !== '-' ? args['--input'] : process.cwd()
let inputBasePath = path.dirname(path.resolve(inputFile))
+ let fullRebuildPaths = [...cssImportPaths]
function compile(css: string) {
return tailwindcss.compile(css, {
loadPlugin: async (pluginPath) => {
- if (pluginPath[0] === '.') {
- return import(pathToFileURL(path.resolve(inputBasePath, pluginPath)).href).then(
- (m) => m.default ?? m,
- )
+ if (pluginPath[0] !== '.') {
+ return import(pluginPath).then((m) => m.default ?? m)
}
- return import(pluginPath).then((m) => m.default ?? m)
+ let resolvedPath = path.resolve(inputBasePath, pluginPath)
+ fullRebuildPaths.push(resolvedPath)
+ fullRebuildPaths.push(...getModuleDependencies(resolvedPath))
+ return import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()).then(
+ (m) => m.default ?? m,
+ )
},
loadConfig: async (configPath) => {
- if (configPath[0] === '.') {
- return import(pathToFileURL(path.resolve(inputBasePath, configPath)).href).then(
- (m) => m.default ?? m,
- )
+ if (configPath[0] !== '.') {
+ return import(configPath).then((m) => m.default ?? m)
}
- return import(configPath).then((m) => m.default ?? m)
+ console.log('RELOAD')
+ let resolvedPath = path.resolve(inputBasePath, configPath)
+ fullRebuildPaths.push(resolvedPath)
+ fullRebuildPaths.push(...getModuleDependencies(resolvedPath))
+ return import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()).then(
+ (m) => m.default ?? m,
+ )
},
})
}
@@ -177,9 +190,11 @@ export async function handle(args: Result>) {
let rebuildStrategy: 'incremental' | 'full' = 'incremental'
for (let file of files) {
- // If one of the changed files is related to the input CSS files, then
- // we need to do a full rebuild because the theme might have changed.
- if (cssImportPaths.includes(file)) {
+ // If one of the changed files is related to the input CSS or JS
+ // config/plugin files, then we need to do a full rebuild because
+ // the theme might have changed.
+ if (fullRebuildPaths.includes(file)) {
+ console.log('#############')
rebuildStrategy = 'full'
// No need to check the rest of the events, because we already know we
@@ -214,6 +229,8 @@ export async function handle(args: Result>) {
`,
args['--input'] ?? base,
)
+ fullRebuildPaths = cssImportPaths
+ clearRequireCache()
// Create a new compiler, given the new `input`
compiler = await compile(input)
diff --git a/packages/@tailwindcss-cli/src/utils/get-module-dependencies.ts b/packages/@tailwindcss-cli/src/utils/get-module-dependencies.ts
new file mode 100644
index 000000000..592306641
--- /dev/null
+++ b/packages/@tailwindcss-cli/src/utils/get-module-dependencies.ts
@@ -0,0 +1,85 @@
+import fs from 'fs'
+import path from 'path'
+
+let jsExtensions = ['.js', '.cjs', '.mjs']
+
+// Given the current file `a.ts`, we want to make sure that when importing `b` that we resolve
+// `b.ts` before `b.js`
+//
+// E.g.:
+//
+// a.ts
+// b // .ts
+// c // .ts
+// a.js
+// b // .js or .ts
+
+let jsResolutionOrder = ['', '.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx']
+let tsResolutionOrder = ['', '.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx']
+
+function resolveWithExtension(file: string, extensions: string[]) {
+ // Try to find `./a.ts`, `./a.cts`, ... from `./a`
+ for (let ext of extensions) {
+ let full = `${file}${ext}`
+ if (fs.existsSync(full) && fs.statSync(full).isFile()) {
+ return full
+ }
+ }
+
+ // Try to find `./a/index.js` from `./a`
+ for (let ext of extensions) {
+ let full = `${file}/index${ext}`
+ if (fs.existsSync(full)) {
+ return full
+ }
+ }
+
+ return null
+}
+
+function* _getModuleDependencies(
+ filename: string,
+ base: string,
+ seen: Set,
+ ext = path.extname(filename),
+): Iterable {
+ // Try to find the file
+ let absoluteFile = resolveWithExtension(
+ path.resolve(base, filename),
+ jsExtensions.includes(ext) ? jsResolutionOrder : tsResolutionOrder,
+ )
+ if (absoluteFile === null) return // File doesn't exist
+
+ // Prevent infinite loops when there are circular dependencies
+ if (seen.has(absoluteFile)) return // Already seen
+ seen.add(absoluteFile)
+
+ // Mark the file as a dependency
+ yield absoluteFile
+
+ // Resolve new base for new imports/requires
+ base = path.dirname(absoluteFile)
+ ext = path.extname(absoluteFile)
+
+ let contents = fs.readFileSync(absoluteFile, 'utf-8')
+
+ // Find imports/requires
+ for (let match of [
+ ...contents.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),
+ ...contents.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),
+ ...contents.matchAll(/export[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),
+ ...contents.matchAll(/require\(['"`](.+)['"`]\)/gi),
+ ]) {
+ // Bail out if it's not a relative file
+ if (!match[1].startsWith('.')) continue
+
+ yield* _getModuleDependencies(match[1], base, seen, ext)
+ }
+}
+
+export function getModuleDependencies(absoluteFilePath: string) {
+ if (absoluteFilePath === null) return new Set()
+ return new Set(
+ _getModuleDependencies(absoluteFilePath, path.dirname(absoluteFilePath), new Set()),
+ )
+}
diff --git a/packages/tailwindcss/src/plugin-api.ts b/packages/tailwindcss/src/plugin-api.ts
index 99324b070..4b74811e0 100644
--- a/packages/tailwindcss/src/plugin-api.ts
+++ b/packages/tailwindcss/src/plugin-api.ts
@@ -366,8 +366,9 @@ export function registerPlugins(
handler(pluginApi)
}
- // Theme keys don't need to read from the compat config because core plugins
- // _already_ look through "fallback" theme keys
+ // Merge the user-configured theme keys into the design system. The compat
+ // config would otherwise expand into namespaces like `background-color` which
+ // core utilities already read from.
mergeIntoTheme(designSystem, userConfig)
return {