link.js 5.2 KB

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