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