3434//! (pnpm's `.pnpm`, Yarn's `.store`, vlt's `.vlt`, bun's `.bun`, npm's
3535//! linked `.store`) resolve inside a `node_modules` tree; the one exception,
3636//! Yarn's pnpm-linker store relocated by `pnpmStoreFolder`, is recognized
37- //! from the project 's `.yarnrc.yml` .
37+ //! only for an active Yarn pnpm install and a registry entry 's layout .
3838
3939use std:: path:: { Path , PathBuf } ;
4040
@@ -221,27 +221,54 @@ fn linked_source_of(pkg_path: &Path, real: PathBuf) -> Option<SharedStore> {
221221 } )
222222}
223223
224- /// Whether `real` is an entry `<store>/<entry>/package` of Yarn's pnpm
225- /// linker store relocated outside `node_modules` (`nodeLinker: pnpm` with
226- /// `pnpmStoreFolder: .cache/.store`): an installed registry copy, not
227- /// first-party source. `<store>` is the `pnpmStoreFolder` of the nearest
228- /// `.yarnrc.yml` at or above the project that sets it, resolved against
229- /// that file's directory as Yarn does. A store that contains the project
230- /// itself (`pnpmStoreFolder: .`) is not honored, so the setting can never
231- /// re-admit a workspace member's source.
224+ /// Whether `real` is an installed registry entry of Yarn's pnpm linker
225+ /// store relocated outside `node_modules` (`nodeLinker: pnpm` with
226+ /// `pnpmStoreFolder: .cache/.store`), not first-party source. Every part of
227+ /// that must hold, so that an inactive or stray `.yarnrc.yml` (an npm
228+ /// workspace carrying `pnpmStoreFolder: packages`) can never admit a
229+ /// workspace member:
230+ ///
231+ /// * the project is a Yarn project: a `yarn.lock` at or above it;
232+ /// * the active linker is pnpm: the nearest `.yarnrc.yml` at or above the
233+ /// project that sets `nodeLinker` sets it to `pnpm`;
234+ /// * `<store>` is the `pnpmStoreFolder` of the nearest `.yarnrc.yml` that
235+ /// sets it, resolved against that file's directory as Yarn does, and does
236+ /// not contain the project (`pnpmStoreFolder: .`);
237+ /// * `real` is exactly `<store>/<entry>/package`, where `<entry>` is Yarn's
238+ /// slug of a registry locator, `<ident>-npm-<version>-<10 hex>`: the
239+ /// layout Yarn gives a hard (installed) package, never a workspace.
232240fn in_yarn_pnpm_store ( node_modules : & Path , real : & Path ) -> bool {
233241 let Some ( project) = node_modules. parent ( ) else {
234242 return false ;
235243 } ;
236- let Some ( store) = project. ancestors ( ) . find_map ( |dir| {
237- let rc = crate :: utils:: fs:: read_regular_to_string_sync ( & dir. join ( ".yarnrc.yml" ) ) . ok ( ) ?;
238- let value = crate :: vendor:: yarn_berry_lock:: yarnrc_scalar ( & rc, "pnpmStoreFolder" ) ?;
239- ( !value. is_empty ( ) ) . then ( || dir. join ( value) )
240- } ) else {
244+ if !project
245+ . ancestors ( )
246+ . any ( |dir| dir. join ( "yarn.lock" ) . is_file ( ) )
247+ {
241248 return false ;
249+ }
250+ let rcs: Vec < ( & Path , String ) > = project
251+ . ancestors ( )
252+ . filter_map ( |dir| {
253+ let rc = crate :: utils:: fs:: read_regular_to_string_sync ( & dir. join ( ".yarnrc.yml" ) ) ;
254+ rc. ok ( ) . map ( |rc| ( dir, rc) )
255+ } )
256+ . collect ( ) ;
257+ let nearest = |key : & str | {
258+ rcs. iter ( ) . find_map ( |( dir, rc) | {
259+ crate :: vendor:: yarn_berry_lock:: yarnrc_scalar ( rc, key) . map ( |v| ( * dir, v. to_string ( ) ) )
260+ } )
242261 } ;
243- let ( Ok ( store) , Ok ( project) ) = ( std:: fs:: canonicalize ( store) , std:: fs:: canonicalize ( project) )
244- else {
262+ if nearest ( "nodeLinker" ) . is_none_or ( |( _, linker) | linker != "pnpm" ) {
263+ return false ;
264+ }
265+ let Some ( ( rc_dir, value) ) = nearest ( "pnpmStoreFolder" ) . filter ( |( _, v) | !v. is_empty ( ) ) else {
266+ return false ;
267+ } ;
268+ let ( Ok ( store) , Ok ( project) ) = (
269+ std:: fs:: canonicalize ( rc_dir. join ( value) ) ,
270+ std:: fs:: canonicalize ( project) ,
271+ ) else {
245272 return false ;
246273 } ;
247274 if project. starts_with ( & store) {
@@ -251,11 +278,25 @@ fn in_yarn_pnpm_store(node_modules: &Path, real: &Path) -> bool {
251278 return false ;
252279 } ;
253280 let mut parts = rest. components ( ) ;
254- parts. next ( ) . is_some ( )
281+ let entry = parts. next ( ) . and_then ( |c| c. as_os_str ( ) . to_str ( ) ) ;
282+ entry. is_some_and ( is_yarn_registry_slug)
255283 && parts. next ( ) . is_some_and ( |c| c. as_os_str ( ) == "package" )
256284 && parts. next ( ) . is_none ( )
257285}
258286
287+ /// `left-pad-npm-1.3.0-0123456789`, `@types-node-npm-20.1.0-abcdef0123`:
288+ /// Yarn's slug of an `npm:` locator, ending in ten hex digits of its hash.
289+ fn is_yarn_registry_slug ( entry : & str ) -> bool {
290+ let Some ( ( head, hash) ) = entry. rsplit_once ( '-' ) else {
291+ return false ;
292+ } ;
293+ hash. len ( ) == 10
294+ && hash. bytes ( ) . all ( |b| b. is_ascii_hexdigit ( ) )
295+ && head
296+ . split_once ( "-npm-" )
297+ . is_some_and ( |( ident, version) | !ident. is_empty ( ) && !version. is_empty ( ) )
298+ }
299+
259300fn is_node_modules ( dir : & Path ) -> bool {
260301 dir. file_name ( ) . is_some_and ( |n| n == "node_modules" )
261302}
@@ -519,70 +560,146 @@ mod tests {
519560
520561 /// Yarn's pnpm linker with a relocated `pnpmStoreFolder` links
521562 /// `node_modules/<name>` to `<store>/<entry>/package` outside every
522- /// `node_modules`: an installed copy, still patchable. The setting is
523- /// read from the nearest `.yarnrc.yml` at or above the project, and
524- /// cannot admit first-party source.
563+ /// `node_modules`: an installed copy, still patchable. Only an active
564+ /// Yarn pnpm install qualifies, and only a registry entry's `package`
565+ /// dir, so the setting can never admit first-party source.
525566 #[ cfg( unix) ]
526567 #[ tokio:: test]
527568 async fn relocated_yarn_pnpm_store_is_not_refused ( ) {
528569 use std:: os:: unix:: fs:: symlink;
570+ let is_linked_source = |pkg : PathBuf | async move {
571+ shared_store_of ( & pkg) . await . map ( |s| s. kind ) == Some ( SharedStoreKind :: LinkedSource )
572+ } ;
529573 let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
530574 let root = dir. path ( ) . join ( "ws" ) ;
531575 let nm = root. join ( "node_modules" ) ;
532576 std:: fs:: create_dir_all ( & nm) . unwrap ( ) ;
533577 let store = root. join ( ".cache" ) . join ( ".store" ) ;
534- let entry = store. join ( "left-pad-npm-1.3.0-x " ) . join ( "package" ) ;
578+ let entry = store. join ( "left-pad-npm-1.3.0-0123456789 " ) . join ( "package" ) ;
535579 std:: fs:: create_dir_all ( & entry) . unwrap ( ) ;
536580 symlink ( & entry, nm. join ( "left-pad" ) ) . unwrap ( ) ;
537581 // A workspace member linked beside it stays refused.
538582 let member = root. join ( "packages" ) . join ( "a" ) ;
539583 std:: fs:: create_dir_all ( & member) . unwrap ( ) ;
540584 symlink ( & member, nm. join ( "a" ) ) . unwrap ( ) ;
541- // A link into the store that is not an entry's `package` dir.
542- symlink ( store. join ( "left-pad-npm-1.3.0-x" ) , nm. join ( "odd" ) ) . unwrap ( ) ;
543-
544- // Without the setting, the relocated store is not recognized.
545- assert_eq ! (
546- shared_store_of( & nm. join( "left-pad" ) ) . await . map( |s| s. kind) ,
547- Some ( SharedStoreKind :: LinkedSource )
548- ) ;
549-
550- // Set in an ancestor's `.yarnrc.yml`, resolved against its dir.
585+ // Store links that are not a registry entry's `package` dir.
586+ symlink ( store. join ( "left-pad-npm-1.3.0-0123456789" ) , nm. join ( "odd" ) ) . unwrap ( ) ;
587+ let soft = store
588+ . join ( "b-workspace-packages-b-0123456789" )
589+ . join ( "package" ) ;
590+ std:: fs:: create_dir_all ( & soft) . unwrap ( ) ;
591+ symlink ( & soft, nm. join ( "b" ) ) . unwrap ( ) ;
592+ let refused = [ nm. join ( "a" ) , nm. join ( "odd" ) , nm. join ( "b" ) ] ;
593+
594+ // Without a Yarn pnpm install, the relocated store is not recognized.
595+ assert ! ( is_linked_source( nm. join( "left-pad" ) ) . await ) ;
551596 std:: fs:: write (
552597 dir. path ( ) . join ( ".yarnrc.yml" ) ,
553598 "nodeLinker: pnpm\n pnpmStoreFolder: \" ws/.cache/.store\" \n " ,
554599 )
555600 . unwrap ( ) ;
601+ assert ! ( is_linked_source( nm. join( "left-pad" ) ) . await , "no yarn.lock" ) ;
602+
603+ // With a yarn.lock, an ancestor's settings resolve against its dir.
604+ std:: fs:: write ( root. join ( "yarn.lock" ) , "" ) . unwrap ( ) ;
556605 assert_eq ! ( shared_store_of( & nm. join( "left-pad" ) ) . await , None ) ;
557606 // The project's own `.yarnrc.yml` wins over the ancestor's.
558607 std:: fs:: write (
559608 root. join ( ".yarnrc.yml" ) ,
560- "nodeLinker: pnpm \n pnpmStoreFolder : .cache/.store # relocated\n " ,
609+ "pnpmStoreFolder : .cache/.store # relocated\n " ,
561610 )
562611 . unwrap ( ) ;
563612 assert_eq ! ( shared_store_of( & nm. join( "left-pad" ) ) . await , None ) ;
564- for pkg in [ nm. join ( "a" ) , nm. join ( "odd" ) ] {
565- assert_eq ! (
566- shared_store_of( & pkg) . await . map( |s| s. kind) ,
567- Some ( SharedStoreKind :: LinkedSource ) ,
568- "{}" ,
569- pkg. display( )
570- ) ;
613+ for pkg in & refused {
614+ assert ! ( is_linked_source( pkg. clone( ) ) . await , "{}" , pkg. display( ) ) ;
571615 }
572-
616+ // An inactive linker setting turns the exception off.
617+ std:: fs:: write (
618+ root. join ( ".yarnrc.yml" ) ,
619+ "nodeLinker: node-modules\n pnpmStoreFolder: .cache/.store\n " ,
620+ )
621+ . unwrap ( ) ;
622+ assert ! (
623+ is_linked_source( nm. join( "left-pad" ) ) . await ,
624+ "node-modules linker"
625+ ) ;
573626 // A store that contains the project is not honored.
574- let member_entry = root. join ( "packages" ) . join ( "b" ) . join ( "package" ) ;
575- std:: fs:: create_dir_all ( & member_entry) . unwrap ( ) ;
576- symlink ( & member_entry, nm. join ( "b" ) ) . unwrap ( ) ;
577- std:: fs:: write ( root. join ( ".yarnrc.yml" ) , "pnpmStoreFolder: .\n " ) . unwrap ( ) ;
578- for pkg in [ nm. join ( "left-pad" ) , nm. join ( "b" ) ] {
627+ std:: fs:: write (
628+ root. join ( ".yarnrc.yml" ) ,
629+ "nodeLinker: pnpm\n pnpmStoreFolder: .\n " ,
630+ )
631+ . unwrap ( ) ;
632+ assert ! (
633+ is_linked_source( nm. join( "left-pad" ) ) . await ,
634+ "store contains project"
635+ ) ;
636+ }
637+
638+ /// The review reproduction: an npm workspace whose stray `.yarnrc.yml`
639+ /// names its `packages/` dir as a pnpm store. npm ignores the file, and
640+ /// `node_modules/left-pad` is the first-party member `packages/foo/package`.
641+ #[ cfg( unix) ]
642+ #[ tokio:: test]
643+ async fn stray_yarn_store_setting_does_not_admit_an_npm_workspace_member ( ) {
644+ use std:: os:: unix:: fs:: symlink;
645+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
646+ let root = dir. path ( ) ;
647+ let nm = root. join ( "node_modules" ) ;
648+ std:: fs:: create_dir_all ( & nm) . unwrap ( ) ;
649+ std:: fs:: write ( root. join ( "package-lock.json" ) , "{}" ) . unwrap ( ) ;
650+ std:: fs:: write (
651+ root. join ( ".yarnrc.yml" ) ,
652+ "nodeLinker: node-modules\n pnpmStoreFolder: packages\n " ,
653+ )
654+ . unwrap ( ) ;
655+ let member = root. join ( "packages" ) . join ( "foo" ) . join ( "package" ) ;
656+ std:: fs:: create_dir_all ( & member) . unwrap ( ) ;
657+ symlink ( & member, nm. join ( "left-pad" ) ) . unwrap ( ) ;
658+ let named = root
659+ . join ( "packages" )
660+ . join ( "left-pad-npm-1.3.0-0123456789" )
661+ . join ( "package" ) ;
662+ std:: fs:: create_dir_all ( & named) . unwrap ( ) ;
663+ symlink ( & named, nm. join ( "named" ) ) . unwrap ( ) ;
664+ for pkg in [ nm. join ( "left-pad" ) , nm. join ( "named" ) ] {
579665 assert_eq ! (
580666 shared_store_of( & pkg) . await . map( |s| s. kind) ,
581667 Some ( SharedStoreKind :: LinkedSource ) ,
582668 "{}" ,
583669 pkg. display( )
584670 ) ;
585671 }
672+ // Even an active-looking setting needs a yarn.lock and a registry slug.
673+ std:: fs:: write (
674+ root. join ( ".yarnrc.yml" ) ,
675+ "nodeLinker: pnpm\n pnpmStoreFolder: packages\n " ,
676+ )
677+ . unwrap ( ) ;
678+ assert_eq ! (
679+ shared_store_of( & nm. join( "left-pad" ) ) . await . map( |s| s. kind) ,
680+ Some ( SharedStoreKind :: LinkedSource )
681+ ) ;
682+ }
683+
684+ #[ test]
685+ fn yarn_registry_slugs ( ) {
686+ for ok in [
687+ "left-pad-npm-1.3.0-0123456789" ,
688+ "@types-node-npm-20.1.0-abcdef0123" ,
689+ ] {
690+ assert ! ( is_yarn_registry_slug( ok) , "{ok}" ) ;
691+ }
692+ for bad in [
693+ "package" ,
694+ "foo" ,
695+ "left-pad-npm-1.3.0-x" ,
696+ "left-pad-npm-1.3.0-012345678" ,
697+ "left-pad-npm-1.3.0-0123456789a" ,
698+ "b-workspace-packages-b-0123456789" ,
699+ "-npm-1.0.0-0123456789" ,
700+ ] {
701+ assert ! ( !is_yarn_registry_slug( bad) , "{bad}" ) ;
702+ }
586703 }
587704
588705 #[ test]
0 commit comments