fix(watchpack): normalize backslashes in withoutCase for Windows pers…#14722
Open
urielvan wants to merge 1 commit into
Open
fix(watchpack): normalize backslashes in withoutCase for Windows pers…#14722urielvan wants to merge 1 commit into
urielvan wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Windows with persistent build cache, hot start emits spurious
removedevents for unchanged source files, cascading into a full rebuild each launch. The root cause is in vendored watchpack'swithoutCase: it lowercases path strings for map lookups but does not normalize path separators. On Windows the same physical file can reach watchpack under two different string spellings—forward slash from rspack's persistent-cache rehydrate (wherePortablePathnormalizes for cross-platform portability), backslash from native fs and loaderaddDependency.DirectoryWatcher.doScan's initial-missing map ends up keyed by whichever slash flavor the caller passed in, then the delete pass keyed bypath.join()'s native-separator output no longer matches, and every survivor watcher fires a spuriousinitial-missing.This PR normalizes backslashes to forward slashes inside
withoutCase, collapsing the two flavors to a single map key. The patch is registered inpackages/rspack/prebundle.config.js's watchpackafterBundleso it re-applies automatically wheneverpnpm run prebundlesyncs upstream watchpack.Repro (before this PR)
experiments.cache: { type: 'persistent' }where the compilation's file / context dependency sets contain paths with mixed separator flavors after hot-start cache rehydrate.this.addDependency. CSS Modules is a typical one—every*.module.{css,less,scss,sass}file plus their generated.d.tscompanions land infileDependencies, so a project with a modest number of CSS Modules files reproduces reliably on the first hot start.building removed ... and N moreon the second launch, followed by a full rebuild triggered by watchpack's cascadedinitial-missingevents.Reproduces stably on Windows; does not reproduce on macOS / Linux (POSIX paths never carry
\, so the map key mismatch cannot happen).Root cause
packages/rspack/compiled/watchpack/index.js(vendoredwebpack/watchpack@2.x):Used at
DirectoryWatcher.doScan()line ~1835:When some watcher keys carry forward slash (typically from rspack's persistent-cache rehydrate path where
PortablePathnormalizes to forward slash for cross-platform portability) andpath.joinproduces backslash, every forward-slash-keyed watcher survives the delete pass and fires a spuriousinitial-missing, propagating through:watchpack → NativeWatchFileSystem → Watching.#mergeWithCollected → compiler.removedFiles → rebuildFix
Normalize backslashes to forward slashes inside
withoutCase, so map lookups are stable regardless of the separator flavor the caller passes:The patch is applied in
packages/rspack/prebundle.config.js's watchpackafterBundlevia the existingreplaceFileContenthelper (same pattern already used for the d.ts adjustment right above), so upstream syncs re-apply it automatically.Performance impact
Microbench (Node 20 / Windows, mixed backslash + forward-slash paths, 5000 calls):
.toLowerCase().toLowerCase().replace(/\\/g, '/')Extra ~0.2 μs per call. A hot start touches
withoutCaseat most a few thousand times, so the added cost is ~2 ms per hot start against a 3-4 s total—well within noise.No indexOf guard: guarding actually made the benchmark slower on real Windows paths (which frequently contain
\) because the guard'sindexOfwalk can't short-circuit and the branch still has to runreplaceanyway.Test / verification
Verified against the following stack on Windows 11:
node_modules/@rspack/core/compiled/watchpack/index.js)Ran 100 consecutive
npm start→ wait 15 s →taskkill /F /Tcycles:built in X.XXswithin 15 s)building removed ... and N morein stdoutHot start performance matches macOS / Linux.
Notes
.replace(/\\/g, '/')matches nothing and is a near-nooppackages/rspack/compiled/watchpack/index.jsis.gitignored, so the visible diff is onlyprebundle.config.js; the vendored file gets regenerated with the patched form on nextpnpm run prebundle