Detect changes to the config file and plugins for the CLI
This commit is contained in:
parent
3a7feb7930
commit
d607f0a40e
8 changed files with 238 additions and 19 deletions
|
|
@ -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',
|
||||
])
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
import { register } from 'node:module'
|
||||
|
||||
register(import.meta.resolve('./esm.loader.mjs'))
|
||||
register(import.meta.resolve('@tailwindcss/cli/esm-loader'))
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
5
packages/@tailwindcss-cli/hook/require-cache.cjs
Normal file
5
packages/@tailwindcss-cli/hook/require-cache.cjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = function clearRequireCache() {
|
||||
for (const key in require.cache) {
|
||||
delete require.cache[key]
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue