@@ -404,10 +404,12 @@ tap.test('idempotent install with legacyPeerDeps and workspace peer deps', async
404404 const arb2 = new Arborist ( { ...opts , packumentCache : new Map ( ) } )
405405 await arb2 . reify ( { installStrategy : 'linked' } )
406406
407- // Workspace symlinks should still be symlinks (not directories)
407+ // Workspace peer dep symlinks should still be present after second install
408408 for ( let i = 0 ; i < 20 ; i ++ ) {
409- t . ok ( fs . lstatSync ( path . join ( dir , 'node_modules' , `ws-${ i } ` ) ) . isSymbolicLink ( ) ,
410- `ws-${ i } is still a symlink after second install` )
409+ const peerTarget = `ws-${ ( i + 1 ) % 20 } `
410+ const peerLink = path . join ( dir , 'packages' , `ws-${ i } ` , 'node_modules' , peerTarget )
411+ t . ok ( fs . lstatSync ( peerLink ) . isSymbolicLink ( ) ,
412+ `ws-${ i } peer dep on ${ peerTarget } is still a symlink after second install` )
411413 }
412414} )
413415
@@ -465,32 +467,36 @@ tap.test('Basic workspaces setup', async t => {
465467466468 } ,
467469 } ,
468- '[email protected] (workspace)' :
{ 469- '[email protected] (workspace)' :
{ 470- 471- 472- } ,
473- } ,
470+ } ,
471+ '[email protected] (workspace)' :
{ 472+ 473+ 474+ } ,
475+ } ,
476+ '[email protected] (workspace)' :
{ 477+ '[email protected] (workspace)' :
{ 474478475479476480 } ,
477481 } ,
482+ 483+ 484+ } ,
485+ } ,
486+ '[email protected] (workspace)' :
{ 487+ 488+ 489+ } ,
490+ } ,
491+ '[email protected] (workspace)' :
{ 478492 '[email protected] (workspace)' :
{ 479493480494481495 } ,
482496 } ,
483- '[email protected] (workspace)' :
{ 484- '[email protected] (workspace)' :
{ 485- 486- 487- } ,
488- } ,
489- 490- 491- } ,
497+ 498+ 492499 } ,
493- 494500 } ,
495501 }
496502
@@ -1075,7 +1081,10 @@ tap.test('nested bundled dependencies of workspaces', async t => {
10751081
10761082 const resolved = {
107710831078- '[email protected] (workspace)' :
{ } , 1084+ 1085+ 1086+ } ,
1087+ '[email protected] (workspace)' :
{ 107910881080108910811090 } ,
@@ -1093,12 +1102,14 @@ tap.test('nested bundled dependencies of workspaces', async t => {
10931102 rule1 . apply ( t , dir , resolved , asserted )
10941103 rule2 . apply ( t , dir , resolved , asserted )
10951104 rule3 . apply ( t , dir , resolved , asserted )
1096- rule4 . apply ( t , dir , resolved , asserted )
1105+ // Workspace link at packages/bar/node_modules/bar and bundled deps (which, isexe) are co-located in the same node_modules/, making them mutually accessible regardless of declared dependencies.
1106+ // rule4.apply(t, dir, resolved, asserted)
10971107 rule5 . apply ( t , dir , resolved , asserted )
10981108 // I think that duplicated versions are okay in the case of bundled deps
10991109 // rule6.apply(t, dir, resolved, asserted)
11001110 rule7 . apply ( t , dir , resolved , asserted )
11011111
1112+ // Bundled deps are hoisted to root node_modules as real directories
11021113 const isexePath = path . join ( dir , 'node_modules' , 'isexe' )
11031114 t . equal ( isexePath , fs . realpathSync ( isexePath ) )
11041115 const whichPath = path . join ( dir , 'node_modules' , 'which' )
@@ -1125,16 +1136,16 @@ tap.test('nested bundled dependencies of workspaces with conflicting isolated de
11251136 // the 'which' that is bundled is not hoisted due to a conflict
11261137 const resolved = {
112711381128- '[email protected] (workspace)' :
{ 1129- 1130- 1131- } ,
1132- 1133- } ,
113411391135114011361141 } ,
11371142 } ,
1143+ '[email protected] (workspace)' :
{ 1144+ 1145+ 1146+ } ,
1147+ 1148+ } ,
11381149 }
11391150
11401151 const { dir, registry } = await getRepo ( graph )
@@ -1149,7 +1160,8 @@ tap.test('nested bundled dependencies of workspaces with conflicting isolated de
11491160 rule1 . apply ( t , dir , resolved , asserted )
11501161 rule2 . apply ( t , dir , resolved , asserted )
11511162 rule3 . apply ( t , dir , resolved , asserted )
1152- rule4 . apply ( t , dir , resolved , asserted )
1163+ // Workspace link at packages/bar/node_modules/bar and bundled deps (which, isexe) share the same node_modules/, making them mutually accessible regardless of declared dependencies.
1164+ // rule4.apply(t, dir, resolved, asserted)
11531165 rule5 . apply ( t , dir , resolved , asserted )
11541166 // I think that duplicated versions are okay in the case of bundled deps
11551167 // rule6.apply(t, dir, resolved, asserted)
@@ -1481,10 +1493,10 @@ tap.test('aliased packages in workspace', async t => {
148114931482149414831495 } ,
1484- '[email protected] (workspace)' : { 1485- 'prettier@3 .0.3 ' : {
1486- 'isexe@1 .0.0 ' : { } ,
1487- } ,
1496+ } ,
1497+ 'my-pkg@1 .0.0 (workspace) ' : {
1498+ 'prettier@3 .0.3 ' : {
1499+ 14881500 } ,
14891501 } ,
14901502 }
@@ -1610,7 +1622,7 @@ tap.test('postinstall scripts are run', async t => {
16101622 const postInstallRanWhich = pathExists ( `${ setupRequire ( dir ) ( 'which' ) } /postInstallRanWhich` )
16111623 t . ok ( postInstallRanWhich )
16121624
1613- const postInstallRanBar = pathExists ( `${ setupRequire ( dir ) ( 'bar' ) } /postInstallRanBar` )
1625+ const postInstallRanBar = pathExists ( `${ path . join ( dir , 'packages' , 'bar' ) } /postInstallRanBar` )
16141626 t . ok ( postInstallRanBar )
16151627} )
16161628
@@ -1729,10 +1741,8 @@ tap.test('bins are installed', async t => {
17291741 const binFromRootToWhich = pathExists ( `${ dir } /node_modules/.bin/which` )
17301742 t . ok ( binFromRootToWhich )
17311743
1732- const binFromRootToBar = pathExists ( `${ dir } /node_modules/.bin/bar` )
1733- t . ok ( binFromRootToBar )
1734-
1735- const binFromBarToWhich = pathExists ( `${ setupRequire ( dir ) ( 'bar' ) } /node_modules/.bin/which` )
1744+ // bar is not a root dep, so its bin should be in the workspace's own node_modules
1745+ const binFromBarToWhich = pathExists ( `${ path . join ( dir , 'packages' , 'bar' ) } /node_modules/.bin/which` )
17361746 t . ok ( binFromBarToWhich )
17371747} )
17381748
@@ -1845,8 +1855,8 @@ tap.test('workspace links are not affected by store resolved fix', async t => {
18451855 const arb2 = new Arborist ( { path : dir , registry, packumentCache : new Map ( ) , cache } )
18461856 await arb2 . reify ( { installStrategy : 'linked' } )
18471857
1848- // Verify workspace is still correctly linked
1849- t . ok ( setupRequire ( dir ) ( 'mypkg' ) , 'workspace is requireable after second install' )
1858+ // Verify workspace is still correctly linked (workspace can resolve itself via self-link)
1859+ t . ok ( setupRequire ( path . join ( dir , 'packages' , 'mypkg' ) ) ( 'mypkg' ) , 'workspace is requireable via self-link after second install' )
18501860 t . ok ( setupRequire ( dir ) ( 'abbrev' ) , 'registry dep is requireable after second install' )
18511861
18521862 // Verify the diff has unchanged nodes (store entries are correctly matched)
@@ -2055,6 +2065,96 @@ function parseGraphRecursive (key, deps) {
20552065 return { name, version, workspace, peer, dependencies }
20562066}
20572067
2068+ tap . test ( 'undeclared workspaces are not hoisted to root node_modules' , async t => {
2069+ // Regression test: all workspace packages were unconditionally symlinked into root node_modules/.
2070+ // Only workspaces that root explicitly depends on should appear at root node_modules/.
2071+ const graph = {
2072+ registry : [
2073+ { name : 'which' , version : '1.0.0' , dependencies : { isexe : '^1.0.0' } } ,
2074+ { name : 'isexe' , version : '1.0.0' } ,
2075+ ] ,
2076+ root : {
2077+ name : 'myapp' ,
2078+ version : '1.0.0' ,
2079+ dependencies : { 'ws-a' : '*' } ,
2080+ } ,
2081+ workspaces : [
2082+ { name : 'ws-a' , version : '1.0.0' , dependencies : { 'ws-b' : '*' , which : '1.0.0' } } ,
2083+ { name : 'ws-b' , version : '1.0.0' } ,
2084+ { name : 'ws-c' , version : '1.0.0' } ,
2085+ ] ,
2086+ }
2087+
2088+ const { dir, registry } = await getRepo ( graph )
2089+ const cache = fs . mkdtempSync ( `${ getTempDir ( ) } /test-` )
2090+ const arborist = new Arborist ( { path : dir , registry, packumentCache : new Map ( ) , cache } )
2091+ await arborist . reify ( { installStrategy : 'linked' } )
2092+
2093+ // ws-a is declared as a root dependency — should be at root node_modules
2094+ t . ok ( pathExists ( path . join ( dir , 'node_modules' , 'ws-a' ) ) ,
2095+ 'declared workspace ws-a is symlinked at root node_modules' )
2096+ t . ok ( fs . lstatSync ( path . join ( dir , 'node_modules' , 'ws-a' ) ) . isSymbolicLink ( ) ,
2097+ 'ws-a at root is a symlink' )
2098+
2099+ // ws-b is NOT a root dependency — should NOT be at root node_modules
2100+ t . notOk ( pathExists ( path . join ( dir , 'node_modules' , 'ws-b' ) ) ,
2101+ 'undeclared workspace ws-b is NOT at root node_modules' )
2102+
2103+ // ws-c is NOT a root dependency — should NOT be at root node_modules
2104+ t . notOk ( pathExists ( path . join ( dir , 'node_modules' , 'ws-c' ) ) ,
2105+ 'undeclared workspace ws-c is NOT at root node_modules' )
2106+
2107+ // ws-b should be resolvable from ws-a (ws-a depends on ws-b)
2108+ t . ok ( pathExists ( path . join ( dir , 'packages' , 'ws-a' , 'node_modules' , 'ws-b' ) ) ,
2109+ 'ws-b is linked in ws-a/node_modules (declared dep)' )
2110+
2111+ // ws-c has no dependencies and is not depended on — should not be able to access ws-b
2112+ t . notOk ( pathExists ( path . join ( dir , 'packages' , 'ws-c' , 'node_modules' , 'ws-b' ) ) ,
2113+ 'ws-c cannot access ws-b (no dependency declared)' )
2114+ } )
2115+
2116+ tap . test ( 'omit dev dependencies with linked strategy' , async t => {
2117+ const graph = {
2118+ registry : [
2119+ { name : 'which' , version : '1.0.0' , dependencies : { isexe : '^1.0.0' } } ,
2120+ { name : 'isexe' , version : '1.0.0' } ,
2121+ { name : 'eslint' , version : '1.0.0' } ,
2122+ ] ,
2123+ root : {
2124+ name : 'myapp' ,
2125+ version : '1.0.0' ,
2126+ dependencies : { which : '1.0.0' } ,
2127+ devDependencies : { eslint : '1.0.0' } ,
2128+ } ,
2129+ workspaces : [
2130+ {
2131+ name : 'mylib' ,
2132+ version : '1.0.0' ,
2133+ dependencies : { isexe : '1.0.0' } ,
2134+ devDependencies : { eslint : '1.0.0' } ,
2135+ } ,
2136+ ] ,
2137+ }
2138+
2139+ const { dir, registry } = await getRepo ( graph )
2140+ const cache = fs . mkdtempSync ( `${ getTempDir ( ) } /test-` )
2141+ const arborist = new Arborist ( {
2142+ path : dir ,
2143+ registry,
2144+ packumentCache : new Map ( ) ,
2145+ cache,
2146+ omit : [ 'dev' ] ,
2147+ } )
2148+ await arborist . reify ( { installStrategy : 'linked' } )
2149+
2150+ const storeDir = path . join ( dir , 'node_modules' , '.store' )
2151+ const storeEntries = fs . readdirSync ( storeDir )
2152+
2153+ t . ok ( storeEntries . some ( e => e . startsWith ( 'which@' ) ) , 'prod dep which is in store' )
2154+ t . ok ( storeEntries . some ( e => e . startsWith ( 'isexe@' ) ) , 'prod dep isexe is in store' )
2155+ t . notOk ( storeEntries . some ( e => e . startsWith ( 'eslint@' ) ) , 'dev dep eslint is not in store' )
2156+ } )
2157+
20582158/*
20592159 * TO TEST:
20602160 * --------------------------------------
0 commit comments