// The order of the code in this file is relevant, because a lot of things // require('npm.js'), but also we need to use some of those modules. So, // we define and instantiate the singleton ahead of loading any modules // required for its methods. // these are all dependencies used in the ctor const EventEmitter = require('events') const { resolve, dirname } = require('path') const Config = require('@npmcli/config') // Patch the global fs module here at the app level require('graceful-fs').gracefulify(require('fs')) const procLogListener = require('./utils/proc-log-listener.js') const proxyCmds = new Proxy({}, { get: (target, cmd) => { const actual = deref(cmd) if (actual && !Reflect.has(target, actual)) { const Impl = require(`./${actual}.js`) const impl = new Impl(npm) // Our existing npm.commands[x] act like a function with attributes, but // our modules have non-inumerable attributes so we can't just assign // them to an anonymous function like we used to. This acts like that // old way of doing things, until we can make breaking changes to the // npm.commands[x] api target[actual] = new Proxy( (args, cb) => npm[_runCmd](cmd, impl, args, cb), { get: (target, attr, receiver) => { return Reflect.get(impl, attr, receiver) }, }) } return target[actual] }, }) const { definitions, flatten, shorthands } = require('./utils/config/index.js') const { shellouts } = require('./utils/cmd-list.js') const usage = require('./utils/npm-usage.js') let warnedNonDashArg = false const _runCmd = Symbol('_runCmd') const _load = Symbol('_load') const _tmpFolder = Symbol('_tmpFolder') const _title = Symbol('_title') const npm = module.exports = new class extends EventEmitter { constructor () { super() require('./utils/perf.js') this.started = Date.now() this.command = null this.commands = proxyCmds procLogListener() process.emit('time', 'npm') this.version = require('../package.json').version this.config = new Config({ npmPath: dirname(__dirname), definitions, flatten, shorthands, }) this[_title] = process.title this.updateNotification = null } get shelloutCommands () { return shellouts } deref (c) { return deref(c) } // this will only ever be called with cmd set to the canonical command name [_runCmd] (cmd, impl, args, cb) { if (!this.loaded) { throw new Error( 'Call npm.load(cb) before using this command.\n' + 'See the README.md or bin/npm-cli.js for example usage.' ) } process.emit('time', `command:${cmd}`) // since 'test', 'start', 'stop', etc. commands re-enter this function // to call the run-script command, we need to only set it one time. if (!this.command) { process.env.npm_command = cmd this.command = cmd } // Options are prefixed by a hyphen-minus (-, \u2d). // Other dash-type chars look similar but are invalid. if (!warnedNonDashArg) { args.filter(arg => /^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(arg)) .forEach(arg => { warnedNonDashArg = true log.error('arg', 'Argument starts with non-ascii dash, this is probably invalid:', arg) }) } const workspacesEnabled = this.config.get('workspaces') const workspacesFilters = this.config.get('workspace') const filterByWorkspaces = workspacesEnabled || workspacesFilters.length > 0 if (this.config.get('usage')) { this.output(impl.usage) cb() } else if (filterByWorkspaces) { impl.execWorkspaces(args, this.config.get('workspace'), er => { process.emit('timeEnd', `command:${cmd}`) cb(er) }) } else { impl.exec(args, er => { process.emit('timeEnd', `command:${cmd}`) cb(er) }) } } // call with parsed CLI options and a callback when done loading // XXX promisify this and stop taking a callback load (cb) { if (!cb || typeof cb !== 'function') throw new TypeError('must call as: npm.load(callback)') this.once('load', cb) if (this.loaded || this.loadErr) { this.emit('load', this.loadErr) return } if (this.loading) return this.loading = true process.emit('time', 'npm:load') this.log.pause() return this[_load]().catch(er => er).then((er) => { this.loading = false this.loadErr = er if (!er && this.config.get('force')) this.log.warn('using --force', 'Recommended protections disabled.') process.emit('timeEnd', 'npm:load') this.emit('load', er) }) } get loaded () { return this.config.loaded } get title () { return this[_title] } set title (t) { process.title = t this[_title] = t } async [_load] () { process.emit('time', 'npm:load:whichnode') const node = await which(process.argv[0]).catch(er => null) process.emit('timeEnd', 'npm:load:whichnode') if (node && node.toUpperCase() !== process.execPath.toUpperCase()) { log.verbose('node symlink', node) process.execPath = node this.config.execPath = node } process.emit('time', 'npm:load:configload') await this.config.load() process.emit('timeEnd', 'npm:load:configload') this.argv = this.config.parsedArgv.remain // note: this MUST be shorter than the actual argv length, because it // uses the same memory, so node will truncate it if it's too long. // if it's a token revocation, then the argv contains a secret, so // don't show that. (Regrettable historical choice to put it there.) // Any other secrets are configs only, so showing only the positional // args keeps those from being leaked. process.emit('time', 'npm:load:setTitle') const tokrev = deref(this.argv[0]) === 'token' && this.argv[1] === 'revoke' this.title = tokrev ? 'npm token revoke' + (this.argv[2] ? ' ***' : '') : ['npm', ...this.argv].join(' ') process.emit('timeEnd', 'npm:load:setTitle') process.emit('time', 'npm:load:setupLog') this.color = setupLog(this.config) process.emit('timeEnd', 'npm:load:setupLog') process.env.COLOR = this.color ? '1' : '0' process.emit('time', 'npm:load:cleanupLog') cleanUpLogFiles(this.cache, this.config.get('logs-max'), log.warn) process.emit('timeEnd', 'npm:load:cleanupLog') log.resume() process.emit('time', 'npm:load:configScope') const configScope = this.config.get('scope') if (configScope && !/^@/.test(configScope)) this.config.set('scope', `@${configScope}`, this.config.find('scope')) process.emit('timeEnd', 'npm:load:configScope') process.emit('time', 'npm:load:projectScope') this.projectScope = this.config.get('scope') || getProjectScope(this.prefix) process.emit('timeEnd', 'npm:load:projectScope') } get flatOptions () { const { flat } = this.config if (this.command) flat.npmCommand = this.command return flat } get lockfileVersion () { return 2 } get log () { return log } get cache () { return this.config.get('cache') } set cache (r) { this.config.set('cache', r) } get globalPrefix () { return this.config.globalPrefix } set globalPrefix (r) { this.config.globalPrefix = r } get localPrefix () { return this.config.localPrefix } set localPrefix (r) { this.config.localPrefix = r } get globalDir () { return process.platform !== 'win32' ? resolve(this.globalPrefix, 'lib', 'node_modules') : resolve(this.globalPrefix, 'node_modules') } get localDir () { return resolve(this.localPrefix, 'node_modules') } get dir () { return (this.config.get('global')) ? this.globalDir : this.localDir } get globalBin () { const b = this.globalPrefix return process.platform !== 'win32' ? resolve(b, 'bin') : b } get localBin () { return resolve(this.dir, '.bin') } get bin () { return this.config.get('global') ? this.globalBin : this.localBin } get prefix () { return this.config.get('global') ? this.globalPrefix : this.localPrefix } set prefix (r) { const k = this.config.get('global') ? 'globalPrefix' : 'localPrefix' this[k] = r } get usage () { return usage(this) } // XXX add logging to see if we actually use this get tmp () { if (!this[_tmpFolder]) { const rand = require('crypto').randomBytes(4).toString('hex') this[_tmpFolder] = `npm-${process.pid}-${rand}` } return resolve(this.config.get('tmp'), this[_tmpFolder]) } // output to stdout in a progress bar compatible way output (...msg) { this.log.clearProgress() console.log(...msg) this.log.showProgress() } }() // now load everything required by the class methods const log = require('npmlog') const { promisify } = require('util') const which = promisify(require('which')) const deref = require('./utils/deref-command.js') const setupLog = require('./utils/setup-log.js') const cleanUpLogFiles = require('./utils/cleanup-log-files.js') const getProjectScope = require('./utils/get-project-scope.js') if (require.main === module) require('./cli.js')(process)