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

@ -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()),
)
}