@@ -9,6 +9,7 @@ use std::fs::File;
9
9
use std:: io:: Read ;
10
10
use std:: io:: { self , BufRead } ;
11
11
use std:: path:: { Component , Path , PathBuf } ;
12
+
12
13
use std:: time:: { SystemTime , UNIX_EPOCH } ;
13
14
14
15
pub type StdErr = String ;
@@ -31,6 +32,36 @@ pub mod emojis {
31
32
pub static LINE_CLEAR : & str = "\x1b [2K\r " ;
32
33
}
33
34
35
+ // Cached check: does the given directory contain a node_modules subfolder?
36
+ fn has_node_modules_cached ( project_context : & ProjectContext , dir : & Path ) -> bool {
37
+ match project_context. node_modules_exist_cache . read ( ) {
38
+ Ok ( cache) => {
39
+ if let Some ( exists) = cache. get ( dir) {
40
+ return * exists;
41
+ }
42
+ }
43
+ Err ( poisoned) => {
44
+ log:: warn!( "node_modules_exist_cache read lock poisoned; recovering" ) ;
45
+ let cache = poisoned. into_inner ( ) ;
46
+ if let Some ( exists) = cache. get ( dir) {
47
+ return * exists;
48
+ }
49
+ }
50
+ }
51
+ let exists = dir. join ( "node_modules" ) . exists ( ) ;
52
+ match project_context. node_modules_exist_cache . write ( ) {
53
+ Ok ( mut cache) => {
54
+ cache. insert ( dir. to_path_buf ( ) , exists) ;
55
+ }
56
+ Err ( poisoned) => {
57
+ log:: warn!( "node_modules_exist_cache write lock poisoned; recovering" ) ;
58
+ let mut cache = poisoned. into_inner ( ) ;
59
+ cache. insert ( dir. to_path_buf ( ) , exists) ;
60
+ }
61
+ }
62
+ exists
63
+ }
64
+
34
65
/// This trait is used to strip the verbatim prefix from a Windows path.
35
66
/// On non-Windows systems, it simply returns the original path.
36
67
/// This is needed until the rescript compiler can handle such paths.
@@ -106,6 +137,25 @@ pub fn package_path(root: &Path, package_name: &str) -> PathBuf {
106
137
root. join ( "node_modules" ) . join ( package_name)
107
138
}
108
139
140
+ // Tap-style helper: cache and return the value (single clone for cache insert)
141
+ fn cache_package_tap (
142
+ project_context : & ProjectContext ,
143
+ key : & ( PathBuf , String ) ,
144
+ value : PathBuf ,
145
+ ) -> anyhow:: Result < PathBuf > {
146
+ match project_context. packages_cache . write ( ) {
147
+ Ok ( mut cache) => {
148
+ cache. insert ( key. clone ( ) , value. clone ( ) ) ;
149
+ }
150
+ Err ( poisoned) => {
151
+ log:: warn!( "packages_cache write lock poisoned; recovering" ) ;
152
+ let mut cache = poisoned. into_inner ( ) ;
153
+ cache. insert ( key. clone ( ) , value. clone ( ) ) ;
154
+ }
155
+ }
156
+ Ok ( value)
157
+ }
158
+
109
159
/// Tries to find a path for input package_name.
110
160
/// The node_modules folder may be found at different levels in the case of a monorepo.
111
161
/// This helper tries a variety of paths.
@@ -114,23 +164,43 @@ pub fn try_package_path(
114
164
project_context : & ProjectContext ,
115
165
package_name : & str ,
116
166
) -> anyhow:: Result < PathBuf > {
117
- // package folder + node_modules + package_name
118
- // This can happen in the following scenario:
119
- // The ProjectContext has a MonoRepoContext::MonorepoRoot.
120
- // We are reading a dependency from the root package.
121
- // And that local dependency has a hoisted dependency.
122
- // Example, we need to find package_name `foo` in the following scenario:
123
- // root/packages/a/node_modules/foo
124
- let path_from_current_package = package_config
167
+ // try cached result first, keyed by (package_dir, package_name)
168
+ let pkg_name = package_name. to_string ( ) ;
169
+ let package_dir = package_config
125
170
. path
126
171
. parent ( )
127
172
. ok_or_else ( || {
128
173
anyhow ! (
129
174
"Expected {} to have a parent folder" ,
130
175
package_config. path. to_string_lossy( )
131
176
)
132
- } )
133
- . map ( |parent_path| helpers:: package_path ( parent_path, package_name) ) ?;
177
+ } ) ?
178
+ . to_path_buf ( ) ;
179
+
180
+ let cache_key = ( package_dir. clone ( ) , pkg_name. clone ( ) ) ;
181
+ match project_context. packages_cache . read ( ) {
182
+ Ok ( cache) => {
183
+ if let Some ( cached) = cache. get ( & cache_key) {
184
+ return Ok ( cached. clone ( ) ) ;
185
+ }
186
+ }
187
+ Err ( poisoned) => {
188
+ log:: warn!( "packages_cache read lock poisoned; recovering" ) ;
189
+ let cache = poisoned. into_inner ( ) ;
190
+ if let Some ( cached) = cache. get ( & cache_key) {
191
+ return Ok ( cached. clone ( ) ) ;
192
+ }
193
+ }
194
+ }
195
+
196
+ // package folder + node_modules + package_name
197
+ // This can happen in the following scenario:
198
+ // The ProjectContext has a MonoRepoContext::MonorepoRoot.
199
+ // We are reading a dependency from the root package.
200
+ // And that local dependency has a hoisted dependency.
201
+ // Example, we need to find package_name `foo` in the following scenario:
202
+ // root/packages/a/node_modules/foo
203
+ let path_from_current_package = helpers:: package_path ( & package_dir, package_name) ;
134
204
135
205
// current folder + node_modules + package_name
136
206
let path_from_current_config = project_context
@@ -148,18 +218,76 @@ pub fn try_package_path(
148
218
// root folder + node_modules + package_name
149
219
let path_from_root = package_path ( project_context. get_root_path ( ) , package_name) ;
150
220
if path_from_current_package. exists ( ) {
151
- Ok ( path_from_current_package)
221
+ cache_package_tap ( project_context , & cache_key , path_from_current_package)
152
222
} else if path_from_current_config. exists ( ) {
153
- Ok ( path_from_current_config)
223
+ cache_package_tap ( project_context , & cache_key , path_from_current_config)
154
224
} else if path_from_root. exists ( ) {
155
- Ok ( path_from_root)
225
+ cache_package_tap ( project_context , & cache_key , path_from_root)
156
226
} else {
227
+ // As a last resort, when we're in a Single project context, traverse upwards
228
+ // starting from the parent of the package root (package_config.path.parent().parent())
229
+ // and probe each ancestor's node_modules for the dependency. This covers hoisted
230
+ // workspace setups when building a package standalone.
231
+ if project_context. monorepo_context . is_none ( ) {
232
+ match package_config. path . parent ( ) . and_then ( |p| p. parent ( ) ) {
233
+ Some ( start_dir) => {
234
+ return find_dep_in_upward_node_modules ( project_context, start_dir, package_name)
235
+ . and_then ( |p| cache_package_tap ( project_context, & cache_key, p) ) ;
236
+ }
237
+ None => {
238
+ log:: debug!(
239
+ "try_package_path: cannot compute start directory for upward traversal from '{}'" ,
240
+ package_config. path. to_string_lossy( )
241
+ ) ;
242
+ }
243
+ }
244
+ }
245
+
157
246
Err ( anyhow ! (
158
247
"The package \" {package_name}\" is not found (are node_modules up-to-date?)..."
159
248
) )
160
249
}
161
250
}
162
251
252
+ fn find_dep_in_upward_node_modules (
253
+ project_context : & ProjectContext ,
254
+ start_dir : & Path ,
255
+ package_name : & str ,
256
+ ) -> anyhow:: Result < PathBuf > {
257
+ log:: debug!(
258
+ "try_package_path: falling back to upward traversal for '{}' starting at '{}'" ,
259
+ package_name,
260
+ start_dir. to_string_lossy( )
261
+ ) ;
262
+
263
+ let mut current = Some ( start_dir) ;
264
+ while let Some ( dir) = current {
265
+ if has_node_modules_cached ( project_context, dir) {
266
+ let candidate = package_path ( dir, package_name) ;
267
+ log:: debug!( "try_package_path: checking '{}'" , candidate. to_string_lossy( ) ) ;
268
+ if candidate. exists ( ) {
269
+ log:: debug!(
270
+ "try_package_path: found '{}' at '{}' via upward traversal" ,
271
+ package_name,
272
+ candidate. to_string_lossy( )
273
+ ) ;
274
+ return Ok ( candidate) ;
275
+ }
276
+ }
277
+ current = dir. parent ( ) ;
278
+ }
279
+ log:: debug!(
280
+ "try_package_path: no '{}' found during upward traversal from '{}'" ,
281
+ package_name,
282
+ start_dir. to_string_lossy( )
283
+ ) ;
284
+ Err ( anyhow ! (
285
+ "try_package_path: upward traversal did not find '{}' starting at '{}'" ,
286
+ package_name,
287
+ start_dir. to_string_lossy( )
288
+ ) )
289
+ }
290
+
163
291
pub fn get_abs_path ( path : & Path ) -> PathBuf {
164
292
let abs_path_buf = PathBuf :: from ( path) ;
165
293
0 commit comments