@@ -4,6 +4,9 @@ const { readdir } = require('fs')
4
4
const { resolve } = require ( 'path' )
5
5
6
6
const Arborist = require ( '@npmcli/arborist' )
7
+ const npa = require ( 'npm-package-arg' )
8
+ const rpj = require ( 'read-package-json-fast' )
9
+ const semver = require ( 'semver' )
7
10
8
11
const npm = require ( './npm.js' )
9
12
const usageUtil = require ( './utils/usage.js' )
@@ -17,7 +20,7 @@ const completion = (opts, cb) => {
17
20
const usage = usageUtil (
18
21
'link' ,
19
22
'npm link (in package dir)' +
20
- '\nnpm link [<@scope>/]<pkg>'
23
+ '\nnpm link [<@scope>/]<pkg>[@<version>] '
21
24
)
22
25
23
26
const cmd = ( args , cb ) => link ( args ) . then ( ( ) => cb ( ) ) . catch ( cb )
@@ -41,29 +44,81 @@ const link = async args => {
41
44
: linkPkg ( )
42
45
}
43
46
47
+ // Returns a list of items that can't be fulfilled by
48
+ // things found in the current arborist inventory
49
+ const missingArgsFromTree = ( tree , args ) => {
50
+ const foundNodes = [ ]
51
+ const missing = args . filter ( a => {
52
+ const arg = npa ( a )
53
+ const nodes = tree . children . values ( )
54
+ const argFound = [ ...nodes ] . every ( node => {
55
+ // TODO: write tests for unmatching version specs, this is hard to test
56
+ // atm but should be simple once we have a mocked registry again
57
+ if ( arg . name !== node . name /* istanbul ignore next */ || (
58
+ arg . version &&
59
+ ! semver . satisfies ( node . version , arg . version )
60
+ ) ) {
61
+ foundNodes . push ( node )
62
+ return true
63
+ }
64
+ } )
65
+ return argFound
66
+ } )
67
+
68
+ // remote nodes from the loaded tree in order
69
+ // to avoid dropping them later when reifying
70
+ for ( const node of foundNodes ) {
71
+ node . parent = null
72
+ }
73
+
74
+ return missing
75
+ }
76
+
44
77
const linkInstall = async args => {
45
78
// load current packages from the global space,
46
79
// and then add symlinks installs locally
47
80
const globalTop = resolve ( npm . globalDir , '..' )
48
- const globalArb = new Arborist ( {
81
+ const globalOpts = {
49
82
...npm . flatOptions ,
50
83
path : globalTop ,
51
- global : true
84
+ global : true ,
85
+ prune : false
86
+ }
87
+ const globalArb = new Arborist ( globalOpts )
88
+
89
+ // get only current top-level packages from the global space
90
+ const globals = await globalArb . loadActual ( {
91
+ filter : ( node , kid ) =>
92
+ ! node . isRoot || args . some ( a => npa ( a ) . name === kid )
52
93
} )
53
94
54
- const globals = await globalArb . loadActual ( )
95
+ // any extra arg that is missing from the current
96
+ // global space should be reified there first
97
+ const missing = missingArgsFromTree ( globals , args )
98
+ await globalArb . reify ( {
99
+ ...globalOpts ,
100
+ add : missing
101
+ } )
55
102
56
- const links = [
57
- ...globals . children . values ( )
58
- ]
59
- . filter ( i => args . some ( j => j === i . name ) )
103
+ // get a list of module names that should be linked in the local prefix
104
+ const names = [ ]
105
+ for ( const a of args ) {
106
+ const arg = npa ( a )
107
+ names . push (
108
+ arg . type === 'directory'
109
+ ? ( await rpj ( resolve ( arg . fetchSpec , 'package.json' ) ) ) . name
110
+ : arg . name
111
+ )
112
+ }
60
113
114
+ // create a new arborist instance for the local prefix and
115
+ // reify all the pending names as symlinks there
61
116
const localArb = new Arborist ( {
62
117
...npm . flatOptions ,
63
118
path : npm . prefix
64
119
} )
65
120
await localArb . reify ( {
66
- add : links . map ( l => `file:${ resolve ( globalTop , 'node_modules' , l . path ) } ` )
121
+ add : names . map ( l => `file:${ resolve ( globalTop , 'node_modules' , l ) } ` )
67
122
} )
68
123
69
124
reifyOutput ( localArb )
0 commit comments