@@ -99,10 +99,10 @@ async fn cargo_member_refusal(root: &Path) -> Option<Refusal> {
9999/// The `pnpm-lock.yaml` pnpm reads for a project directory that holds no
100100/// npm-family lock of its own, when it lives elsewhere and exists:
101101///
102- /// 1. a `lockfile-dir` in the project's `.npmrc`, or `lockfileDir` in its
103- /// `pnpm- workspace.yaml`, naming another directory;
104- /// 2. otherwise the nearest ancestor holding ` pnpm-workspace.yaml` (pnpm's
105- /// workspace root lookup), when the project has none of its own .
102+ /// The nearest `pnpm-workspace.yaml` supplies `lockfileDir`, ahead of the
103+ /// project's `.npmrc` and then the workspace root's `.npmrc`. A configured
104+ /// relative directory is resolved from the invocation cwd, as pnpm does;
105+ /// without an override, the workspace's lock lives at its root .
106106async fn pnpm_lock_elsewhere ( root : & Path ) -> Option < PathBuf > {
107107 let has_own_lock = OWN_LOCKS
108108 . iter ( )
@@ -117,33 +117,51 @@ async fn pnpm_lock_elsewhere(root: &Path) -> Option<PathBuf> {
117117 . await
118118 . unwrap_or_else ( |_| root. to_path_buf ( ) ) ;
119119
120- let workspace_yaml = read_regular_to_string ( & root. join ( PNPM_WORKSPACE ) )
121- . await
122- . ok ( ) ;
123- let configured = match read_regular_to_string ( & root. join ( ".npmrc" ) ) . await {
124- Ok ( npmrc) => npmrc_top_level_value ( & npmrc, "lockfile-dir" ) ,
125- Err ( _) => None ,
120+ let mut workspace = None ;
121+ for ancestor in canonical. ancestors ( ) {
122+ let path = ancestor. join ( PNPM_WORKSPACE ) ;
123+ if let Ok ( yaml) = read_regular_to_string ( & path) . await {
124+ workspace = Some ( ( ancestor. to_path_buf ( ) , yaml) ) ;
125+ break ;
126+ }
127+ if ancestor == canonical && path. exists ( ) {
128+ // An unreadable local workspace file still bounds the project.
129+ break ;
130+ }
131+ }
132+
133+ // Native pnpm 10: workspace YAML beats both npmrc files; the member's
134+ // npmrc beats the workspace root's. A member inherits root npmrc settings
135+ // even when its own directory has no pnpm-workspace.yaml.
136+ let mut configured = workspace
137+ . as_ref ( )
138+ . and_then ( |( _, yaml) | workspace_lockfile_dir ( yaml) ) ;
139+ if configured. is_none ( ) {
140+ configured = npmrc_lockfile_dir ( & canonical) . await ;
141+ }
142+ if configured. is_none ( ) {
143+ if let Some ( ( workspace_root, _) ) = & workspace {
144+ if workspace_root != & canonical {
145+ configured = npmrc_lockfile_dir ( workspace_root) . await ;
146+ }
147+ }
126148 }
127- . or_else ( || workspace_yaml. as_deref ( ) . and_then ( workspace_lockfile_dir) ) ;
128149 if let Some ( dir) = configured {
150+ // Even an inherited relative override is based on the invocation
151+ // directory, not on the directory containing the setting.
129152 return lock_elsewhere ( & canonical, & canonical, & dir) . await ;
130153 }
131- if workspace_yaml. is_some ( ) || root. join ( PNPM_WORKSPACE ) . exists ( ) {
132- // The project is its own workspace root.
133- return None ;
134- }
135- for ancestor in canonical. ancestors ( ) . skip ( 1 ) {
136- let Ok ( yaml) = read_regular_to_string ( & ancestor. join ( PNPM_WORKSPACE ) ) . await else {
137- continue ;
138- } ;
139- // The workspace root may relocate the lock with its own
140- // `lockfileDir`, relative to the root.
141- let dir = workspace_lockfile_dir ( & yaml) . unwrap_or_else ( || "." . to_string ( ) ) ;
142- return lock_elsewhere ( & canonical, ancestor, & dir) . await ;
154+ if let Some ( ( workspace_root, _) ) = workspace {
155+ return lock_elsewhere ( & canonical, & workspace_root, "." ) . await ;
143156 }
144157 None
145158}
146159
160+ async fn npmrc_lockfile_dir ( root : & Path ) -> Option < String > {
161+ let npmrc = read_regular_to_string ( & root. join ( ".npmrc" ) ) . await . ok ( ) ?;
162+ npmrc_top_level_value ( & npmrc, "lockfile-dir" )
163+ }
164+
147165/// `<base>/<dir>/pnpm-lock.yaml` when it exists and `<base>/<dir>` is not
148166/// the project directory itself.
149167async fn lock_elsewhere ( project : & Path , base : & Path , dir : & str ) -> Option < PathBuf > {
@@ -286,12 +304,16 @@ mod tests {
286304 assert_eq ! ( code( tmp. path( ) , "npm" ) . await , None ) ;
287305 }
288306
289- /// A workspace root that relocates its lock with `lockfileDir` still
290- /// governs its members (Bugbot on #598) .
307+ /// An inherited relative `lockfileDir` is resolved from the member cwd,
308+ /// verified with native pnpm, rather than from the workspace root .
291309 #[ tokio:: test]
292310 async fn pnpm_member_of_workspace_with_relocated_lock_is_refused ( ) {
293311 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
294- write ( tmp. path ( ) , "pnpm-lock.yaml" , "lockfileVersion: '9.0'\n " ) ;
312+ write (
313+ tmp. path ( ) ,
314+ "ws/packages/pnpm-lock.yaml" ,
315+ "lockfileVersion: '9.0'\n " ,
316+ ) ;
295317 write (
296318 tmp. path ( ) ,
297319 "ws/pnpm-workspace.yaml" ,
@@ -305,6 +327,71 @@ mod tests {
305327 ) ;
306328 }
307329
330+ #[ tokio:: test]
331+ async fn pnpm_config_precedence_matches_native_workspace_install ( ) {
332+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
333+ let root = tmp. path ( ) . join ( "ws" ) ;
334+ let member = root. join ( "packages/a" ) ;
335+ write ( & root, "packages/a/package.json" , "{}" ) ;
336+ write ( & root, PNPM_LOCK , "lockfileVersion: '9.0'\n " ) ;
337+ for dir in [ "yaml-locks" , "root-rc-locks" , "member-rc-locks" ] {
338+ write (
339+ tmp. path ( ) ,
340+ & format ! ( "{dir}/{PNPM_LOCK}" ) ,
341+ "lockfileVersion: '9.0'\n " ,
342+ ) ;
343+ }
344+ let yaml_lock = tmp. path ( ) . join ( "yaml-locks" ) ;
345+ let root_rc_lock = tmp. path ( ) . join ( "root-rc-locks" ) ;
346+ let member_rc_lock = tmp. path ( ) . join ( "member-rc-locks" ) ;
347+ write (
348+ & root,
349+ PNPM_WORKSPACE ,
350+ & format ! (
351+ "packages:\n - packages/*\n lockfileDir: '{}'\n " ,
352+ yaml_lock. display( )
353+ ) ,
354+ ) ;
355+ write (
356+ & root,
357+ ".npmrc" ,
358+ & format ! ( "lockfile-dir={}\n " , root_rc_lock. display( ) ) ,
359+ ) ;
360+ write (
361+ & member,
362+ ".npmrc" ,
363+ & format ! ( "lockfile-dir={}\n " , member_rc_lock. display( ) ) ,
364+ ) ;
365+ for expected in [ & yaml_lock, & member_rc_lock, & root_rc_lock, & root] {
366+ let found = pnpm_lock_elsewhere ( & member) . await . expect ( "governing lock" ) ;
367+ assert_eq ! (
368+ std:: fs:: canonicalize( found) . unwrap( ) ,
369+ std:: fs:: canonicalize( expected. join( PNPM_LOCK ) ) . unwrap( )
370+ ) ;
371+ if expected == & yaml_lock {
372+ write ( & root, PNPM_WORKSPACE , "packages:\n - packages/*\n " ) ;
373+ } else if expected == & member_rc_lock {
374+ std:: fs:: remove_file ( member. join ( ".npmrc" ) ) . unwrap ( ) ;
375+ } else if expected == & root_rc_lock {
376+ std:: fs:: remove_file ( root. join ( ".npmrc" ) ) . unwrap ( ) ;
377+ }
378+ }
379+ }
380+
381+ #[ tokio:: test]
382+ async fn pnpm_member_own_workspace_bounds_ancestor_lookup ( ) {
383+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
384+ write ( tmp. path ( ) , PNPM_WORKSPACE , "packages:\n - packages/*\n " ) ;
385+ write ( tmp. path ( ) , PNPM_LOCK , "lockfileVersion: '9.0'\n " ) ;
386+ write ( tmp. path ( ) , "packages/a/package.json" , "{}" ) ;
387+ write (
388+ tmp. path ( ) ,
389+ "packages/a/pnpm-workspace.yaml" ,
390+ "packages: []\n " ,
391+ ) ;
392+ assert_eq ! ( code( & tmp. path( ) . join( "packages/a" ) , "npm" ) . await , None ) ;
393+ }
394+
308395 /// #417: a cargo workspace member is refused with the vendored code; the
309396 /// root and a standalone crate are not.
310397 #[ tokio:: test]
0 commit comments