Skip to content

Commit 8e09524

Browse files
committed
ensure @tailwindcss/cli builds recover from deleted files
1 parent aaea970 commit 8e09524

1 file changed

Lines changed: 57 additions & 4 deletions

File tree

  • packages/@tailwindcss-cli/src/commands/build

packages/@tailwindcss-cli/src/commands/build/index.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
223223
let inputBasePath = inputFilePath ? path.dirname(inputFilePath) : process.cwd()
224224

225225
let fullRebuildPaths: string[] = inputFilePath ? [inputFilePath] : []
226+
let backupRebuildPaths = fullRebuildPaths
226227

227228
async function createCompiler(css: string, I: Instrumentation) {
228229
DEBUG && I.start('Setup compiler')
@@ -312,11 +313,23 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
312313
@import 'tailwindcss';
313314
`
314315
clearRequireCache(resolvedFullRebuildPaths)
316+
317+
// Track current rebuild paths in case something goes wrong when
318+
// performing a full rebuild.
319+
backupRebuildPaths = fullRebuildPaths.slice()
320+
321+
// The `inputFilePath`, if provided, will be the only known full
322+
// rebuild path before the compiler is re-created.
315323
fullRebuildPaths = inputFilePath ? [inputFilePath] : []
316324

317325
// Create a new compiler, given the new `input`
318326
;[compiler, scanner] = await createCompiler(input, I)
319327

328+
// Succesfully created a new compiler, so the `fullRebuildPaths`
329+
// will be updated. If other errors occur, we should be able to
330+
// restore the paths unconditionally.
331+
backupRebuildPaths = fullRebuildPaths.slice()
332+
320333
// Scan the directory for candidates
321334
DEBUG && I.start('Scan for candidates')
322335
let candidates = scanner.scan()
@@ -376,6 +389,40 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
376389
let end = process.hrtime.bigint()
377390
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
378391
} catch (err) {
392+
// It's important that we perform a full rebuild when any of the
393+
// dependencies tracked in `fullRebuildPaths` has been changed.
394+
//
395+
// If we remove one of those files, then a subsequent build will be
396+
// triggered, but it will fail because the dependency is gone. The
397+
// compiler itself will be in a broken state and won't be able to
398+
// register any dependencies therefore we want to restore all the
399+
// dependencies from before. If we don't do that, then we won't be
400+
// able to recover from a bug in a transitive dependency.
401+
//
402+
// E.g.:
403+
// ```css
404+
// /* input.css — known full rebuild path */
405+
// @import 'tailwindcss';
406+
// @config "./tailwind.config.js";
407+
// ```
408+
//
409+
// ```js
410+
// // tailwind.config.js
411+
// const theme = require('./my-theme.js');
412+
//
413+
// module.exports = {
414+
// theme
415+
// }
416+
// ```
417+
// In this case `my-theme.js` is a transitive dependency of
418+
// `input.css` via `tailwind.config.js`. Removing `my-theme.js` will
419+
// result in an error, restoring `my-theme.js` should trigger a
420+
// fresh build even though the compiler didn't restore.
421+
//
422+
// Once the build error is fixed, a fresh full rebuild will happen
423+
// which in turn will fixup the full rebuild paths.
424+
fullRebuildPaths = backupRebuildPaths
425+
379426
// Catch any errors and print them to stderr, but don't exit the process
380427
// and keep watching.
381428
eprintln(
@@ -489,10 +536,16 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
489536

490537
await Promise.all(
491538
events.map(async (event) => {
492-
// We currently don't handle deleted files because it doesn't influence
493-
// the CSS output. This is because we currently keep all scanned
494-
// candidates in a cache for performance reasons.
495-
if (event.type === 'delete') return
539+
// When a file is deleted, a rebuild should be triggered such that we
540+
// can figure out whether this file must trigger a fresh build or not.
541+
//
542+
// If it must trigger a fresh build, then we will temporarily end up
543+
// in a broken state, but an error will be shown to the user. Once the
544+
// user resolves the issue, the CLI will recover.
545+
if (event.type === 'delete') {
546+
files.add(event.path)
547+
return
548+
}
496549

497550
// Ignore directory changes. We only care about file changes
498551
let stats: Stats | null = null

0 commit comments

Comments
 (0)