Skip to content

Add --poll option to @tailwindcss/cli#20297

Merged
RobinMalfait merged 3 commits into
mainfrom
feat/add-poll-option
Jul 1, 2026
Merged

Add --poll option to @tailwindcss/cli#20297
RobinMalfait merged 3 commits into
mainfrom
feat/add-poll-option

Conversation

@RobinMalfait

@RobinMalfait RobinMalfait commented Jul 1, 2026

Copy link
Copy Markdown
Member

This PR re-adds the --poll option to the @tailwindcss/cli that we had in Tailwind CSS v3, but didn't in Tailwind CSS v4.

In Tailwind CSS v4, we started using @parcel/watcher instead of chokidar for our watcher in the CLI. However, this currently doesn't support a --poll option.

This PR implements our own --poll option such that you can use it in environments where fs events don't work properly (e.g. Docker).

Polling can be enabled by using --watch --poll, in this case we will poll every 250ms (I'm open for a different default value). You can also pick your own interval by using --watch --poll 500 which is defined in milliseconds.

The --poll option will be less efficient than a normal --watch. But if you are in a situation where you can't use --watch on its own then this is a good fallback.

One thing you can do today is run the build command manually. If you do have some tooling that does work on your machine (such as watchexec) then you can automatically perform a full build. The biggest downside of this approach is that you are doing a full build every time, instead of an incremental build.

With this PR, we try to fix that by still allowing incremental builds. This should result in the same behavior as the normal --watch function:

  1. First run, will trigger a full build
  2. When any of the source files changes:
    • If all classes were already known, then it will be a no-op, but you will see a log in the terminal about it.
    • If a new class is detected, then the CSS will be updated, but it will be much more efficient than a full rebuild
  3. When the input CSS file changes, or any of its dependencies, then a full rebuild will be triggered (such that your new @utility are available, and @theme values are updated).

The implementation is a little bit more complex just because I didn't want to spam the terminal output even if we are polling every 250ms.

In the Oxide scanner we do track the modified times of each file. Every 250ms we traverse the file system and skip the files that we know didn't change (since the mtime is the same). If the file was touched, then we will parse it again to extract possible Tailwind CSS classes. We will also track which files were scanned such that we can know whether we have to trigger a full-rebuild or not (in case the input.css file or any of its dependencies was changed).

Fixes: #18109
Fixes: #18540
Fixes: #15750

Test plan

  1. Existing tests pass
  2. An integration test has been added for the --poll option
  3. Tested it on the tailwindcss.com codebase:
image

Annotated:

≈ tailwindcss v4.3.2

Done in 105ms                    Initial build
Done in 3ms                      Saved a file that resulted in a no-op
Done in 2ms                      Saved a file that resulted in a no-op
Done in 3ms                      Saved a file that resulted in a no-op
Done in 53ms                     Saved a file with a new class
Done in 2ms                      Saved a file that resulted in a no-op
Done in 2ms                      Saved a file that resulted in a no-op
Done in 3ms                      Saved a file that resulted in a no-op
Done in 88ms                     Saved a the input.css file
Done in 3ms                      Saved a file that resulted in a no-op
Polling for changes…

We check the file system every 250ms by default, but we won't log to prevent spamming the terminal.

@RobinMalfait RobinMalfait force-pushed the feat/add-poll-option branch from 732f595 to e1bd806 Compare July 1, 2026 18:37
@RobinMalfait RobinMalfait marked this pull request as ready for review July 1, 2026 18:42
@RobinMalfait RobinMalfait requested a review from a team as a code owner July 1, 2026 18:42
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

This is close, but one polling rebuild case should be fixed before merging.

  • Invalid --poll=foo values now get rejected.
  • Changed non-UTF-8 source paths can still be omitted from the file list used by polling rebuilds.
  • That can leave generated CSS stale after a source edit.

crates/oxide/src/scanner/mod.rs

Reviews (3): Last reviewed commit: "update changelog" | Re-trigger Greptile

Comment thread packages/@tailwindcss-cli/src/utils/args.ts Outdated
Comment thread crates/oxide/src/scanner/mod.rs
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change adds polling-based watch mode to the Tailwind CLI and records the set of files selected during the latest Rust scanner pass. The scanner now carries modification times through filesystem walking, computes changed files during discovery, and exposes them through Rust and Node getters. The CLI adds --poll, updates rebuild selection and output handling, and includes polling status, cleanup, and tests. The usage text and changelog were also updated.

Changes

Related PRs: #20297
Suggested labels: cli, rust, scanner
Suggested reviewers: thecrypticace, philipp-spiess

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant createPollingWatcher
  participant getRebuildStrategy
  participant Scanner
  CLI->>createPollingWatcher: start(pollInterval, callback)
  loop each poll interval
    createPollingWatcher->>CLI: invoke callback
    CLI->>Scanner: scan()
    CLI->>getRebuildStrategy: files, fullRebuildPaths
    getRebuildStrategy-->>CLI: rebuild strategy
    CLI->>CLI: write(output)
  end
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a --poll option to the Tailwind CLI.
Description check ✅ Passed The description is directly related to the changeset and explains the new polling watch behavior.
Linked Issues check ✅ Passed The PR adds polling watch mode, preserves incremental rebuilds, and addresses the watcher and Docker bind-mount issues raised in the linked tickets.
Out of Scope Changes check ✅ Passed The changes are focused on the polling watch feature, its scanner support, tests, and docs with no clear unrelated additions.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/oxide/tests/scanner.rs`:
- Around line 1090-1091: The test in scanner.rs is using too short a delay
before rewriting src/index.html, which can cause the filesystem mtime to stay
unchanged and make the scan flaky. Increase the sleep in this test to match the
more reliable delay used elsewhere in the file, so the mtime differs before the
rewrite and the assertion in the same test remains stable.

In `@packages/`@tailwindcss-cli/src/commands/build/index.ts:
- Around line 578-637: The polling rebuild path in build/index.ts is using
scanner.scan() as if it were the incremental delta, but scan() returns the full
candidate set and causes unnecessary rebuilds. In the watcher/polling flow
around getRebuildStrategy and compiler.build, switch the non-full rebuild path
to derive candidates from strategy.changedFiles using scanner.scanFiles(...) or
an equivalent delta-producing call, and only feed that changed-file candidate
set into compiler.build so --watch --poll preserves no-op/incremental behavior.

In `@packages/`@tailwindcss-cli/src/commands/help/index.ts:
- Around line 119-123: The help formatter in the option rendering loop is making
`--poll` look like it requires a value even though `handle()` accepts a bare
flag and maps `true` to the default delay. Update the usage/help generation in
the help command (the logic that formats `options` and the shared `buildUsage()`
path) so boolean-or-value flags like `poll` render their value as optional, e.g.
using bracketed syntax instead of a required `=placeholder` form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e199b8e6-61df-44c1-b1f3-0c6ba6eb2b82

📥 Commits

Reviewing files that changed from the base of the PR and between 056a155 and e1bd806.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • crates/node/src/lib.rs
  • crates/oxide/src/scanner/mod.rs
  • crates/oxide/tests/scanner.rs
  • integrations/cli/index.test.ts
  • packages/@tailwindcss-cli/src/commands/build/index.ts
  • packages/@tailwindcss-cli/src/commands/help/index.ts
  • packages/@tailwindcss-cli/src/index.ts
  • packages/@tailwindcss-cli/src/utils/args.test.ts
  • packages/@tailwindcss-cli/src/utils/args.ts

Comment thread crates/oxide/tests/scanner.rs
Comment thread packages/@tailwindcss-cli/src/commands/build/index.ts
Comment thread packages/@tailwindcss-cli/src/commands/help/index.ts Outdated
@RobinMalfait RobinMalfait force-pushed the feat/add-poll-option branch from e1bd806 to 3cc1089 Compare July 1, 2026 19:04
Comment thread packages/@tailwindcss-cli/src/commands/build/index.ts
The idea for `--poll` is that we don't have to rely on file system
events but instead we poll the file system.

The Oxide scanner has a cache of mtimes for changed files. This allows
us to skip parsing files that we know haven't changed.

This also adds a small new internal api `scan_incremental` which results
in changed files that were scanned and their combined `candidates`. With
this information we can compute the next CSS output, and we can figure
out whether we need to perform a full rebuild or not.

We could simplify this, and just keep polling over and over again, but
it would spam the terminal output as well which is not ideal.
@RobinMalfait RobinMalfait force-pushed the feat/add-poll-option branch from 3cc1089 to 67ab08d Compare July 1, 2026 19:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/@tailwindcss-cli/src/commands/build/index.ts (2)

586-588: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Exclude generated files before scanning, not after.

scanner.scan() has already walked and extracted from changed files before this filter runs. If --output or an external --map lives under the scanner root, the polling loop can scan generated artifacts and mutate scanner state before discarding them from files. Add negated scanner sources for generated output/map paths when creating the scanner, or otherwise exclude them at the scanner source level.

Proposed direction
     if (inputFilePath !== null) {
       sources.push({
         base: path.dirname(inputFilePath),
         pattern: path.basename(inputFilePath),
         negated: false,
       })
     }
+
+    for (let generated of [
+      args['--output'] && args['--output'] !== '-' ? args['--output'] : null,
+      typeof args['--map'] === 'string' ? args['--map'] : null,
+    ]) {
+      if (generated !== null) {
+        sources.push({
+          base: path.dirname(generated),
+          pattern: path.basename(generated),
+          negated: true,
+        })
+      }
+    }

794-798: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Avoid raw error spam from the polling loop.

A persistent scanner.scan()/build/write error will currently console.error every polling interval. Route these errors through the polling renderer and de-dupe repeated messages so --poll does not spam the terminal on every tick.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cba53c0d-f586-4c41-ba9e-6ddd3c11f065

📥 Commits

Reviewing files that changed from the base of the PR and between e1bd806 and 3cc1089.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/node/src/lib.rs
  • crates/oxide/src/scanner/mod.rs
  • crates/oxide/tests/scanner.rs
  • packages/@tailwindcss-cli/src/commands/build/index.ts
  • packages/@tailwindcss-cli/src/index.ts
  • packages/@tailwindcss-cli/src/utils/args.test.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/@tailwindcss-cli/src/index.ts
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/node/src/lib.rs
  • packages/@tailwindcss-cli/src/utils/args.test.ts
  • crates/oxide/tests/scanner.rs

Comment thread crates/oxide/src/scanner/mod.rs
Comment thread crates/oxide/src/scanner/mod.rs
Comment thread crates/oxide/src/scanner/mod.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f3644028-201d-4a07-b4b5-3822ff87eeca

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc1089 and 67ab08d.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • crates/node/src/lib.rs
  • crates/oxide/src/scanner/mod.rs
  • crates/oxide/tests/scanner.rs
  • packages/@tailwindcss-cli/src/commands/build/index.ts
  • packages/@tailwindcss-cli/src/index.ts
  • packages/@tailwindcss-cli/src/utils/args.test.ts
✅ Files skipped from review due to trivial changes (2)
  • CHANGELOG.md
  • packages/@tailwindcss-cli/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/node/src/lib.rs
  • crates/oxide/tests/scanner.rs
  • crates/oxide/src/scanner/mod.rs

Comment thread packages/@tailwindcss-cli/src/commands/build/index.ts
@RobinMalfait RobinMalfait merged commit 39656f7 into main Jul 1, 2026
10 checks passed
@RobinMalfait RobinMalfait deleted the feat/add-poll-option branch July 1, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant