@@ -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,6 +313,13 @@ 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`
@@ -376,6 +384,40 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
376384 let end = process . hrtime . bigint ( )
377385 if ( ! args [ '--silent' ] ) eprintln ( `Done in ${ formatDuration ( end - start ) } ` )
378386 } catch ( err ) {
387+ // It's important that we perform a full rebuild when any of the
388+ // dependencies tracked in `fullRebuildPaths` has been changed.
389+ //
390+ // If we remove one of those files, then a subsequent build will be
391+ // triggered, but it will fail because the dependency is gone. The
392+ // compiler itself will be in a broken state and won't be able to
393+ // register any dependencies therefore we want to restore all the
394+ // dependencies from before. If we don't do that, then we won't be
395+ // able to recover from a bug in a transitive dependency.
396+ //
397+ // E.g.:
398+ // ```css
399+ // /* input.css — known full rebuild path */
400+ // @import 'tailwindcss';
401+ // @config "./tailwind.config.js";
402+ // ```
403+ //
404+ // ```js
405+ // // tailwind.config.js
406+ // const theme = require('./my-theme.js');
407+ //
408+ // module.exports = {
409+ // theme
410+ // }
411+ // ```
412+ // In this cse `my-theme.js` is a transitive dependency of
413+ // `input.css` via `tailwind.config.js`. Removing `my-theme.js` will
414+ // result in an error, restoring `my-theme.js` should trigger a
415+ // fresh build even though the compiler didn't restore.
416+ //
417+ // Once the build error is fixed, a fresh full rebuild will happen
418+ // which in turn will fixup the full rebuild paths.
419+ fullRebuildPaths = backupRebuildPaths
420+
379421 // Catch any errors and print them to stderr, but don't exit the process
380422 // and keep watching.
381423 eprintln (
@@ -488,10 +530,16 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
488530
489531 await Promise . all (
490532 events . map ( async ( event ) => {
491- // We currently don't handle deleted files because it doesn't influence
492- // the CSS output. This is because we currently keep all scanned
493- // candidates in a cache for performance reasons.
494- if ( event . type === 'delete' ) return
533+ // When a file is deleted, a rebuild should be triggered such that we
534+ // can figure out whether this file must trigger a fresh build or not.
535+ //
536+ // If it must trigger a fresh build, then we will temporarily end up
537+ // in a broken state, but an error will be shown to the user. Once the
538+ // user resolves the issue, the CLI will recover.
539+ if ( event . type === 'delete' ) {
540+ files . add ( event . path )
541+ return
542+ }
495543
496544 // Ignore directory changes. We only care about file changes
497545 let stats : Stats | null = null
0 commit comments