Detect changes to the config file and plugins for the CLI

This commit is contained in:
Philipp Spiess 2024-08-23 13:33:12 +02:00
parent 3a7feb7930
commit d607f0a40e
8 changed files with 238 additions and 19 deletions

View file

@ -83,3 +83,109 @@ test(
])
},
)
test(
'Config files (CJS, watch mode)',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'index.html': html`
<div class="text-primary"></div>
`,
'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`
<div class="text-primary"></div>
`,
'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',
])
},
)

View file

@ -1,3 +1,3 @@
import { register } from 'node:module'
register(import.meta.resolve('./esm.loader.mjs'))
register(import.meta.resolve('@tailwindcss/cli/esm-loader'))

View file

@ -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}`,

View file

@ -0,0 +1,5 @@
module.exports = function clearRequireCache() {
for (const key in require.cache) {
delete require.cache[key]
}
}

View file

@ -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,

View file

@ -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<ReturnType<typeof options>>) {
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<ReturnType<typeof options>>) {
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<ReturnType<typeof options>>) {
`,
args['--input'] ?? base,
)
fullRebuildPaths = cssImportPaths
clearRequireCache()
// Create a new compiler, given the new `input`
compiler = await compile(input)

View file

@ -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<string>,
ext = path.extname(filename),
): Iterable<string> {
// 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<string>()
return new Set(
_getModuleDependencies(absoluteFilePath, path.dirname(absoluteFilePath), new Set()),
)
}

View file

@ -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 {