From 870a6e8382b766e5985e30b2da77be816d820e66 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 17 Sep 2025 13:25:15 +0200 Subject: [PATCH 01/13] Traverse upwards for single context projects --- rewatch/src/helpers.rs | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index 9a85f94e99..a81e198141 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -154,12 +154,63 @@ pub fn try_package_path( } else if path_from_root.exists() { Ok(path_from_root) } else { + // As a last resort, when we're in a Single project context, traverse upwards + // starting from the parent of the package root (package_config.path.parent().parent()) + // and probe each ancestor's node_modules for the dependency. This covers hoisted + // workspace setups when building a package standalone. + if project_context.monorepo_context.is_none() { + match package_config.path.parent().and_then(|p| p.parent()) { + Some(start_dir) => { + return find_dep_in_upward_node_modules(start_dir, package_name); + } + None => { + log::debug!( + "try_package_path: cannot compute start directory for upward traversal from '{}'", + package_config.path.to_string_lossy() + ); + } + } + } + Err(anyhow!( "The package \"{package_name}\" is not found (are node_modules up-to-date?)..." )) } } +fn find_dep_in_upward_node_modules(start_dir: &Path, package_name: &str) -> anyhow::Result { + log::debug!( + "try_package_path: falling back to upward traversal for '{}' starting at '{}'", + package_name, + start_dir.to_string_lossy() + ); + + let mut current = Some(start_dir); + while let Some(dir) = current { + let candidate = package_path(dir, package_name); + log::debug!("try_package_path: checking '{}'", candidate.to_string_lossy()); + if candidate.exists() { + log::debug!( + "try_package_path: found '{}' at '{}' via upward traversal", + package_name, + candidate.to_string_lossy() + ); + return Ok(candidate); + } + current = dir.parent(); + } + log::debug!( + "try_package_path: no '{}' found during upward traversal from '{}'", + package_name, + start_dir.to_string_lossy() + ); + Err(anyhow!( + "try_package_path: upward traversal did not find '{}' starting at '{}'", + package_name, + start_dir.to_string_lossy() + )) +} + pub fn get_abs_path(path: &Path) -> PathBuf { let abs_path_buf = PathBuf::from(path); From 8779e66f34871d66f9e5bd87a100e438159277e0 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 17 Sep 2025 13:37:48 +0200 Subject: [PATCH 02/13] Add cache for upward traversal --- rewatch/src/helpers.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index a81e198141..b9b091cf5a 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -9,6 +9,7 @@ use std::fs::File; use std::io::Read; use std::io::{self, BufRead}; use std::path::{Component, Path, PathBuf}; +use std::sync::{LazyLock, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; pub type StdErr = String; @@ -31,6 +32,25 @@ pub mod emojis { pub static LINE_CLEAR: &str = "\x1b[2K\r"; } +// Cache existence checks for candidate node_modules paths during upward traversal. +// Keyed by the absolute candidate path string; value is the existence boolean. +static NODE_MODULES_EXIST_CACHE: LazyLock>> = + LazyLock::new(|| RwLock::new(ahash::AHashMap::new())); + +fn cached_path_exists(path: &Path) -> bool { + let key = path.to_string_lossy().to_string(); + if let Ok(cache) = NODE_MODULES_EXIST_CACHE.read() { + if let Some(exists) = cache.get(&key) { + return *exists; + } + } + let exists = path.exists(); + if let Ok(mut cache) = NODE_MODULES_EXIST_CACHE.write() { + cache.insert(key, exists); + } + exists +} + /// This trait is used to strip the verbatim prefix from a Windows path. /// On non-Windows systems, it simply returns the original path. /// This is needed until the rescript compiler can handle such paths. @@ -189,7 +209,7 @@ fn find_dep_in_upward_node_modules(start_dir: &Path, package_name: &str) -> anyh while let Some(dir) = current { let candidate = package_path(dir, package_name); log::debug!("try_package_path: checking '{}'", candidate.to_string_lossy()); - if candidate.exists() { + if cached_path_exists(&candidate) { log::debug!( "try_package_path: found '{}' at '{}' via upward traversal", package_name, From 80072327af7ca0c840cecd977b3d21304ddca79e Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 17 Sep 2025 14:25:28 +0200 Subject: [PATCH 03/13] Add sample to test repo --- rewatch/testrepo/package.json | 9 +++++---- rewatch/testrepo/packages/standalone/package.json | 7 +++++++ .../testrepo/packages/standalone/rescript.json | 13 +++++++++++++ .../packages/standalone/src/Standalone.mjs | 13 +++++++++++++ .../packages/standalone/src/Standalone.res | 4 ++++ rewatch/testrepo/yarn.lock | 8 ++++++++ rewatch/tests/compile.sh | 15 +++++++++++++++ 7 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 rewatch/testrepo/packages/standalone/package.json create mode 100644 rewatch/testrepo/packages/standalone/rescript.json create mode 100644 rewatch/testrepo/packages/standalone/src/Standalone.mjs create mode 100644 rewatch/testrepo/packages/standalone/src/Standalone.res diff --git a/rewatch/testrepo/package.json b/rewatch/testrepo/package.json index 4de0800eac..f54aef232d 100644 --- a/rewatch/testrepo/package.json +++ b/rewatch/testrepo/package.json @@ -16,7 +16,8 @@ "packages/file-casing-no-namespace", "packages/pure-dev", "packages/with-ppx", - "packages/nohoist" + "packages/nohoist", + "packages/standalone" ], "nohoist": [ "rescript-bun" @@ -26,11 +27,11 @@ "rescript": "12.0.0-beta.1" }, "scripts": { - "build": "../target/release/rewatch build .", + "build": "../target/release/rescript build .", "build:rescript": "rescript legacy build", - "watch": "../target/release/rewatch watch .", + "watch": "../target/release/rescript watch .", "watch:rescript": "rescript legacy watch", - "clean": "../target/release/rewatch clean .", + "clean": "../target/release/rescript clean .", "clean:rescript": "rescript clean" } } diff --git a/rewatch/testrepo/packages/standalone/package.json b/rewatch/testrepo/packages/standalone/package.json new file mode 100644 index 0000000000..3d21e324b0 --- /dev/null +++ b/rewatch/testrepo/packages/standalone/package.json @@ -0,0 +1,7 @@ +{ + "name": "@testrepo/standalone", + "version": "1.0.0", + "dependencies": { + "@testrepo/dep01": "*" + } +} diff --git a/rewatch/testrepo/packages/standalone/rescript.json b/rewatch/testrepo/packages/standalone/rescript.json new file mode 100644 index 0000000000..c04ff06f22 --- /dev/null +++ b/rewatch/testrepo/packages/standalone/rescript.json @@ -0,0 +1,13 @@ +{ + "name": "standalone", + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "es6", + "in-source": true + }, + "suffix": ".mjs", + "dependencies": ["@testrepo/dep01"] +} \ No newline at end of file diff --git a/rewatch/testrepo/packages/standalone/src/Standalone.mjs b/rewatch/testrepo/packages/standalone/src/Standalone.mjs new file mode 100644 index 0000000000..18efa44368 --- /dev/null +++ b/rewatch/testrepo/packages/standalone/src/Standalone.mjs @@ -0,0 +1,13 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Dep01 from "@testrepo/dep01/src/Dep01.mjs"; + +function standalone() { + Dep01.log(); + console.log("standalone"); +} + +export { + standalone, +} +/* Dep01 Not a pure module */ diff --git a/rewatch/testrepo/packages/standalone/src/Standalone.res b/rewatch/testrepo/packages/standalone/src/Standalone.res new file mode 100644 index 0000000000..202c4f430a --- /dev/null +++ b/rewatch/testrepo/packages/standalone/src/Standalone.res @@ -0,0 +1,4 @@ +let standalone = () => { + Dep01.log() + Js.log("standalone") +} \ No newline at end of file diff --git a/rewatch/testrepo/yarn.lock b/rewatch/testrepo/yarn.lock index 153c6db527..5b46a73174 100644 --- a/rewatch/testrepo/yarn.lock +++ b/rewatch/testrepo/yarn.lock @@ -164,6 +164,14 @@ __metadata: languageName: unknown linkType: soft +"@testrepo/standalone@workspace:packages/standalone": + version: 0.0.0-use.local + resolution: "@testrepo/standalone@workspace:packages/standalone" + dependencies: + "@testrepo/dep01": "npm:*" + languageName: unknown + linkType: soft + "@testrepo/with-dev-deps@workspace:packages/with-dev-deps": version: 0.0.0-use.local resolution: "@testrepo/with-dev-deps@workspace:packages/with-dev-deps" diff --git a/rewatch/tests/compile.sh b/rewatch/tests/compile.sh index 6e98209466..3c8e18eef1 100755 --- a/rewatch/tests/compile.sh +++ b/rewatch/tests/compile.sh @@ -34,6 +34,21 @@ else exit 1 fi +# Build from standalone package folder using `rescript build` +bold "Test: Standalone package can build via rescript from package folder" +pushd ./packages/standalone > /dev/null +error_output=$(rescript build 2>&1) +if [ $? -eq 0 ]; +then + success "Standalone package built" +else + error "Error building standalone package" + printf "%s\n" "$error_output" >&2 + popd > /dev/null + exit 1 +fi +popd > /dev/null + node ./packages/main/src/Main.mjs > ./packages/main/src/output.txt mv ./packages/main/src/Main.res ./packages/main/src/Main2.res From c6309ad9183e85002ef96ee1f2b782cf1f695341 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 17 Sep 2025 14:44:00 +0200 Subject: [PATCH 04/13] Invoke rewatch executable --- rewatch/tests/compile.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewatch/tests/compile.sh b/rewatch/tests/compile.sh index 3c8e18eef1..9287eada3f 100755 --- a/rewatch/tests/compile.sh +++ b/rewatch/tests/compile.sh @@ -37,7 +37,7 @@ fi # Build from standalone package folder using `rescript build` bold "Test: Standalone package can build via rescript from package folder" pushd ./packages/standalone > /dev/null -error_output=$(rescript build 2>&1) +error_output=$("../../$REWATCH_EXECUTABLE" build 2>&1) if [ $? -eq 0 ]; then success "Standalone package built" From 810d4585f30c162f6ac233ad19f930fdbb70bd0f Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 17 Sep 2025 14:58:24 +0200 Subject: [PATCH 05/13] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b020f7241..d854f35e1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ #### :nail_care: Polish - Add (dev-)dependencies to build schema. https://github.com/rescript-lang/rescript/pull/7892 +- Rewatch: Traverse upwards for package resolution in single context projects. https://github.com/rescript-lang/rescript/pull/7896 #### :house: Internal From d285b9be2d5a466749b6cea39cd195147dfa7240 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 15:33:51 +0200 Subject: [PATCH 06/13] Apply code review feedback --- rewatch/src/helpers.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index b9b091cf5a..897abf57d5 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -33,20 +33,35 @@ pub mod emojis { } // Cache existence checks for candidate node_modules paths during upward traversal. -// Keyed by the absolute candidate path string; value is the existence boolean. -static NODE_MODULES_EXIST_CACHE: LazyLock>> = +// Keyed by the absolute candidate path; value is the existence boolean. +static NODE_MODULES_EXIST_CACHE: LazyLock>> = LazyLock::new(|| RwLock::new(ahash::AHashMap::new())); fn cached_path_exists(path: &Path) -> bool { - let key = path.to_string_lossy().to_string(); - if let Ok(cache) = NODE_MODULES_EXIST_CACHE.read() { - if let Some(exists) = cache.get(&key) { - return *exists; + match NODE_MODULES_EXIST_CACHE.read() { + Ok(cache) => { + if let Some(exists) = cache.get(path) { + return *exists; + } + } + Err(poisoned) => { + log::warn!("NODE_MODULES_EXIST_CACHE read lock poisoned; recovering"); + let cache = poisoned.into_inner(); + if let Some(exists) = cache.get(path) { + return *exists; + } } } let exists = path.exists(); - if let Ok(mut cache) = NODE_MODULES_EXIST_CACHE.write() { - cache.insert(key, exists); + match NODE_MODULES_EXIST_CACHE.write() { + Ok(mut cache) => { + cache.insert(path.to_path_buf(), exists); + } + Err(poisoned) => { + log::warn!("NODE_MODULES_EXIST_CACHE write lock poisoned; recovering"); + let mut cache = poisoned.into_inner(); + cache.insert(path.to_path_buf(), exists); + } } exists } From bf559e0e1b78731eac605ac0c36054f00634c426 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 15:43:52 +0200 Subject: [PATCH 07/13] Fix unrelated clippy warnings --- rewatch/src/build/parse.rs | 2 +- rewatch/src/config.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index f1a8583ffe..f9172fb7c9 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -199,7 +199,7 @@ pub fn generate_asts( &build_state.bsc_path, ) { has_failure = true; - stderr.push_str(&format!("{}\n", err)); + stderr.push_str(&format!("{err}\n")); } let mlmap_hash_after = helpers::compute_file_hash(Path::new(&compile_path)); diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index b0e192a28e..2d2c0e8d98 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -352,7 +352,7 @@ pub fn flatten_ppx_flags( let path = helpers::try_package_path( package_config, project_context, - format!("{}{}{}", &package_config.name, MAIN_SEPARATOR, y).as_str(), + &format!("{}{}{}", &package_config.name, MAIN_SEPARATOR, y), ) .map(|p| p.to_string_lossy().to_string())?; @@ -374,7 +374,7 @@ pub fn flatten_ppx_flags( Some('.') => helpers::try_package_path( package_config, project_context, - format!("{}{}{}", package_config.name, MAIN_SEPARATOR, &ys[0]).as_str(), + &format!("{}{}{}", package_config.name, MAIN_SEPARATOR, &ys[0]), ) .map(|p| p.to_string_lossy().to_string())?, _ => helpers::try_package_path(package_config, project_context, &ys[0]) From f00eb78ab68f4259b666218b7404e5f57c5d362c Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 15:52:23 +0200 Subject: [PATCH 08/13] Use a fixed rust version so there are no clippy surprises during CI --- .github/workflows/ci.yml | 2 +- rewatch/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 124bad72be..620740fe86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,7 +116,7 @@ jobs: if: steps.rewatch-build-cache.outputs.cache-hit != 'true' uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: 1.88.0 targets: ${{ matrix.rust-target }} - name: Build rewatch diff --git a/rewatch/Cargo.toml b/rewatch/Cargo.toml index 2eace6cfc2..68fcdac31f 100644 --- a/rewatch/Cargo.toml +++ b/rewatch/Cargo.toml @@ -2,7 +2,7 @@ name = "rescript" version = "12.0.0-beta.12" edition = "2024" -rust-version = "1.85" +rust-version = "1.88" [dependencies] ahash = "0.8.3" From 0ef8930990c078ccc8f5cf8e0821f7c0fde2f5e5 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 16:07:17 +0200 Subject: [PATCH 09/13] Add clippy? --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 620740fe86..ea81c57900 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,6 +118,7 @@ jobs: with: toolchain: 1.88.0 targets: ${{ matrix.rust-target }} + components: clippy - name: Build rewatch if: steps.rewatch-build-cache.outputs.cache-hit != 'true' From e2b7400c61af30c2f6796946e7754cc9d8ea168e Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 18 Sep 2025 16:14:43 +0200 Subject: [PATCH 10/13] Add fmt --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea81c57900..44c87e65bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: with: toolchain: 1.88.0 targets: ${{ matrix.rust-target }} - components: clippy + components: clippy, rustfmt - name: Build rewatch if: steps.rewatch-build-cache.outputs.cache-hit != 'true' From 14630aca4aa7f58cb34c2c12f20251dc381280a6 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 26 Sep 2025 13:11:51 +0200 Subject: [PATCH 11/13] Move node_modules_exist_cache to ProjectContext --- rewatch/src/helpers.rs | 27 +++++++++++++-------------- rewatch/src/project_context.rs | 9 +++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index 897abf57d5..ae279dfe52 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -9,7 +9,7 @@ use std::fs::File; use std::io::Read; use std::io::{self, BufRead}; use std::path::{Component, Path, PathBuf}; -use std::sync::{LazyLock, RwLock}; + use std::time::{SystemTime, UNIX_EPOCH}; pub type StdErr = String; @@ -32,20 +32,15 @@ pub mod emojis { pub static LINE_CLEAR: &str = "\x1b[2K\r"; } -// Cache existence checks for candidate node_modules paths during upward traversal. -// Keyed by the absolute candidate path; value is the existence boolean. -static NODE_MODULES_EXIST_CACHE: LazyLock>> = - LazyLock::new(|| RwLock::new(ahash::AHashMap::new())); - -fn cached_path_exists(path: &Path) -> bool { - match NODE_MODULES_EXIST_CACHE.read() { +fn cached_path_exists(project_context: &ProjectContext, path: &Path) -> bool { + match project_context.node_modules_exist_cache.read() { Ok(cache) => { if let Some(exists) = cache.get(path) { return *exists; } } Err(poisoned) => { - log::warn!("NODE_MODULES_EXIST_CACHE read lock poisoned; recovering"); + log::warn!("node_modules_exist_cache read lock poisoned; recovering"); let cache = poisoned.into_inner(); if let Some(exists) = cache.get(path) { return *exists; @@ -53,12 +48,12 @@ fn cached_path_exists(path: &Path) -> bool { } } let exists = path.exists(); - match NODE_MODULES_EXIST_CACHE.write() { + match project_context.node_modules_exist_cache.write() { Ok(mut cache) => { cache.insert(path.to_path_buf(), exists); } Err(poisoned) => { - log::warn!("NODE_MODULES_EXIST_CACHE write lock poisoned; recovering"); + log::warn!("node_modules_exist_cache write lock poisoned; recovering"); let mut cache = poisoned.into_inner(); cache.insert(path.to_path_buf(), exists); } @@ -196,7 +191,7 @@ pub fn try_package_path( if project_context.monorepo_context.is_none() { match package_config.path.parent().and_then(|p| p.parent()) { Some(start_dir) => { - return find_dep_in_upward_node_modules(start_dir, package_name); + return find_dep_in_upward_node_modules(project_context, start_dir, package_name); } None => { log::debug!( @@ -213,7 +208,11 @@ pub fn try_package_path( } } -fn find_dep_in_upward_node_modules(start_dir: &Path, package_name: &str) -> anyhow::Result { +fn find_dep_in_upward_node_modules( + project_context: &ProjectContext, + start_dir: &Path, + package_name: &str, +) -> anyhow::Result { log::debug!( "try_package_path: falling back to upward traversal for '{}' starting at '{}'", package_name, @@ -224,7 +223,7 @@ fn find_dep_in_upward_node_modules(start_dir: &Path, package_name: &str) -> anyh while let Some(dir) = current { let candidate = package_path(dir, package_name); log::debug!("try_package_path: checking '{}'", candidate.to_string_lossy()); - if cached_path_exists(&candidate) { + if cached_path_exists(project_context, &candidate) { log::debug!( "try_package_path: found '{}' at '{}' via upward traversal", package_name, diff --git a/rewatch/src/project_context.rs b/rewatch/src/project_context.rs index edf35e1b4d..fd075bf14d 100644 --- a/rewatch/src/project_context.rs +++ b/rewatch/src/project_context.rs @@ -1,12 +1,13 @@ use crate::build::packages; use crate::config::Config; use crate::helpers; -use ahash::AHashSet; +use ahash::{AHashMap, AHashSet}; use anyhow::Result; use anyhow::anyhow; use log::debug; use std::fmt; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::sync::RwLock; pub enum MonoRepoContext { /// Monorepo root - contains local dependencies (symlinked in node_modules) @@ -21,6 +22,7 @@ pub enum MonoRepoContext { pub struct ProjectContext { pub current_config: Config, pub monorepo_context: Option, + pub node_modules_exist_cache: RwLock>, // caches existence checks for candidate node_modules paths } fn format_dependencies(dependencies: &AHashSet) -> String { @@ -130,6 +132,7 @@ fn monorepo_or_single_project(path: &Path, current_config: Config) -> Result Result { From bbf71da95c52923151d7f36a89f51ad61734fc73 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 26 Sep 2025 13:38:13 +0200 Subject: [PATCH 12/13] Cache node_modules exists check instead. --- rewatch/src/helpers.rs | 33 ++++++++++++++++++--------------- rewatch/src/project_context.rs | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index ae279dfe52..25a7378f67 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -32,30 +32,31 @@ pub mod emojis { pub static LINE_CLEAR: &str = "\x1b[2K\r"; } -fn cached_path_exists(project_context: &ProjectContext, path: &Path) -> bool { +// Cached check: does the given directory contain a node_modules subfolder? +fn has_node_modules_cached(project_context: &ProjectContext, dir: &Path) -> bool { match project_context.node_modules_exist_cache.read() { Ok(cache) => { - if let Some(exists) = cache.get(path) { + if let Some(exists) = cache.get(dir) { return *exists; } } Err(poisoned) => { log::warn!("node_modules_exist_cache read lock poisoned; recovering"); let cache = poisoned.into_inner(); - if let Some(exists) = cache.get(path) { + if let Some(exists) = cache.get(dir) { return *exists; } } } - let exists = path.exists(); + let exists = dir.join("node_modules").exists(); match project_context.node_modules_exist_cache.write() { Ok(mut cache) => { - cache.insert(path.to_path_buf(), exists); + cache.insert(dir.to_path_buf(), exists); } Err(poisoned) => { log::warn!("node_modules_exist_cache write lock poisoned; recovering"); let mut cache = poisoned.into_inner(); - cache.insert(path.to_path_buf(), exists); + cache.insert(dir.to_path_buf(), exists); } } exists @@ -221,15 +222,17 @@ fn find_dep_in_upward_node_modules( let mut current = Some(start_dir); while let Some(dir) = current { - let candidate = package_path(dir, package_name); - log::debug!("try_package_path: checking '{}'", candidate.to_string_lossy()); - if cached_path_exists(project_context, &candidate) { - log::debug!( - "try_package_path: found '{}' at '{}' via upward traversal", - package_name, - candidate.to_string_lossy() - ); - return Ok(candidate); + if has_node_modules_cached(project_context, dir) { + let candidate = package_path(dir, package_name); + log::debug!("try_package_path: checking '{}'", candidate.to_string_lossy()); + if candidate.exists() { + log::debug!( + "try_package_path: found '{}' at '{}' via upward traversal", + package_name, + candidate.to_string_lossy() + ); + return Ok(candidate); + } } current = dir.parent(); } diff --git a/rewatch/src/project_context.rs b/rewatch/src/project_context.rs index fd075bf14d..cfd127860f 100644 --- a/rewatch/src/project_context.rs +++ b/rewatch/src/project_context.rs @@ -22,7 +22,7 @@ pub enum MonoRepoContext { pub struct ProjectContext { pub current_config: Config, pub monorepo_context: Option, - pub node_modules_exist_cache: RwLock>, // caches existence checks for candidate node_modules paths + pub node_modules_exist_cache: RwLock>, // caches whether a directory contains a node_modules subfolder } fn format_dependencies(dependencies: &AHashSet) -> String { From bdac509c70bbfe6f8610198756ac531d7609e7df Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 26 Sep 2025 13:58:36 +0200 Subject: [PATCH 13/13] Add project_cache to project_context. --- rewatch/src/helpers.rs | 68 +++++++++++++++++++++++++++------- rewatch/src/project_context.rs | 4 ++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/rewatch/src/helpers.rs b/rewatch/src/helpers.rs index 25a7378f67..553190c488 100644 --- a/rewatch/src/helpers.rs +++ b/rewatch/src/helpers.rs @@ -137,6 +137,25 @@ pub fn package_path(root: &Path, package_name: &str) -> PathBuf { root.join("node_modules").join(package_name) } +// Tap-style helper: cache and return the value (single clone for cache insert) +fn cache_package_tap( + project_context: &ProjectContext, + key: &(PathBuf, String), + value: PathBuf, +) -> anyhow::Result { + match project_context.packages_cache.write() { + Ok(mut cache) => { + cache.insert(key.clone(), value.clone()); + } + Err(poisoned) => { + log::warn!("packages_cache write lock poisoned; recovering"); + let mut cache = poisoned.into_inner(); + cache.insert(key.clone(), value.clone()); + } + } + Ok(value) +} + /// Tries to find a path for input package_name. /// The node_modules folder may be found at different levels in the case of a monorepo. /// This helper tries a variety of paths. @@ -145,14 +164,9 @@ pub fn try_package_path( project_context: &ProjectContext, package_name: &str, ) -> anyhow::Result { - // package folder + node_modules + package_name - // This can happen in the following scenario: - // The ProjectContext has a MonoRepoContext::MonorepoRoot. - // We are reading a dependency from the root package. - // And that local dependency has a hoisted dependency. - // Example, we need to find package_name `foo` in the following scenario: - // root/packages/a/node_modules/foo - let path_from_current_package = package_config + // try cached result first, keyed by (package_dir, package_name) + let pkg_name = package_name.to_string(); + let package_dir = package_config .path .parent() .ok_or_else(|| { @@ -160,8 +174,33 @@ pub fn try_package_path( "Expected {} to have a parent folder", package_config.path.to_string_lossy() ) - }) - .map(|parent_path| helpers::package_path(parent_path, package_name))?; + })? + .to_path_buf(); + + let cache_key = (package_dir.clone(), pkg_name.clone()); + match project_context.packages_cache.read() { + Ok(cache) => { + if let Some(cached) = cache.get(&cache_key) { + return Ok(cached.clone()); + } + } + Err(poisoned) => { + log::warn!("packages_cache read lock poisoned; recovering"); + let cache = poisoned.into_inner(); + if let Some(cached) = cache.get(&cache_key) { + return Ok(cached.clone()); + } + } + } + + // package folder + node_modules + package_name + // This can happen in the following scenario: + // The ProjectContext has a MonoRepoContext::MonorepoRoot. + // We are reading a dependency from the root package. + // And that local dependency has a hoisted dependency. + // Example, we need to find package_name `foo` in the following scenario: + // root/packages/a/node_modules/foo + let path_from_current_package = helpers::package_path(&package_dir, package_name); // current folder + node_modules + package_name let path_from_current_config = project_context @@ -179,11 +218,11 @@ pub fn try_package_path( // root folder + node_modules + package_name let path_from_root = package_path(project_context.get_root_path(), package_name); if path_from_current_package.exists() { - Ok(path_from_current_package) + cache_package_tap(project_context, &cache_key, path_from_current_package) } else if path_from_current_config.exists() { - Ok(path_from_current_config) + cache_package_tap(project_context, &cache_key, path_from_current_config) } else if path_from_root.exists() { - Ok(path_from_root) + cache_package_tap(project_context, &cache_key, path_from_root) } else { // As a last resort, when we're in a Single project context, traverse upwards // starting from the parent of the package root (package_config.path.parent().parent()) @@ -192,7 +231,8 @@ pub fn try_package_path( if project_context.monorepo_context.is_none() { match package_config.path.parent().and_then(|p| p.parent()) { Some(start_dir) => { - return find_dep_in_upward_node_modules(project_context, start_dir, package_name); + return find_dep_in_upward_node_modules(project_context, start_dir, package_name) + .and_then(|p| cache_package_tap(project_context, &cache_key, p)); } None => { log::debug!( diff --git a/rewatch/src/project_context.rs b/rewatch/src/project_context.rs index cfd127860f..439e3d58e2 100644 --- a/rewatch/src/project_context.rs +++ b/rewatch/src/project_context.rs @@ -23,6 +23,7 @@ pub struct ProjectContext { pub current_config: Config, pub monorepo_context: Option, pub node_modules_exist_cache: RwLock>, // caches whether a directory contains a node_modules subfolder + pub packages_cache: RwLock>, // caches full results of helpers::try_package_path per (package_dir, package_name) } fn format_dependencies(dependencies: &AHashSet) -> String { @@ -133,6 +134,7 @@ fn monorepo_or_single_project(path: &Path, current_config: Config) -> Result Result {