@@ -1789,6 +1789,57 @@ tap.test('file: dependency with linked strategy', async t => {
17891789 t . ok ( setupRequire ( dir ) ( 'project2' ) , 'project2 can be required from root' )
17901790} )
17911791
1792+ tap . test ( 'npm link (external file: dep) with linked strategy' , async t => {
1793+ // Regression test: `npm link` creates a file: dependency pointing outside the project root.
1794+ // The linked strategy should symlink it directly instead of trying to extract it into .store/.
1795+ const graph = {
1796+ registry : [
1797+ { name : 'abbrev' , version : '2.0.0' } ,
1798+ ] ,
1799+ root : {
1800+ name : 'my-app' ,
1801+ version : '1.0.0' ,
1802+ dependencies : { abbrev : '2.0.0' } ,
1803+ } ,
1804+ }
1805+
1806+ const { dir, registry } = await getRepo ( graph )
1807+
1808+ // Create an external package OUTSIDE the project root (simulates npm link target)
1809+ const externalPkgDir = path . join ( path . dirname ( dir ) , 'external-pkg' )
1810+ fs . mkdirSync ( externalPkgDir , { recursive : true } )
1811+ fs . writeFileSync ( path . join ( externalPkgDir , 'package.json' ) , JSON . stringify ( {
1812+ name : 'external-pkg' ,
1813+ version : '1.0.0' ,
1814+ } ) )
1815+ fs . writeFileSync ( path . join ( externalPkgDir , 'index.js' ) , "module.exports = 'external'" )
1816+
1817+ const cache = fs . mkdtempSync ( `${ getTempDir ( ) } /test-` )
1818+
1819+ // First install without the linked package
1820+ const arb1 = new Arborist ( { path : dir , registry, packumentCache : new Map ( ) , cache } )
1821+ await arb1 . reify ( { installStrategy : 'linked' } )
1822+
1823+ // Now simulate `npm link external-pkg` by adding a file: dep and reifying
1824+ const arb2 = new Arborist ( { path : dir , registry, packumentCache : new Map ( ) , cache } )
1825+ await arb2 . reify ( { installStrategy : 'linked' , add : [ `file:${ externalPkgDir } ` ] } )
1826+
1827+ // The external package should be symlinked in node_modules
1828+ const linkPath = path . join ( dir , 'node_modules' , 'external-pkg' )
1829+ const stat = fs . lstatSync ( linkPath )
1830+ t . ok ( stat . isSymbolicLink ( ) , 'external-pkg is a symlink in node_modules' )
1831+
1832+ // The symlink should resolve to the actual external directory
1833+ const realpath = fs . realpathSync ( linkPath )
1834+ t . equal ( realpath , externalPkgDir , 'symlink points to the correct external directory' )
1835+
1836+ // The existing store packages should still be intact
1837+ const storePath = path . join ( dir , 'node_modules' , '.store' )
1838+ const storeEntries = fs . readdirSync ( storePath )
1839+ t . ok ( storeEntries . some ( e => e . startsWith ( 'abbrev@' ) ) , 'abbrev is still in the store' )
1840+ t . notOk ( storeEntries . some ( e => e . startsWith ( 'external-pkg@' ) ) , 'external-pkg is NOT in the store' )
1841+ } )
1842+
17921843tap . test ( 'subsequent linked install is a no-op' , async t => {
17931844 const graph = {
17941845 registry : [
0 commit comments