1const fs = require('fs') 2const util = require('util') 3const readdir = util.promisify(fs.readdir) 4const { resolve } = require('path') 5 6const npa = require('npm-package-arg') 7const pkgJson = require('@npmcli/package-json') 8const semver = require('semver') 9 10const reifyFinish = require('../utils/reify-finish.js') 11 12const ArboristWorkspaceCmd = require('../arborist-cmd.js') 13class Link extends ArboristWorkspaceCmd { 14 static description = 'Symlink a package folder' 15 static name = 'link' 16 static usage = [ 17 '[<package-spec>]', 18 ] 19 20 static params = [ 21 'save', 22 'save-exact', 23 'global', 24 'install-strategy', 25 'legacy-bundling', 26 'global-style', 27 'strict-peer-deps', 28 'package-lock', 29 'omit', 30 'include', 31 'ignore-scripts', 32 'audit', 33 'bin-links', 34 'fund', 35 'dry-run', 36 ...super.params, 37 ] 38 39 static async completion (opts, npm) { 40 const dir = npm.globalDir 41 const files = await readdir(dir) 42 return files.filter(f => !/^[._-]/.test(f)) 43 } 44 45 async exec (args) { 46 if (this.npm.global) { 47 throw Object.assign( 48 new Error( 49 'link should never be --global.\n' + 50 'Please re-run this command with --local' 51 ), 52 { code: 'ELINKGLOBAL' } 53 ) 54 } 55 // install-links is implicitly false when running `npm link` 56 this.npm.config.set('install-links', false) 57 58 // link with no args: symlink the folder to the global location 59 // link with package arg: symlink the global to the local 60 args = args.filter(a => resolve(a) !== this.npm.prefix) 61 return args.length 62 ? this.linkInstall(args) 63 : this.linkPkg() 64 } 65 66 async linkInstall (args) { 67 // load current packages from the global space, 68 // and then add symlinks installs locally 69 const globalTop = resolve(this.npm.globalDir, '..') 70 const Arborist = require('@npmcli/arborist') 71 const globalOpts = { 72 ...this.npm.flatOptions, 73 Arborist, 74 path: globalTop, 75 global: true, 76 prune: false, 77 } 78 const globalArb = new Arborist(globalOpts) 79 80 // get only current top-level packages from the global space 81 const globals = await globalArb.loadActual({ 82 filter: (node, kid) => 83 !node.isRoot || args.some(a => npa(a).name === kid), 84 }) 85 86 // any extra arg that is missing from the current 87 // global space should be reified there first 88 const missing = this.missingArgsFromTree(globals, args) 89 if (missing.length) { 90 await globalArb.reify({ 91 ...globalOpts, 92 add: missing, 93 }) 94 } 95 96 // get a list of module names that should be linked in the local prefix 97 const names = [] 98 for (const a of args) { 99 const arg = npa(a) 100 if (arg.type === 'directory') { 101 const { content } = await pkgJson.normalize(arg.fetchSpec) 102 names.push(content.name) 103 } else { 104 names.push(arg.name) 105 } 106 } 107 108 // npm link should not save=true by default unless you're 109 // using any of --save-dev or other types 110 const save = 111 Boolean( 112 (this.npm.config.find('save') !== 'default' && 113 this.npm.config.get('save')) || 114 this.npm.config.get('save-optional') || 115 this.npm.config.get('save-peer') || 116 this.npm.config.get('save-dev') || 117 this.npm.config.get('save-prod') 118 ) 119 // create a new arborist instance for the local prefix and 120 // reify all the pending names as symlinks there 121 const localArb = new Arborist({ 122 ...this.npm.flatOptions, 123 prune: false, 124 path: this.npm.prefix, 125 save, 126 }) 127 await localArb.reify({ 128 ...this.npm.flatOptions, 129 prune: false, 130 path: this.npm.prefix, 131 add: names.map(l => `file:${resolve(globalTop, 'node_modules', l).replace(/#/g, '%23')}`), 132 save, 133 workspaces: this.workspaceNames, 134 }) 135 136 await reifyFinish(this.npm, localArb) 137 } 138 139 async linkPkg () { 140 const wsp = this.workspacePaths 141 const paths = wsp && wsp.length ? wsp : [this.npm.prefix] 142 const add = paths.map(path => `file:${path.replace(/#/g, '%23')}`) 143 const globalTop = resolve(this.npm.globalDir, '..') 144 const Arborist = require('@npmcli/arborist') 145 const arb = new Arborist({ 146 ...this.npm.flatOptions, 147 Arborist, 148 path: globalTop, 149 global: true, 150 }) 151 await arb.reify({ 152 add, 153 }) 154 await reifyFinish(this.npm, arb) 155 } 156 157 // Returns a list of items that can't be fulfilled by 158 // things found in the current arborist inventory 159 missingArgsFromTree (tree, args) { 160 if (tree.isLink) { 161 return this.missingArgsFromTree(tree.target, args) 162 } 163 164 const foundNodes = [] 165 const missing = args.filter(a => { 166 const arg = npa(a) 167 const nodes = tree.children.values() 168 const argFound = [...nodes].every(node => { 169 // TODO: write tests for unmatching version specs, this is hard to test 170 // atm but should be simple once we have a mocked registry again 171 if (arg.name !== node.name /* istanbul ignore next */ || ( 172 arg.version && 173 /* istanbul ignore next */ 174 !semver.satisfies(node.version, arg.version) 175 )) { 176 foundNodes.push(node) 177 return true 178 } 179 }) 180 return argFound 181 }) 182 183 // remote nodes from the loaded tree in order 184 // to avoid dropping them later when reifying 185 for (const node of foundNodes) { 186 node.parent = null 187 } 188 189 return missing 190 } 191} 192module.exports = Link 193