-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
fix(cli): exclude noise directories from --watch file watcher #20388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
9c3d968
d5b8ac9
45921ed
269b78a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,14 @@ const css = String.raw | |
| const DEBUG = env.DEBUG | ||
| const DEFAULT_POLL_INTERVAL_MS = 250 | ||
|
|
||
| // 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 { | ||
| '--input': { | ||
|
|
@@ -696,6 +704,13 @@ async function loadWatcher(): Promise<typeof import('@parcel/watcher')> { | |
| 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. | ||
|
|
@@ -747,44 +762,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: watchIgnoreFor(dir, allRequestedDirs) }, | ||
| ) | ||
|
|
||
| // Ensure we cleanup the watcher when we're done. | ||
| watchers.add(unsubscribe) | ||
|
|
@@ -847,6 +866,25 @@ function createPollingWatcher(cb: () => Promise<void>, 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), | ||
|
Comment on lines
+895
to
+897
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a normal Tailwind import or another compiler dependency resolves inside Knowledge Base Used: Build tool integrations Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| ) | ||
| }).map((segment) => `**/${segment}/**`) | ||
| } | ||
|
|
||
| async function watchDirectories(scanner: Scanner) { | ||
| let directories = ( | ||
| await Promise.all( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.