Skip to content

Ensure @source files behind symlinks trigger rebuilds in @tailwindcss/vite#20347

Open
Nic-Polumeyv wants to merge 2 commits into
tailwindlabs:mainfrom
Nic-Polumeyv:fix/vite-symlinked-source-hmr
Open

Ensure @source files behind symlinks trigger rebuilds in @tailwindcss/vite#20347
Nic-Polumeyv wants to merge 2 commits into
tailwindlabs:mainfrom
Nic-Polumeyv:fix/vite-symlinked-source-hmr

Conversation

@Nic-Polumeyv

Copy link
Copy Markdown

Fixes #20346

Summary

Since #20203 the scanner preserves symlinks in @source paths, so scanner.files can contain a path like app/node_modules/pkg/index.js while Vite tracks the module and its file change events under the real packages/pkg/index.js. The plugin registered only the written path via addWatchFile, which Vite's watcher ignores inside node_modules, so editing the real file behind a workspace symlink never invalidated the CSS module and new candidates only appeared after restarting the dev server.

This registers the resolved path alongside the written one for scanned files and glob bases, cached per root. isScannedFile gets the missing direction too: it already resolved the changed file before comparing against scanned paths, now it also resolves scanned paths before comparing against the changed file, so full reloads for non-module @source files keep working when the path goes through a symlink.

Test plan

Added an integration test with a pnpm workspace where @source points through the node_modules symlink of a linked workspace package. It fails without the change (the new candidate never appears until the dev server restarts) and passes with it. The full integrations/vite suite passes. Also verified the reproduction from #20346 directly: with this build, editing the file behind the symlink updates the page without a restart.

@Nic-Polumeyv
Nic-Polumeyv requested a review from a team as a code owner July 17, 2026 21:35
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e1bbcc27-555d-40d5-a3a5-f9024f1266df

📥 Commits

Reviewing files that changed from the base of the PR and between b899733 and 5e45469.

📒 Files selected for processing (2)
  • integrations/vite/index.test.ts
  • packages/@tailwindcss-vite/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/@tailwindcss-vite/src/index.ts
  • integrations/vite/index.test.ts

Walkthrough

The Vite plugin now caches resolved real paths for scanned files, watches both symlinked and resolved source paths, and matches file changes using either path form. A new integration test covers rebuilding when a workspace package’s real source file changes through a node_modules symlink. The unreleased changelog records this fix.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: symlinked @source files in @tailwindcss/vite now trigger rebuilds.
Description check ✅ Passed The description is directly about the symlink hot-reload fix and the added test, so it matches the changeset.
Linked Issues check ✅ Passed The code and test address #20346 by watching realpaths and rescanning symlinked @source files when the underlying file changes.
Out of Scope Changes check ✅ Passed The changelog update and integration test are in scope, and no unrelated changes are apparent.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

🧹 Nitpick comments (1)
packages/@tailwindcss-vite/src/index.ts (1)

539-549: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider comparing the real path of the scanned file with the real path of the changed file.

To guarantee robustness in complex workspace topologies (e.g. where multiple symlinks point to the same physical file and Vite reports a different symlink than the one scanned), consider explicitly comparing their resolved real paths as a final fallback.

💡 Proposed refactor
   isScannedFile(file: string, realpath: string | null): boolean {
     for (let scanned of this.scannedFiles) {
       if (scanned === file) return true
       if (realpath !== null && scanned === realpath) return true
-      if (this.realpath(scanned) === file) return true
+
+      let scannedReal = this.realpath(scanned)
+      if (scannedReal === file) return true
+      if (realpath !== null && scannedReal === realpath) return true
     }
     return false
   }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6ae12ef6-df9f-412b-97e6-6c5b02e0a68f

📥 Commits

Reviewing files that changed from the base of the PR and between 094bf62 and b899733.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • integrations/vite/index.test.ts
  • packages/@tailwindcss-vite/src/index.ts

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to symlink resolution in the Vite plugin and is covered by a new integration test that reproduces the exact failure scenario.

The change adds a lazily-populated, per-rebuild cache of real paths, registers both the symlinked and real paths with Vite's watcher, and extends isScannedFile to compare against both sides. All code paths fall back gracefully on realpathSync errors, the cache is cleared on full rebuilds to handle symlink retargeting, and the duplicate-glob-base case is handled correctly with a Set. No existing logic is removed, only extended.

No files require special attention.

Reviews (2): Last reviewed commit: "Clear resolved path cache on full rebuil..." | Re-trigger Greptile

@Nic-Polumeyv

Copy link
Copy Markdown
Author

Testing this a bit further, the same class shows up in the scanner's other consumers.

The CLI's native --watch is affected with the mechanism inverted: watchDirectories resolves watch dirs since #20242, so events arrive with real paths and scanner.scanFiles() rejects them against the symlink-based source allowlist. In the integration harness, an edit to a plain @source file rebuilds while the same edit behind a pnpm workspace symlink does not. --watch --poll is fine since it rescans through the source paths themselves.

@tailwindcss/postcss registers the symlink path in its dependency messages (app/node_modules/pkg/index.js in a pnpm workspace). Consumers that skip watching node_modules never see changes to the real file, which would bring back #13533 on v4; I haven't verified that end to end.

Happy to fix the CLI the same way in a follow-up, though canonicalizing once in scan_files might beat patching each consumer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@tailwindcss/vite does not hot-reload new candidates from explicit @source files behind workspace symlinks since v4.3.1

1 participant