From 9c3d968fae84246dd1b0ced4b97870b96d92ec94 Mon Sep 17 00:00:00 2001
From: SomSamantray <>
Date: Wed, 5 Aug 2026 20:46:51 +0530
Subject: [PATCH 1/4] fix(cli): exclude noise directories from --watch file
watcher
Prevent the CLI's file watcher from recursing into node_modules, .git,
.hg, and .svn by passing an ignore option to @parcel/watcher's
subscribe(). Previously these directories were watched with no
filtering, causing spurious rebuild cycles and contributing to the
reported CPU-pegging hang on cold Watchman starts against large,
uncached trees.
Fixes #17246
---
.../src/commands/build/index.ts | 79 +++++++++++--------
1 file changed, 45 insertions(+), 34 deletions(-)
diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts
index c85eda28c0d9..c33f7355b96b 100644
--- a/packages/@tailwindcss-cli/src/commands/build/index.ts
+++ b/packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -28,6 +28,13 @@ const css = String.raw
const DEBUG = env.DEBUG
const DEFAULT_POLL_INTERVAL_MS = 250
+// Directories that should never be watched, regardless of which project
+// directories are being watched. Watching these can cause excessive CPU
+// usage or hangs on file watcher backends (e.g. Watchman) when they contain
+// large or frequently-changing trees, and Tailwind never needs to react to
+// changes inside them.
+const DEFAULT_WATCH_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.hg/**', '**/.svn/**']
+
export function options() {
return {
'--input': {
@@ -747,44 +754,48 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
// Setup a watcher for every directory.
for (let dir of dirs) {
- let { unsubscribe } = await watcher.subscribe(dir, async (err, events) => {
- // Whenever an error occurs we want to let the user know about it but we
- // want to keep watching for changes.
- if (err) {
- console.error(err)
- return
- }
+ let { unsubscribe } = await watcher.subscribe(
+ dir,
+ async (err, events) => {
+ // Whenever an error occurs we want to let the user know about it but we
+ // want to keep watching for changes.
+ if (err) {
+ console.error(err)
+ return
+ }
- await Promise.all(
- events.map(async (event) => {
- // When a file is deleted, a rebuild should be triggered such that we
- // can figure out whether this file must trigger a fresh build or not.
- //
- // If it must trigger a fresh build, then we will temporarily end up
- // in a broken state, but an error will be shown to the user. Once the
- // user resolves the issue, the CLI will recover.
- if (event.type === 'delete') {
- files.add(event.path)
- return
- }
+ await Promise.all(
+ events.map(async (event) => {
+ // When a file is deleted, a rebuild should be triggered such that we
+ // can figure out whether this file must trigger a fresh build or not.
+ //
+ // If it must trigger a fresh build, then we will temporarily end up
+ // in a broken state, but an error will be shown to the user. Once the
+ // user resolves the issue, the CLI will recover.
+ if (event.type === 'delete') {
+ files.add(event.path)
+ return
+ }
- // Ignore directory changes. We only care about file changes
- let stats: Stats | null = null
- try {
- stats = await fs.lstat(event.path)
- } catch {}
- if (!stats?.isFile() && !stats?.isSymbolicLink()) {
- return
- }
+ // Ignore directory changes. We only care about file changes
+ let stats: Stats | null = null
+ try {
+ stats = await fs.lstat(event.path)
+ } catch {}
+ if (!stats?.isFile() && !stats?.isSymbolicLink()) {
+ return
+ }
- // Track the changed file.
- files.add(event.path)
- }),
- )
+ // Track the changed file.
+ files.add(event.path)
+ }),
+ )
- // Handle the tracked files at some point in the future.
- await enqueueCallback()
- })
+ // Handle the tracked files at some point in the future.
+ await enqueueCallback()
+ },
+ { ignore: DEFAULT_WATCH_IGNORE },
+ )
// Ensure we cleanup the watcher when we're done.
watchers.add(unsubscribe)
From d5b8ac915cd1039a1ca53eea49308ea456511b54 Mon Sep 17 00:00:00 2001
From: SomSamantray <>
Date: Wed, 5 Aug 2026 20:51:56 +0530
Subject: [PATCH 2/4] test(cli): verify watch mode ignores node_modules and
.git
Adds coverage for the ignore-option fix: asserts that changes inside
node_modules or .git no longer trigger a watcher rebuild cycle, while
confirming the watcher still reacts to real source changes.
Could not be executed in this environment (requires a full monorepo
build producing dist/ tarballs, which needs the native Oxide crate);
the underlying mechanism was independently verified against the
installed @parcel/watcher@2.6.0 binary directly.
---
integrations/cli/index.test.ts | 35 ++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index 310086802779..876995b7796c 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -375,6 +375,41 @@ describe.each([
},
)
+ // https://github.com/tailwindlabs/tailwindcss/issues/17246
+ test(
+ 'watch mode does not rebuild for changes inside node_modules or .git',
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'index.html': html`
`,
+ 'src/index.css': css` @import 'tailwindcss/utilities'; `,
+ 'node_modules/some-dep/index.js': js` module.exports = {} `,
+ '.git/HEAD': txt`ref: refs/heads/main`,
+ },
+ },
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`)
+ await process.onStderr((m) => m.includes('Done in'))
+ process.flush()
+
+ await fs.write('node_modules/some-dep/index.js', js`module.exports = { touched: true }`)
+ await fs.write('.git/HEAD', txt`ref: refs/heads/other`)
+
+ // A watcher rebuild cycle (real or a no-op early return) always logs
+ // "Done in", so if the watcher incorrectly reacted to either change
+ // above, this would resolve. It must not.
+ let sawRebuild = await Promise.race([
+ process.onStderr((m) => m.includes('Done in')).then(() => true),
+ new Promise((resolve) => setTimeout(() => resolve(false), 1000)),
+ ])
+ expect(sawRebuild).toBe(false)
+
+ // Sanity check: the watcher is still alive and reacts to real changes.
+ await fs.write('index.html', html``)
+ await fs.expectFileToContain('dist/out.css', [candidate`flex`])
+ },
+ )
+
test(
'watch mode with polling',
{
From 45921ede3c3666c810abd9f05f5b35bc6f7adbab Mon Sep 17 00:00:00 2001
From: SomSamantray <>
Date: Wed, 5 Aug 2026 21:04:33 +0530
Subject: [PATCH 3/4] fix(cli): don't ignore noise dirs that contain an
explicit @source
Two independent code-review passes (correctness and reliability) found
that a blanket node_modules/.git ignore on every watcher.subscribe()
call can silently break watch-mode rebuilds for an explicit @source
path living inside node_modules, if that path's own watch root gets
collapsed into a broader ancestor by the existing dedup step -- since
@parcel/watcher matches ignore globs relative to the watched root.
watchIgnoreFor() now skips ignoring a noise segment for a given
directory when another originally-requested directory got collapsed
into it and lives inside that segment. Verified empirically against
the installed @parcel/watcher@2.6.0 binary directly (0 events before
this fix for the nested case, 2 after -- matching pre-regression
behavior), and with isolated unit tests of the pure ignore-list logic.
Also fixes a test timeout that wasn't scaled for Windows like the rest
of the suite (per testing-reviewer), and adds e2e coverage for the
nested-@source-inside-node_modules scenario.
---
integrations/cli/index.test.ts | 38 +++++++++++++++++
.../src/commands/build/index.ts | 41 +++++++++++++++----
2 files changed, 72 insertions(+), 7 deletions(-)
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index 876995b7796c..a96eee321324 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -410,6 +410,44 @@ describe.each([
},
)
+ // https://github.com/tailwindlabs/tailwindcss/issues/17246
+ test(
+ 'watch mode still rebuilds for an explicit @source nested inside node_modules, even when its watch root is collapsed into a broader ancestor',
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ @source '../**/*.html';
+ @source '../node_modules/my-lib/src/*.html';
+ `,
+ 'index.html': html``,
+ // This lives inside node_modules, but is explicitly opted back in via
+ // @source above. The broader `../**/*.html` source also covers the
+ // project root, so this directory gets collapsed into it by
+ // createWatchers' dedup step -- the ignore filter must not apply to
+ // node_modules for that collapsed root.
+ 'node_modules/my-lib/src/index.html': html`
+
+ `,
+ },
+ },
+ async ({ fs, spawn }) => {
+ let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`)
+ await process.onStderr((m) => m.includes('Done in'))
+
+ await fs.expectFileToContain('dist/out.css', [candidate`content-['initial']`])
+
+ await fs.write(
+ 'node_modules/my-lib/src/index.html',
+ html``,
+ )
+ await fs.expectFileToContain('dist/out.css', [candidate`content-['changed']`])
+ },
+ )
+
test(
'watch mode with polling',
{
diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts
index c33f7355b96b..54b5ce5b07a2 100644
--- a/packages/@tailwindcss-cli/src/commands/build/index.ts
+++ b/packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -28,12 +28,13 @@ const css = String.raw
const DEBUG = env.DEBUG
const DEFAULT_POLL_INTERVAL_MS = 250
-// Directories that should never be watched, regardless of which project
-// directories are being watched. Watching these can cause excessive CPU
-// usage or hangs on file watcher backends (e.g. Watchman) when they contain
-// large or frequently-changing trees, and Tailwind never needs to react to
-// changes inside them.
-const DEFAULT_WATCH_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.hg/**', '**/.svn/**']
+// Directory segments that should not be watched by default, regardless of
+// which project directories are being watched. Watching these can cause
+// excessive CPU usage or hangs on file watcher backends (e.g. Watchman) when
+// they contain large or frequently-changing trees, and Tailwind normally
+// never needs to react to changes inside them. See `watchIgnoreFor` for the
+// one exception: an explicit `@source` pointing inside one of these.
+const DEFAULT_WATCH_IGNORE_SEGMENTS = ['node_modules', '.git', '.hg', '.svn']
export function options() {
return {
@@ -703,6 +704,13 @@ async function loadWatcher(): Promise {
async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
let watcher = await loadWatcher()
+ // Keep every originally-requested directory before the dedup step below
+ // collapses child directories into an already-watched parent. We need
+ // this to detect when an explicit source directory (e.g. an `@source`
+ // pointing inside `node_modules`) ends up nested under a broader watched
+ // root, so we know not to ignore that noise directory for that root.
+ let allRequestedDirs = dirs.slice()
+
// Remove any directories that are children of an already watched directory.
// If we don't we may not get notified of certain filesystem events regardless
// of whether or not they are for the directory that is duplicated.
@@ -794,7 +802,7 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
// Handle the tracked files at some point in the future.
await enqueueCallback()
},
- { ignore: DEFAULT_WATCH_IGNORE },
+ { ignore: watchIgnoreFor(dir, allRequestedDirs) },
)
// Ensure we cleanup the watcher when we're done.
@@ -858,6 +866,25 @@ function createPollingWatcher(cb: () => Promise, pollInterval: number) {
}
}
+// Compute the default watch-ignore glob list for `dir`, one of the
+// (already deduped) directories being watched. Skips any noise segment
+// (`node_modules`, `.git`, etc.) that contains another originally-requested
+// directory nested inside it — e.g. an explicit `@source` pointing inside
+// `node_modules` whose own watch root got collapsed into this broader `dir`
+// by the dedup step in `createWatchers`. Ignoring that segment for `dir`
+// would otherwise silently stop the watcher from picking up changes to that
+// explicitly-configured source, since `@parcel/watcher` matches `ignore`
+// globs relative to the watched root — the segment only needs to be
+// preserved for the root that actually ended up watching that subtree.
+function watchIgnoreFor(dir: string, allRequestedDirs: string[]): string[] {
+ return DEFAULT_WATCH_IGNORE_SEGMENTS.filter((segment) => {
+ let marker = `/${segment}/`
+ return !allRequestedDirs.some(
+ (other) => other !== dir && other.startsWith(`${dir}/`) && `${other}/`.includes(marker),
+ )
+ }).map((segment) => `**/${segment}/**`)
+}
+
async function watchDirectories(scanner: Scanner) {
let directories = (
await Promise.all(
From 269b78ac69a7d20d19f1c3c2361594551eafdcac Mon Sep 17 00:00:00 2001
From: SomSamantray <>
Date: Wed, 5 Aug 2026 22:13:24 +0530
Subject: [PATCH 4/4] fix(cli): don't ignore noise dirs containing a tracked
dependency
Greptile review on #20388 found that the ignore-list exemption only
considered scanner.normalizedSources (explicit @source paths), not
fullRebuildPaths -- compiler dependencies discovered via @import (e.g.
tailwindcss itself, or any imported CSS package resolved from
node_modules). If such a dependency's directory was nested under a
watched root with no other reason to be exempted, editing it would no
longer trigger a rebuild.
createWatchers() now also accepts the current fullRebuildPaths so
watchIgnoreFor() can exempt a noise segment for a root when a tracked
dependency lives inside it, the same way it already does for explicit
source directories. Verified with isolated unit tests of the ignore
logic covering both the reported regression and a control case
(unrelated dependency paths must not spuriously exempt anything), plus
new e2e coverage in integrations/cli/index.test.ts.
---
integrations/cli/index.test.ts | 52 +++
.../src/commands/build/index.ts | 312 +++++++++---------
2 files changed, 215 insertions(+), 149 deletions(-)
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index a96eee321324..5d9daaa75501 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -448,6 +448,58 @@ describe.each([
},
)
+ // https://github.com/tailwindlabs/tailwindcss/pull/20388#discussion (Greptile)
+ test(
+ 'watch mode still rebuilds when a tracked @import dependency inside node_modules changes',
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ @import '../node_modules/my-base-styles/base.css';
+ `,
+ 'index.html': html``,
+ // Not covered by any @source -- this is a plain @import dependency,
+ // tracked via `onDependency`/`fullRebuildPaths` rather than
+ // `scanner.normalizedSources`. It must still be exempted from the
+ // node_modules ignore filter for the collapsed project root.
+ 'node_modules/my-base-styles/base.css': css`
+ .imported-marker {
+ color: red;
+ }
+ `,
+ },
+ },
+ async ({ fs, spawn }) => {
+ let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`)
+ await process.onStderr((m) => m.includes('Done in'))
+
+ await fs.expectFileToContain('dist/out.css', [
+ css`
+ .imported-marker {
+ color: red;
+ }
+ `,
+ ])
+
+ await fs.write(
+ 'node_modules/my-base-styles/base.css',
+ css`
+ .imported-marker {
+ color: blue;
+ }
+ `,
+ )
+ await fs.expectFileToContain('dist/out.css', [
+ css`
+ .imported-marker {
+ color: blue;
+ }
+ `,
+ ])
+ },
+ )
+
test(
'watch mode with polling',
{
diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts
index 54b5ce5b07a2..140232423066 100644
--- a/packages/@tailwindcss-cli/src/commands/build/index.ts
+++ b/packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -342,154 +342,162 @@ export async function handle(args: Result>) {
await handleError(() => loadWatcher())
cleanupWatchers.push(
- await createWatchers(await watchDirectories(scanner), async function handle(files) {
- try {
- // If the only change happened to the output file, then we don't want to
- // trigger a rebuild because that will result in an infinite loop.
- if (files.length === 1 && files[0] === args['--output']) return
-
- using I = new Instrumentation()
- DEBUG && I.start('[@tailwindcss/cli] (watcher)')
-
- // Re-compile the input
- let start = process.hrtime.bigint()
-
- let resolvedFullRebuildPaths = fullRebuildPaths
- let rebuildStrategy = getRebuildStrategy(files, resolvedFullRebuildPaths)
-
- // Track the compiled CSS
- let compiledCss = ''
- let compiledMap: SourceMap | null = null
-
- // Scan the entire `base` directory for full rebuilds.
- if (rebuildStrategy.kind === 'full') {
- // Read the new `input`.
- let input = args['--input']
- ? args['--input'] === '-'
- ? await drainStdin()
- : await fs.readFile(args['--input'], 'utf-8')
- : css`
- @import 'tailwindcss';
- `
- clearRequireCache(resolvedFullRebuildPaths)
-
- // Track current rebuild paths in case something goes wrong when
- // performing a full rebuild.
- backupRebuildPaths = fullRebuildPaths.slice()
-
- // The `inputFilePath`, if provided, will be the only known full
- // rebuild path before the compiler is re-created.
- fullRebuildPaths = inputFilePath ? [inputFilePath] : []
-
- // Create a new compiler, given the new `input`
- ;[compiler, scanner] = await createCompiler(input, I)
-
- // Succesfully created a new compiler, so the `fullRebuildPaths`
- // will be updated. If other errors occur, we should be able to
- // restore the paths unconditionally.
- backupRebuildPaths = fullRebuildPaths.slice()
-
- // Scan the directory for candidates
- DEBUG && I.start('Scan for candidates')
- let candidates = scanner.scan()
- DEBUG && I.end('Scan for candidates')
-
- // Setup new watchers
- DEBUG && I.start('Setup new watchers')
- let newCleanupFunction = await createWatchers(await watchDirectories(scanner), handle)
- DEBUG && I.end('Setup new watchers')
-
- // Clear old watchers
- DEBUG && I.start('Cleanup old watchers')
- await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup()))
- DEBUG && I.end('Cleanup old watchers')
-
- cleanupWatchers.push(newCleanupFunction)
-
- // Re-compile the CSS
- DEBUG && I.start('Build CSS')
- compiledCss = compiler.build(candidates)
- DEBUG && I.end('Build CSS')
-
- if (args['--map']) {
- DEBUG && I.start('Build Source Map')
- compiledMap = toSourceMap(compiler.buildSourceMap())
- DEBUG && I.end('Build Source Map')
+ await createWatchers(
+ await watchDirectories(scanner),
+ async function handle(files) {
+ try {
+ // If the only change happened to the output file, then we don't want to
+ // trigger a rebuild because that will result in an infinite loop.
+ if (files.length === 1 && files[0] === args['--output']) return
+
+ using I = new Instrumentation()
+ DEBUG && I.start('[@tailwindcss/cli] (watcher)')
+
+ // Re-compile the input
+ let start = process.hrtime.bigint()
+
+ let resolvedFullRebuildPaths = fullRebuildPaths
+ let rebuildStrategy = getRebuildStrategy(files, resolvedFullRebuildPaths)
+
+ // Track the compiled CSS
+ let compiledCss = ''
+ let compiledMap: SourceMap | null = null
+
+ // Scan the entire `base` directory for full rebuilds.
+ if (rebuildStrategy.kind === 'full') {
+ // Read the new `input`.
+ let input = args['--input']
+ ? args['--input'] === '-'
+ ? await drainStdin()
+ : await fs.readFile(args['--input'], 'utf-8')
+ : css`
+ @import 'tailwindcss';
+ `
+ clearRequireCache(resolvedFullRebuildPaths)
+
+ // Track current rebuild paths in case something goes wrong when
+ // performing a full rebuild.
+ backupRebuildPaths = fullRebuildPaths.slice()
+
+ // The `inputFilePath`, if provided, will be the only known full
+ // rebuild path before the compiler is re-created.
+ fullRebuildPaths = inputFilePath ? [inputFilePath] : []
+
+ // Create a new compiler, given the new `input`
+ ;[compiler, scanner] = await createCompiler(input, I)
+
+ // Succesfully created a new compiler, so the `fullRebuildPaths`
+ // will be updated. If other errors occur, we should be able to
+ // restore the paths unconditionally.
+ backupRebuildPaths = fullRebuildPaths.slice()
+
+ // Scan the directory for candidates
+ DEBUG && I.start('Scan for candidates')
+ let candidates = scanner.scan()
+ DEBUG && I.end('Scan for candidates')
+
+ // Setup new watchers
+ DEBUG && I.start('Setup new watchers')
+ let newCleanupFunction = await createWatchers(
+ await watchDirectories(scanner),
+ handle,
+ fullRebuildPaths,
+ )
+ DEBUG && I.end('Setup new watchers')
+
+ // Clear old watchers
+ DEBUG && I.start('Cleanup old watchers')
+ await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup()))
+ DEBUG && I.end('Cleanup old watchers')
+
+ cleanupWatchers.push(newCleanupFunction)
+
+ // Re-compile the CSS
+ DEBUG && I.start('Build CSS')
+ compiledCss = compiler.build(candidates)
+ DEBUG && I.end('Build CSS')
+
+ if (args['--map']) {
+ DEBUG && I.start('Build Source Map')
+ compiledMap = toSourceMap(compiler.buildSourceMap())
+ DEBUG && I.end('Build Source Map')
+ }
}
- }
- // Scan changed files only for incremental rebuilds.
- else if (rebuildStrategy.kind === 'incremental') {
- DEBUG && I.start('Scan for candidates')
- let newCandidates = scanner.scanFiles(rebuildStrategy.changedFiles)
- DEBUG && I.end('Scan for candidates')
-
- // No new candidates found which means we don't need to write to
- // disk, and can return early.
- if (newCandidates.length <= 0) {
- let end = process.hrtime.bigint()
- if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
- return
+ // Scan changed files only for incremental rebuilds.
+ else if (rebuildStrategy.kind === 'incremental') {
+ DEBUG && I.start('Scan for candidates')
+ let newCandidates = scanner.scanFiles(rebuildStrategy.changedFiles)
+ DEBUG && I.end('Scan for candidates')
+
+ // No new candidates found which means we don't need to write to
+ // disk, and can return early.
+ if (newCandidates.length <= 0) {
+ let end = process.hrtime.bigint()
+ if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
+ return
+ }
+
+ DEBUG && I.start('Build CSS')
+ compiledCss = compiler.build(newCandidates)
+ DEBUG && I.end('Build CSS')
+
+ if (args['--map']) {
+ DEBUG && I.start('Build Source Map')
+ compiledMap = toSourceMap(compiler.buildSourceMap())
+ DEBUG && I.end('Build Source Map')
+ }
}
- DEBUG && I.start('Build CSS')
- compiledCss = compiler.build(newCandidates)
- DEBUG && I.end('Build CSS')
+ await write(compiledCss, compiledMap, args, I)
- if (args['--map']) {
- DEBUG && I.start('Build Source Map')
- compiledMap = toSourceMap(compiler.buildSourceMap())
- DEBUG && I.end('Build Source Map')
- }
- }
+ let end = process.hrtime.bigint()
+ if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
+ } catch (err) {
+ // It's important that we perform a full rebuild when any of the
+ // dependencies tracked in `fullRebuildPaths` has been changed.
+ //
+ // If we remove one of those files, then a subsequent build will be
+ // triggered, but it will fail because the dependency is gone. The
+ // compiler itself will be in a broken state and won't be able to
+ // register any dependencies therefore we want to restore all the
+ // dependencies from before. If we don't do that, then we won't be
+ // able to recover from a bug in a transitive dependency.
+ //
+ // E.g.:
+ // ```css
+ // /* input.css — known full rebuild path */
+ // @import 'tailwindcss';
+ // @config "./tailwind.config.js";
+ // ```
+ //
+ // ```js
+ // // tailwind.config.js
+ // const theme = require('./my-theme.js');
+ //
+ // module.exports = {
+ // theme
+ // }
+ // ```
+ // In this case `my-theme.js` is a transitive dependency of
+ // `input.css` via `tailwind.config.js`. Removing `my-theme.js` will
+ // result in an error, restoring `my-theme.js` should trigger a
+ // fresh build even though the compiler didn't restore.
+ //
+ // Once the build error is fixed, a fresh full rebuild will happen
+ // which in turn will fixup the full rebuild paths.
+ fullRebuildPaths = backupRebuildPaths
- await write(compiledCss, compiledMap, args, I)
-
- let end = process.hrtime.bigint()
- if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
- } catch (err) {
- // It's important that we perform a full rebuild when any of the
- // dependencies tracked in `fullRebuildPaths` has been changed.
- //
- // If we remove one of those files, then a subsequent build will be
- // triggered, but it will fail because the dependency is gone. The
- // compiler itself will be in a broken state and won't be able to
- // register any dependencies therefore we want to restore all the
- // dependencies from before. If we don't do that, then we won't be
- // able to recover from a bug in a transitive dependency.
- //
- // E.g.:
- // ```css
- // /* input.css — known full rebuild path */
- // @import 'tailwindcss';
- // @config "./tailwind.config.js";
- // ```
- //
- // ```js
- // // tailwind.config.js
- // const theme = require('./my-theme.js');
- //
- // module.exports = {
- // theme
- // }
- // ```
- // In this case `my-theme.js` is a transitive dependency of
- // `input.css` via `tailwind.config.js`. Removing `my-theme.js` will
- // result in an error, restoring `my-theme.js` should trigger a
- // fresh build even though the compiler didn't restore.
- //
- // Once the build error is fixed, a fresh full rebuild will happen
- // which in turn will fixup the full rebuild paths.
- fullRebuildPaths = backupRebuildPaths
-
- // Catch any errors and print them to stderr, but don't exit the process
- // and keep watching.
- eprintln(formatError(err))
-
- let end = process.hrtime.bigint()
- if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
- }
- }),
+ // Catch any errors and print them to stderr, but don't exit the process
+ // and keep watching.
+ eprintln(formatError(err))
+
+ let end = process.hrtime.bigint()
+ if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
+ }
+ },
+ fullRebuildPaths,
+ ),
)
// Abort the watcher if `stdin` is closed to avoid zombie processes. You can
@@ -701,15 +709,21 @@ async function loadWatcher(): Promise {
}
}
-async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
+async function createWatchers(
+ dirs: string[],
+ cb: (files: string[]) => void,
+ extraWatchPaths: string[] = [],
+) {
let watcher = await loadWatcher()
- // Keep every originally-requested directory before the dedup step below
- // collapses child directories into an already-watched parent. We need
- // this to detect when an explicit source directory (e.g. an `@source`
- // pointing inside `node_modules`) ends up nested under a broader watched
- // root, so we know not to ignore that noise directory for that root.
- let allRequestedDirs = dirs.slice()
+ // Keep every originally-requested directory, plus any tracked compiler
+ // dependency path (e.g. an `@import`-resolved file inside `node_modules`,
+ // such as `tailwindcss` itself), before the dedup step below collapses
+ // child directories into an already-watched parent. We need this to
+ // detect when an explicit source or dependency file ends up nested under
+ // a broader watched root, so we know not to ignore that noise directory
+ // for that root -- otherwise we'd never observe changes to it again.
+ let allRequestedDirs = dirs.concat(extraWatchPaths)
// Remove any directories that are children of an already watched directory.
// If we don't we may not get notified of certain filesystem events regardless