outdated.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. const os = require('os')
  2. const path = require('path')
  3. const pacote = require('pacote')
  4. const table = require('text-table')
  5. const color = require('chalk')
  6. const styles = require('ansistyles')
  7. const npa = require('npm-package-arg')
  8. const pickManifest = require('npm-pick-manifest')
  9. const localeCompare = require('@isaacs/string-locale-compare')('en')
  10. const Arborist = require('@npmcli/arborist')
  11. const ansiTrim = require('./utils/ansi-trim.js')
  12. const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
  13. class Outdated extends ArboristWorkspaceCmd {
  14. /* istanbul ignore next - see test/lib/load-all-commands.js */
  15. static get description () {
  16. return 'Check for outdated packages'
  17. }
  18. /* istanbul ignore next - see test/lib/load-all-commands.js */
  19. static get name () {
  20. return 'outdated'
  21. }
  22. /* istanbul ignore next - see test/lib/load-all-commands.js */
  23. static get usage () {
  24. return ['[[<@scope>/]<pkg> ...]']
  25. }
  26. /* istanbul ignore next - see test/lib/load-all-commands.js */
  27. static get params () {
  28. return [
  29. 'all',
  30. 'json',
  31. 'long',
  32. 'parseable',
  33. 'global',
  34. 'workspace',
  35. ]
  36. }
  37. exec (args, cb) {
  38. this.outdated(args).then(() => cb()).catch(cb)
  39. }
  40. async outdated (args) {
  41. const global = path.resolve(this.npm.globalDir, '..')
  42. const where = this.npm.config.get('global')
  43. ? global
  44. : this.npm.prefix
  45. const arb = new Arborist({
  46. ...this.npm.flatOptions,
  47. path: where,
  48. })
  49. this.edges = new Set()
  50. this.list = []
  51. this.tree = await arb.loadActual()
  52. if (this.workspaceNames && this.workspaceNames.length) {
  53. this.filterSet =
  54. arb.workspaceDependencySet(
  55. this.tree,
  56. this.workspaceNames,
  57. this.npm.flatOptions.includeWorkspaceRoot
  58. )
  59. } else if (!this.npm.flatOptions.workspacesEnabled) {
  60. this.filterSet =
  61. arb.excludeWorkspacesDependencySet(this.tree)
  62. }
  63. if (args.length !== 0) {
  64. // specific deps
  65. for (let i = 0; i < args.length; i++) {
  66. const nodes = this.tree.inventory.query('name', args[i])
  67. this.getEdges(nodes, 'edgesIn')
  68. }
  69. } else {
  70. if (this.npm.config.get('all')) {
  71. // all deps in tree
  72. const nodes = this.tree.inventory.values()
  73. this.getEdges(nodes, 'edgesOut')
  74. }
  75. // top-level deps
  76. this.getEdges()
  77. }
  78. await Promise.all(Array.from(this.edges).map((edge) => {
  79. return this.getOutdatedInfo(edge)
  80. }))
  81. // sorts list alphabetically
  82. const outdated = this.list.sort((a, b) => localeCompare(a.name, b.name))
  83. if (outdated.length > 0)
  84. process.exitCode = 1
  85. // return if no outdated packages
  86. if (outdated.length === 0 && !this.npm.config.get('json'))
  87. return
  88. // display results
  89. if (this.npm.config.get('json'))
  90. this.npm.output(this.makeJSON(outdated))
  91. else if (this.npm.config.get('parseable'))
  92. this.npm.output(this.makeParseable(outdated))
  93. else {
  94. const outList = outdated.map(x => this.makePretty(x))
  95. const outHead = ['Package',
  96. 'Current',
  97. 'Wanted',
  98. 'Latest',
  99. 'Location',
  100. 'Depended by',
  101. ]
  102. if (this.npm.config.get('long'))
  103. outHead.push('Package Type', 'Homepage')
  104. const outTable = [outHead].concat(outList)
  105. if (this.npm.color)
  106. outTable[0] = outTable[0].map(heading => styles.underline(heading))
  107. const tableOpts = {
  108. align: ['l', 'r', 'r', 'r', 'l'],
  109. stringLength: s => ansiTrim(s).length,
  110. }
  111. this.npm.output(table(outTable, tableOpts))
  112. }
  113. }
  114. getEdges (nodes, type) {
  115. // when no nodes are provided then it should only read direct deps
  116. // from the root node and its workspaces direct dependencies
  117. if (!nodes) {
  118. this.getEdgesOut(this.tree)
  119. this.getWorkspacesEdges()
  120. return
  121. }
  122. for (const node of nodes) {
  123. type === 'edgesOut'
  124. ? this.getEdgesOut(node)
  125. : this.getEdgesIn(node)
  126. }
  127. }
  128. getEdgesIn (node) {
  129. for (const edge of node.edgesIn)
  130. this.trackEdge(edge)
  131. }
  132. getEdgesOut (node) {
  133. // TODO: normalize usage of edges and avoid looping through nodes here
  134. if (this.npm.config.get('global')) {
  135. for (const child of node.children.values())
  136. this.trackEdge(child)
  137. } else {
  138. for (const edge of node.edgesOut.values())
  139. this.trackEdge(edge)
  140. }
  141. }
  142. trackEdge (edge) {
  143. const filteredOut =
  144. edge.from
  145. && this.filterSet
  146. && this.filterSet.size > 0
  147. && !this.filterSet.has(edge.from.target)
  148. if (filteredOut)
  149. return
  150. this.edges.add(edge)
  151. }
  152. getWorkspacesEdges (node) {
  153. if (this.npm.config.get('global'))
  154. return
  155. for (const edge of this.tree.edgesOut.values()) {
  156. const workspace = edge
  157. && edge.to
  158. && edge.to.target
  159. && edge.to.target.isWorkspace
  160. if (workspace)
  161. this.getEdgesOut(edge.to.target)
  162. }
  163. }
  164. async getPackument (spec) {
  165. const packument = await pacote.packument(spec, {
  166. ...this.npm.flatOptions,
  167. fullMetadata: this.npm.config.get('long'),
  168. preferOnline: true,
  169. })
  170. return packument
  171. }
  172. async getOutdatedInfo (edge) {
  173. const spec = npa(edge.name)
  174. const node = edge.to || edge
  175. const { path, location } = node
  176. const { version: current } = node.package || {}
  177. const type = edge.optional ? 'optionalDependencies'
  178. : edge.peer ? 'peerDependencies'
  179. : edge.dev ? 'devDependencies'
  180. : 'dependencies'
  181. for (const omitType of this.npm.config.get('omit')) {
  182. if (node[omitType])
  183. return
  184. }
  185. // deps different from prod not currently
  186. // on disk are not included in the output
  187. if (edge.error === 'MISSING' && type !== 'dependencies')
  188. return
  189. try {
  190. const packument = await this.getPackument(spec)
  191. const expected = edge.spec
  192. // if it's not a range, version, or tag, skip it
  193. try {
  194. if (!npa(`${edge.name}@${edge.spec}`).registry)
  195. return null
  196. } catch (err) {
  197. return null
  198. }
  199. const wanted = pickManifest(packument, expected, this.npm.flatOptions)
  200. const latest = pickManifest(packument, '*', this.npm.flatOptions)
  201. if (
  202. !current ||
  203. current !== wanted.version ||
  204. wanted.version !== latest.version
  205. ) {
  206. const dependent = edge.from ?
  207. this.maybeWorkspaceName(edge.from)
  208. : 'global'
  209. this.list.push({
  210. name: edge.name,
  211. path,
  212. type,
  213. current,
  214. location,
  215. wanted: wanted.version,
  216. latest: latest.version,
  217. dependent,
  218. homepage: packument.homepage,
  219. })
  220. }
  221. } catch (err) {
  222. // silently catch and ignore ETARGET, E403 &
  223. // E404 errors, deps are just skipped
  224. if (!(
  225. err.code === 'ETARGET' ||
  226. err.code === 'E403' ||
  227. err.code === 'E404')
  228. )
  229. throw err
  230. }
  231. }
  232. maybeWorkspaceName (node) {
  233. if (!node.isWorkspace)
  234. return node.name
  235. const humanOutput =
  236. !this.npm.config.get('json') && !this.npm.config.get('parseable')
  237. const workspaceName =
  238. humanOutput
  239. ? node.pkgid
  240. : node.name
  241. return this.npm.color && humanOutput
  242. ? color.green(workspaceName)
  243. : workspaceName
  244. }
  245. // formatting functions
  246. makePretty (dep) {
  247. const {
  248. current = 'MISSING',
  249. location = '-',
  250. homepage = '',
  251. name,
  252. wanted,
  253. latest,
  254. type,
  255. dependent,
  256. } = dep
  257. const columns = [name, current, wanted, latest, location, dependent]
  258. if (this.npm.config.get('long')) {
  259. columns[6] = type
  260. columns[7] = homepage
  261. }
  262. if (this.npm.color) {
  263. columns[0] = color[current === wanted ? 'yellow' : 'red'](columns[0]) // current
  264. columns[2] = color.green(columns[2]) // wanted
  265. columns[3] = color.magenta(columns[3]) // latest
  266. }
  267. return columns
  268. }
  269. // --parseable creates output like this:
  270. // <fullpath>:<name@wanted>:<name@installed>:<name@latest>:<dependedby>
  271. makeParseable (list) {
  272. return list.map(dep => {
  273. const {
  274. name,
  275. current,
  276. wanted,
  277. latest,
  278. path,
  279. dependent,
  280. type,
  281. homepage,
  282. } = dep
  283. const out = [
  284. path,
  285. name + '@' + wanted,
  286. current ? (name + '@' + current) : 'MISSING',
  287. name + '@' + latest,
  288. dependent,
  289. ]
  290. if (this.npm.config.get('long'))
  291. out.push(type, homepage)
  292. return out.join(':')
  293. }).join(os.EOL)
  294. }
  295. makeJSON (list) {
  296. const out = {}
  297. list.forEach(dep => {
  298. const {
  299. name,
  300. current,
  301. wanted,
  302. latest,
  303. path,
  304. type,
  305. dependent,
  306. homepage,
  307. } = dep
  308. out[name] = {
  309. current,
  310. wanted,
  311. latest,
  312. dependent,
  313. location: path,
  314. }
  315. if (this.npm.config.get('long')) {
  316. out[name].type = type
  317. out[name].homepage = homepage
  318. }
  319. })
  320. return JSON.stringify(out, null, 2)
  321. }
  322. }
  323. module.exports = Outdated