npm.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. const EventEmitter = require('events')
  2. const { resolve, dirname } = require('path')
  3. const Config = require('@npmcli/config')
  4. const log = require('npmlog')
  5. // Patch the global fs module here at the app level
  6. require('graceful-fs').gracefulify(require('fs'))
  7. // TODO make this only ever load once (or unload) in tests
  8. const procLogListener = require('./utils/proc-log-listener.js')
  9. const proxyCmds = new Proxy({}, {
  10. get: (target, cmd) => {
  11. const actual = deref(cmd)
  12. if (actual && !Reflect.has(target, actual)) {
  13. const Impl = require(`./${actual}.js`)
  14. const impl = new Impl(npm)
  15. // Our existing npm.commands[x] act like a function with attributes, but
  16. // our modules have non-inumerable attributes so we can't just assign
  17. // them to an anonymous function like we used to. This acts like that
  18. // old way of doing things, until we can make breaking changes to the
  19. // npm.commands[x] api
  20. target[actual] = new Proxy(
  21. (args, cb) => npm[_runCmd](actual, impl, args, cb),
  22. {
  23. get: (target, attr, receiver) => {
  24. return Reflect.get(impl, attr, receiver)
  25. },
  26. })
  27. }
  28. return target[actual]
  29. },
  30. })
  31. // Timers in progress
  32. const timers = new Map()
  33. // Finished timers
  34. const timings = {}
  35. const processOnTimeHandler = (name) => {
  36. timers.set(name, Date.now())
  37. }
  38. const processOnTimeEndHandler = (name) => {
  39. if (timers.has(name)) {
  40. const ms = Date.now() - timers.get(name)
  41. log.timing(name, `Completed in ${ms}ms`)
  42. timings[name] = ms
  43. timers.delete(name)
  44. } else
  45. log.silly('timing', "Tried to end timer that doesn't exist:", name)
  46. }
  47. const { definitions, flatten, shorthands } = require('./utils/config/index.js')
  48. const { shellouts } = require('./utils/cmd-list.js')
  49. const usage = require('./utils/npm-usage.js')
  50. const which = require('which')
  51. const deref = require('./utils/deref-command.js')
  52. const setupLog = require('./utils/setup-log.js')
  53. const cleanUpLogFiles = require('./utils/cleanup-log-files.js')
  54. const getProjectScope = require('./utils/get-project-scope.js')
  55. let warnedNonDashArg = false
  56. const _runCmd = Symbol('_runCmd')
  57. const _load = Symbol('_load')
  58. const _tmpFolder = Symbol('_tmpFolder')
  59. const _title = Symbol('_title')
  60. const npm = module.exports = new class extends EventEmitter {
  61. constructor () {
  62. super()
  63. this.started = Date.now()
  64. this.command = null
  65. this.commands = proxyCmds
  66. this.timings = timings
  67. this.timers = timers
  68. this.perfStart()
  69. procLogListener()
  70. process.emit('time', 'npm')
  71. this.version = require('../package.json').version
  72. this.config = new Config({
  73. npmPath: dirname(__dirname),
  74. definitions,
  75. flatten,
  76. shorthands,
  77. })
  78. this[_title] = process.title
  79. this.updateNotification = null
  80. }
  81. perfStart () {
  82. process.on('time', processOnTimeHandler)
  83. process.on('timeEnd', processOnTimeEndHandler)
  84. }
  85. perfStop () {
  86. process.off('time', processOnTimeHandler)
  87. process.off('timeEnd', processOnTimeEndHandler)
  88. }
  89. get shelloutCommands () {
  90. return shellouts
  91. }
  92. deref (c) {
  93. return deref(c)
  94. }
  95. // this will only ever be called with cmd set to the canonical command name
  96. [_runCmd] (cmd, impl, args, cb) {
  97. if (!this.loaded) {
  98. throw new Error(
  99. 'Call npm.load() before using this command.\n' +
  100. 'See lib/cli.js for example usage.'
  101. )
  102. }
  103. process.emit('time', `command:${cmd}`)
  104. // since 'test', 'start', 'stop', etc. commands re-enter this function
  105. // to call the run-script command, we need to only set it one time.
  106. if (!this.command) {
  107. process.env.npm_command = cmd
  108. this.command = cmd
  109. }
  110. // Options are prefixed by a hyphen-minus (-, \u2d).
  111. // Other dash-type chars look similar but are invalid.
  112. if (!warnedNonDashArg) {
  113. args.filter(arg => /^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(arg))
  114. .forEach(arg => {
  115. warnedNonDashArg = true
  116. this.log.error('arg', 'Argument starts with non-ascii dash, this is probably invalid:', arg)
  117. })
  118. }
  119. const workspacesEnabled = this.config.get('workspaces')
  120. const workspacesFilters = this.config.get('workspace')
  121. if (workspacesEnabled === false && workspacesFilters.length > 0)
  122. return cb(new Error('Can not use --no-workspaces and --workspace at the same time'))
  123. const filterByWorkspaces = workspacesEnabled || workspacesFilters.length > 0
  124. // normally this would go in the constructor, but our tests don't
  125. // actually use a real npm object so this.npm.config isn't always
  126. // populated. this is the compromise until we can make that a reality
  127. // and then move this into the constructor.
  128. impl.workspaces = this.config.get('workspaces')
  129. impl.workspacePaths = null
  130. // normally this would be evaluated in base-command#setWorkspaces, see
  131. // above for explanation
  132. impl.includeWorkspaceRoot = this.config.get('include-workspace-root')
  133. if (this.config.get('usage')) {
  134. this.output(impl.usage)
  135. cb()
  136. } else if (filterByWorkspaces) {
  137. if (this.config.get('global'))
  138. return cb(new Error('Workspaces not supported for global packages'))
  139. impl.execWorkspaces(args, this.config.get('workspace'), er => {
  140. process.emit('timeEnd', `command:${cmd}`)
  141. cb(er)
  142. })
  143. } else {
  144. impl.exec(args, er => {
  145. process.emit('timeEnd', `command:${cmd}`)
  146. cb(er)
  147. })
  148. }
  149. }
  150. load (cb) {
  151. if (cb && typeof cb !== 'function')
  152. throw new TypeError('callback must be a function if provided')
  153. if (!this.loadPromise) {
  154. process.emit('time', 'npm:load')
  155. this.log.pause()
  156. this.loadPromise = new Promise((resolve, reject) => {
  157. this[_load]().catch(er => er).then((er) => {
  158. this.loadErr = er
  159. if (!er && this.config.get('force'))
  160. this.log.warn('using --force', 'Recommended protections disabled.')
  161. process.emit('timeEnd', 'npm:load')
  162. if (er)
  163. return reject(er)
  164. resolve()
  165. })
  166. })
  167. }
  168. if (!cb)
  169. return this.loadPromise
  170. // loadPromise is returned here for legacy purposes, old code was allowing
  171. // the mixing of callback and promise here.
  172. return this.loadPromise.then(cb, cb)
  173. }
  174. get loaded () {
  175. return this.config.loaded
  176. }
  177. get title () {
  178. return this[_title]
  179. }
  180. set title (t) {
  181. process.title = t
  182. this[_title] = t
  183. }
  184. async [_load] () {
  185. process.emit('time', 'npm:load:whichnode')
  186. let node
  187. try {
  188. node = which.sync(process.argv[0])
  189. } catch (_) {
  190. // TODO should we throw here?
  191. }
  192. process.emit('timeEnd', 'npm:load:whichnode')
  193. if (node && node.toUpperCase() !== process.execPath.toUpperCase()) {
  194. this.log.verbose('node symlink', node)
  195. process.execPath = node
  196. this.config.execPath = node
  197. }
  198. process.emit('time', 'npm:load:configload')
  199. await this.config.load()
  200. process.emit('timeEnd', 'npm:load:configload')
  201. this.argv = this.config.parsedArgv.remain
  202. // note: this MUST be shorter than the actual argv length, because it
  203. // uses the same memory, so node will truncate it if it's too long.
  204. // if it's a token revocation, then the argv contains a secret, so
  205. // don't show that. (Regrettable historical choice to put it there.)
  206. // Any other secrets are configs only, so showing only the positional
  207. // args keeps those from being leaked.
  208. process.emit('time', 'npm:load:setTitle')
  209. const tokrev = deref(this.argv[0]) === 'token' && this.argv[1] === 'revoke'
  210. this.title = tokrev ? 'npm token revoke' + (this.argv[2] ? ' ***' : '')
  211. : ['npm', ...this.argv].join(' ')
  212. process.emit('timeEnd', 'npm:load:setTitle')
  213. process.emit('time', 'npm:load:setupLog')
  214. setupLog(this.config)
  215. process.emit('timeEnd', 'npm:load:setupLog')
  216. process.env.COLOR = this.color ? '1' : '0'
  217. process.emit('time', 'npm:load:cleanupLog')
  218. cleanUpLogFiles(this.cache, this.config.get('logs-max'), this.log.warn)
  219. process.emit('timeEnd', 'npm:load:cleanupLog')
  220. this.log.resume()
  221. process.emit('time', 'npm:load:configScope')
  222. const configScope = this.config.get('scope')
  223. if (configScope && !/^@/.test(configScope))
  224. this.config.set('scope', `@${configScope}`, this.config.find('scope'))
  225. process.emit('timeEnd', 'npm:load:configScope')
  226. process.emit('time', 'npm:load:projectScope')
  227. this.projectScope = this.config.get('scope') ||
  228. getProjectScope(this.prefix)
  229. process.emit('timeEnd', 'npm:load:projectScope')
  230. }
  231. get flatOptions () {
  232. const { flat } = this.config
  233. if (this.command)
  234. flat.npmCommand = this.command
  235. return flat
  236. }
  237. get color () {
  238. // This is a special derived value that takes into consideration not only
  239. // the config, but whether or not we are operating in a tty.
  240. return this.flatOptions.color
  241. }
  242. get lockfileVersion () {
  243. return 2
  244. }
  245. get log () {
  246. return log
  247. }
  248. get cache () {
  249. return this.config.get('cache')
  250. }
  251. set cache (r) {
  252. this.config.set('cache', r)
  253. }
  254. get globalPrefix () {
  255. return this.config.globalPrefix
  256. }
  257. set globalPrefix (r) {
  258. this.config.globalPrefix = r
  259. }
  260. get localPrefix () {
  261. return this.config.localPrefix
  262. }
  263. set localPrefix (r) {
  264. this.config.localPrefix = r
  265. }
  266. get globalDir () {
  267. return process.platform !== 'win32'
  268. ? resolve(this.globalPrefix, 'lib', 'node_modules')
  269. : resolve(this.globalPrefix, 'node_modules')
  270. }
  271. get localDir () {
  272. return resolve(this.localPrefix, 'node_modules')
  273. }
  274. get dir () {
  275. return (this.config.get('global')) ? this.globalDir : this.localDir
  276. }
  277. get globalBin () {
  278. const b = this.globalPrefix
  279. return process.platform !== 'win32' ? resolve(b, 'bin') : b
  280. }
  281. get localBin () {
  282. return resolve(this.dir, '.bin')
  283. }
  284. get bin () {
  285. return this.config.get('global') ? this.globalBin : this.localBin
  286. }
  287. get prefix () {
  288. return this.config.get('global') ? this.globalPrefix : this.localPrefix
  289. }
  290. set prefix (r) {
  291. const k = this.config.get('global') ? 'globalPrefix' : 'localPrefix'
  292. this[k] = r
  293. }
  294. get usage () {
  295. return usage(this)
  296. }
  297. // XXX add logging to see if we actually use this
  298. get tmp () {
  299. if (!this[_tmpFolder]) {
  300. const rand = require('crypto').randomBytes(4).toString('hex')
  301. this[_tmpFolder] = `npm-${process.pid}-${rand}`
  302. }
  303. return resolve(this.config.get('tmp'), this[_tmpFolder])
  304. }
  305. // output to stdout in a progress bar compatible way
  306. output (...msg) {
  307. this.log.clearProgress()
  308. console.log(...msg)
  309. this.log.showProgress()
  310. }
  311. }()