# Fix changes to symlinked files outside of the project works

This PR fixes an issue where changes to a symlinked file wouldn't result in hot-reload when using @tailwincdss/vite.

| | |
| --- | --- |
| Status | MERGED |
| Source | github:tailwindlabs/tailwindcss#20356 |
| Original | https://github.com/tailwindlabs/tailwindcss/pull/20356 |
| Workspace | Showcase |
| Tags | github, code, rust, typescript, docs |

## What it does

**The change is spread across 4 files in 3 areas, with no single file dominating.**

4 files changed across 3 areas of tailwindlabs/tailwindcss, 359 added and 65 removed.

This PR fixes an issue where changes to a symlinked file wouldn't result in hot-reload when using @tailwincdss/vite.

**Diagram: Where the change landed. Node size is not to scale; the line counts are exact.**

- tailwindcss (4 files, 424 lines) [accent]
- Tests (2 files, 232 lines) [good]
- Source (1 file, 188 lines) [accent]
- Docs (1 file, 4 lines) [neutral]
- scanner.rs (+125 −0) [neutral]
- tailwindcss -> Tests
- tailwindcss -> Source
- tailwindcss -> Docs
- Tests -> scanner.rs : heaviest

- **Heaviest file.** crates/oxide/src/scanner/mod.rs, +124 −64, which is 44% of the change.
- **Tests moved with it.** 2 test files changed alongside 1 source file.
- **Spread.** 4 files, 424 changed lines in total.

## How it works

**The largest single edit is in mod.rs.**

Quoted verbatim from the diff. This is the biggest hunk by changed lines, which is where a reviewer's attention usually belongs first.

**Diagram: Source files matched to test files by filename. A name-based match, so treat it as a prompt to look rather than as proof.**

- NO TEST CHANGED (1)
  - mod.rs (+124 −64) [bad]
- TEST CHANGED TOO (0)
  - no source file paired [bad]

```rust
@@ -388,47 +401,93 @@ impl Scanner {
         self.extensions.clear();
         self.globs = None;
 
-        for (path, is_dir, extension, mtime) in all_entries {
-            if is_dir {
-                self.dirs.insert(path);
-            } else {
-                // Deduplicate: parallel walk can visit the same file from multiple threads
-                if !self.files.insert(path.clone()) {
-                    continue;
+        // Cache canonicalized folders in case a file itself is not symlinked, but any of the parent
+        // folders are symlinked.
+        let mut cached_canonical_dirs: FxHashMap<PathBuf, PathBuf> = FxHashMap::default();
+
+        for entry in all_entries {
+            match entry {
+                WalkEntry::Dir(path) => {
+                    self.dirs.insert(path);
                 }
-                self.extensions.insert(extension.clone());
-
```

_crates/oxide/src/scanner/mod.rs, +124 −64. The largest hunk in the largest file._

## File map

**Every file this change touches, heaviest first.**

Role is inferred from the path and risk from the path and the line count. Neither was read from the code.

| File | Role | Change | Risk |
| --- | --- | --- | --- |
| `crates/oxide/src/scanner/mod.rs` | application code | modified, +124 −64 | medium |
| `crates/oxide/tests/scanner.rs` | test coverage | modified, +125 −0 | low |
| `integrations/vite/index.test.ts` | test coverage | modified, +107 −0 | low |
| `CHANGELOG.md` | documentation | modified, +3 −1 | none |

## Receipts

**Where each number above comes from.**

All of it is arithmetic over the file list and the check results, so it can be recomputed from the source.

- **Claim.** Repeated block: 2 occurrences.
  - Evidence: // Mimic a pnpm workspace where `node_modules` contains a symlink to the actual package
fs::create_dir_all(dir.join("node_modules")).unwrap();
let _ = symlink(dir.join("packages/repro"), dir.join("node_modules/repro"));
  - Where: crates/oxide/tests/scanner.rs
- **Claim.** 4 files changed, +359 −65.
  - Evidence: crates/oxide/src/scanner/mod.rs (+124 −64); crates/oxide/tests/scanner.rs (+125 −0); integrations/vite/index.test.ts (+107 −0)
  - Where: https://github.com/tailwindlabs/tailwindcss/pull/20356
- **Claim.** mod.rs carries 44% of the changed lines.
  - Evidence: 188 of 424 changed lines
  - Where: crates/oxide/src/scanner/mod.rs
- **Claim.** It arrived as 5 commits.
  - Evidence: add regression integration test
  - Where: commit 05eb6ea
- **Claim.** CI reports 0 failing of 24.
  - Evidence: Greptile Review
  - Where: CI check: Greptile Review
- **Claim.** The change drew 7 comments.
  - Evidence: <a href="#"><img alt="P1" src="https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=9" align="top"></a> **Canonical paths change …
  - Where: comment by greptile-apps[bot]

## Merge order

**A reading order for the review, not a deployment plan.**

Ordered by how expensive each part is to get wrong, which is a structural property rather than an informed one.

1. **Then mod.rs.** The largest single file in the change, +124 −64.
2. **Finish with the tests.** 2 test files changed. Reading them last tells you what the author believed they were changing.

## What this brief could not check

- No language model read this change. Every statement here is computed from file paths, line counts, and CI status, so it describes the shape of the diff and not what the diff means.
