From 88747db6b55ae33d1cfaf4255d1843264ea277e3 Mon Sep 17 00:00:00 2001 From: pshu Date: Sun, 24 May 2026 17:06:19 +0800 Subject: [PATCH] fix(cache): replay ctx.add_missing_dependency on cached_node_modules warm hit When node_modules doesn't exist, the cold path adds the directory to ctx.missing_dependencies via module_directory -> is_dir. The warm cache early-return skipped this tracking, so webpack/rspack watchers would miss the signal to rebuild when a node_modules directory was later created. Mirrors the same replay pattern already applied to package_json in PR #224. --- src/cache.rs | 5 ++++ src/tests/dependencies.rs | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/cache.rs b/src/cache.rs index b99d2817..3fc3b92e 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -279,6 +279,11 @@ impl CachedPathImpl { ctx: &mut Ctx, ) -> Option { if let Some(nm) = self.node_modules.get() { + // Replay ctx tracking from the cold path: module_directory -> is_dir calls + // ctx.add_missing_dependency when node_modules doesn't exist on disk. + if nm.is_none() { + ctx.add_missing_dependency(self.path.join("node_modules")); + } return nm.clone(); } self diff --git a/src/tests/dependencies.rs b/src/tests/dependencies.rs index 4ba5fa3d..be9fe04f 100644 --- a/src/tests/dependencies.rs +++ b/src/tests/dependencies.rs @@ -1,5 +1,60 @@ //! https://github.com/webpack/enhanced-resolve/blob/main/test/dependencies.test.js +// Warm-cache calls must report the same missing_dependencies as the cold path so that +// webpack/rspack watchers can track non-existent node_modules dirs across multiple resolves. +#[cfg(not(target_os = "windows"))] +mod warm_cache_missing_dependencies { + use std::path::PathBuf; + + use super::super::memory_fs::MemoryFS; + use crate::{ResolveContext, ResolveOptions, ResolverGeneric}; + + fn file_system() -> MemoryFS { + MemoryFS::new(&[ + ("/a/b/c/some.js", ""), // makes /a/b/c and /a/b real dirs; neither has node_modules + ("/a/node_modules/module/index.js", ""), + ]) + } + + #[tokio::test] + async fn node_modules_missing_deps_same_on_warm_cache() { + let resolver = ResolverGeneric::::new_with_file_system( + file_system(), + ResolveOptions { + extensions: vec![".js".into()], + ..ResolveOptions::default() + }, + ); + + let path = PathBuf::from("/a/b/c"); + + let mut ctx_cold = ResolveContext::default(); + let cold = resolver + .resolve_with_context(path.clone(), "module", &mut ctx_cold) + .await; + + let mut ctx_warm = ResolveContext::default(); + let warm = resolver + .resolve_with_context(path.clone(), "module", &mut ctx_warm) + .await; + + assert_eq!(cold.map(|r| r.full_path()), warm.map(|r| r.full_path())); + + assert_eq!( + ctx_cold.missing_dependencies, ctx_warm.missing_dependencies, + "cold: {:?}\nwarm: {:?}", + ctx_cold.missing_dependencies, ctx_warm.missing_dependencies, + ); + + // enhanced-resolve lists traversed-but-absent node_modules dirs in missingDependencies + for dir in ["/a/b/c/node_modules", "/a/b/node_modules"] { + assert!(ctx_warm + .missing_dependencies + .contains(&PathBuf::from(dir).into())); + } + } +} + #[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows. mod windows { use std::path::PathBuf;