link.js 5.6 KB

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