From 00fc313f6bf73814f655b95666effba7c39b1ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Tue, 20 Jun 2017 23:28:38 +0200 Subject: [PATCH] fix(perf): remove bluebird and defer some requires for SPEED --- child.js | 33 ++++++++--------- get-prefix.js | 58 +++++++++++++++++++----------- index.js | 91 ++++++++++++++++++++++++++++------------------- package-lock.json | 3 +- package.json | 1 - parse-args.js | 39 +++++++++++--------- 6 files changed, 134 insertions(+), 91 deletions(-) diff --git a/child.js b/child.js index 93efda9..159ee07 100644 --- a/child.js +++ b/child.js @@ -1,10 +1,7 @@ 'use strict' -const BB = require('bluebird') - const cp = require('child_process') const path = require('path') -const Y = require('./y.js') module.exports.runCommand = runCommand function runCommand (command, opts) { @@ -13,31 +10,35 @@ function runCommand (command, opts) { return spawn(cmd, copts, { shell: opts.shell || !!opts.call, stdio: opts.stdio || 'inherit' - }).catch({code: 'ENOENT'}, () => { - const err = new Error( - Y`npx: command not found: ${path.basename(cmd)}` - ) - err.exitCode = 127 + }).catch(err => { + if (err.code === 'ENOENT') { + err = new Error( + require('./y.js')`npx: command not found: ${path.basename(cmd)}` + ) + err.exitCode = 127 + } throw err }) } module.exports.spawn = spawn function spawn (cmd, args, opts) { - return BB.fromNode(cb => { + return new Promise((resolve, reject) => { const child = cp.spawn(cmd, args, opts) let stdout = '' let stderr = '' child.stdout && child.stdout.on('data', d => { stdout += d }) child.stderr && child.stderr.on('data', d => { stderr += d }) - child.on('error', cb) + child.on('error', reject) child.on('close', code => { if (code) { - const err = new Error(Y`Command failed: ${cmd} ${args.join(' ')}`) + const err = new Error( + require('./y.js')`Command failed: ${cmd} ${args.join(' ')}` + ) err.exitCode = code - cb(err) - } else if (child.stdout || child.stderr) { - cb(null, {code, stdout, stderr}) + reject(err) + } else { + resolve({code, stdout, stderr}) } }) }) @@ -46,10 +47,10 @@ function spawn (cmd, args, opts) { module.exports.exec = exec function exec (cmd, args, opts) { opts = opts || {} - return BB.fromNode(cb => { + return new Promise((resolve, reject) => { cp.exec(`${escapeArg(cmd, true)} ${ args.join(' ') - }`, opts, cb) + }`, opts, (err, stdout) => err ? reject(err) : resolve(stdout)) }) } diff --git a/get-prefix.js b/get-prefix.js index 97b4cc0..57fb704 100644 --- a/get-prefix.js +++ b/get-prefix.js @@ -1,8 +1,6 @@ 'use strict' -const BB = require('bluebird') - -const statAsync = BB.promisify(require('fs').stat) +const statAsync = promisify(require('fs').stat) const path = require('path') module.exports = getPrefix @@ -13,38 +11,43 @@ function getPrefix (current, root) { root = path.dirname(root) } if (original !== root) { - return BB.resolve(root) + return Promise.resolve(root) } else { return getPrefix(root, root) } } if (isRootPath(current)) { - return BB.resolve(root) + return Promise.resolve(root) } else { - return BB.join( + return Promise.all([ fileExists(path.join(current, 'package.json')), - fileExists(path.join(current, 'node_modules')), - (hasPkg, hasModules) => { - if (hasPkg || hasModules) { + fileExists(path.join(current, 'node_modules')) + ]).then(args => { + const hasPkg = args[0] + const hasModules = args[1] + if (hasPkg || hasModules) { + return current + } else { + const parent = path.dirname(current) + if (parent === current) { + // This _should_ only happen for root paths, but we already + // guard against that above. I think this is pretty much dead + // code, but npm was doing it, and I'm paranoid :s return current } else { - const parent = path.dirname(current) - if (parent === current) { - // This _should_ only happen for root paths, but we already - // guard against that above. I think this is pretty much dead - // code, but npm was doing it, and I'm paranoid :s - return current - } else { - return getPrefix(parent, root) - } + return getPrefix(parent, root) } } - ) + }) } } function fileExists (f) { - return statAsync(f).catch({code: 'ENOENT'}, () => false) + return statAsync(f).catch(err => { + if (err.code !== 'ENOENT') { + throw err + } + }) } function isRootPath (p) { @@ -52,3 +55,18 @@ function isRootPath (p) { ? p.match(/^[a-z]+:[/\\]?$/i) : p === '/' } + +function promisify (f) { + const util = require('util') + if (util.promisify) { + return util.promisify(f) + } else { + return function () { + return new Promise((resolve, reject) => { + f.apply(this, [].slice.call(arguments).concat((err, val) => { + err ? reject(err) : resolve(val) + })) + }) + } + } +} diff --git a/index.js b/index.js index 819f567..756382c 100755 --- a/index.js +++ b/index.js @@ -1,17 +1,12 @@ #!/usr/bin/env node 'use strict' -const BB = require('bluebird') - const child = require('./child') -const dotenv = require('dotenv') -const getPrefix = require('./get-prefix.js') const parseArgs = require('./parse-args.js') const path = require('path') const pkg = require('./package.json') const updateNotifier = require('update-notifier') -const which = BB.promisify(require('which')) -const Y = require('./y.js') +const which = promisify(require('which')) const PATH_SEP = process.platform === 'win32' ? ';' : ':' @@ -34,7 +29,7 @@ function main (argv) { } if (!argv.call && (!argv.command || !argv.package)) { - console.error(Y`\nERROR: You must supply a command.\n`) + console.error(Y()`\nERROR: You must supply a command.\n`) parseArgs.showHelp() process.exitCode = 1 return @@ -49,7 +44,7 @@ function main (argv) { // Local project paths take priority. Go ahead and prepend it. process.env.PATH = `${local}${PATH_SEP}${process.env.PATH}` } - return BB.join( + return Promise.all([ // Figuring out if a command exists, early on, lets us maybe // short-circuit a few things later. This bit here primarily benefits // calls like `$ npx foo`, where we might just be trying to invoke @@ -59,26 +54,27 @@ function main (argv) { // we take a bit of extra time to pick up npm's full lifecycle script // environment (so you can use `$npm_package_xxxxx` and company). // Without that flag, we just use the current env. - argv.call && getEnv(argv), - (existing, newEnv) => { - if (newEnv) { - // NOTE - we don't need to manipulate PATH further here, because - // npm has already done so. And even added the node-gyp path! - process.env = newEnv - } - if ((!existing && !argv.call) || argv.packageRequested) { - // Some npm packages need to be installed. Let's install them! - return ensurePackages(argv.package, argv).then(results => { - console.error(Y`npx: installed ${ - results.added.length + results.updated.length - } in ${(Date.now() - startTime) / 1000}s`) - }).then(() => existing) - } else { - // We can skip any extra installation, 'cause everything exists. - return existing - } + argv.call && getEnv(argv) + ]).then(args => { + const existing = args[0] + const newEnv = args[1] + if (newEnv) { + // NOTE - we don't need to manipulate PATH further here, because + // npm has already done so. And even added the node-gyp path! + process.env = newEnv } - ).then(existing => { + if ((!existing && !argv.call) || argv.packageRequested) { + // Some npm packages need to be installed. Let's install them! + return ensurePackages(argv.package, argv).then(results => { + console.error(Y()`npx: installed ${ + results.added.length + results.updated.length + } in ${(Date.now() - startTime) / 1000}s`) + }).then(() => existing) + } else { + // We can skip any extra installation, 'cause everything exists. + return existing + } + }).then(existing => { return child.runCommand(existing, argv).catch(err => { if (err.isOperational && err.exitCode) { // At this point, we want to treat errors from the child as if @@ -98,25 +94,25 @@ function main (argv) { module.exports._localBinPath = localBinPath function localBinPath (cwd) { - return getPrefix(cwd).then(prefix => { + return require('./get-prefix.js')(cwd).then(prefix => { return path.join(prefix, 'node_modules', '.bin') }) } module.exports._getEnv = getEnv function getEnv (opts) { - return child.exec(opts.npm, ['run', 'env']).then(dotenv.parse) + return child.exec(opts.npm, ['run', 'env']).then(require('dotenv').parse) } function ensurePackages (specs, opts) { return ( - opts.cache ? BB.resolve(opts.cache) : getNpmCache(opts) + opts.cache ? Promise.resolve(opts.cache) : getNpmCache(opts) ).then(cache => { const prefix = path.join(cache, '_npx') const bins = process.platform === 'win32' ? prefix : path.join(prefix, 'bin') - return BB.promisify(require('rimraf'))(bins).then(() => { + return promisify(require('rimraf'))(bins).then(() => { return installPackages(specs, prefix, opts) }).then(info => { // This will make temp bins _higher priority_ than even local bins. @@ -131,11 +127,15 @@ function ensurePackages (specs, opts) { module.exports._getExistingPath = getExistingPath function getExistingPath (command, opts) { if (opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting) { - return BB.resolve(false) + return Promise.resolve(false) } else { - return which(command).catch({code: 'ENOENT'}, err => { - if (!opts.install) { - err.exitCode = 127 + return which(command).catch(err => { + if (err.code === 'ENOENT') { + if (!opts.install) { + err.exitCode = 127 + throw err + } + } else { throw err } }) @@ -172,9 +172,28 @@ function installPackages (specs, prefix, opts) { stdio: [0, 'pipe', 2] }).then(deets => deets.stdout ? JSON.parse(deets.stdout) : null, err => { if (err.exitCode) { - err.message = Y`Install for ${specs} failed with code ${err.exitCode}` + err.message = Y()`Install for ${specs} failed with code ${err.exitCode}` } throw err }) }) } + +function Y () { + return require('./y.js') +} + +function promisify (f) { + const util = require('util') + if (util.promisify) { + return util.promisify(f) + } else { + return function () { + return new Promise((resolve, reject) => { + f.apply(this, [].slice.call(arguments).concat((err, val) => { + err ? reject(err) : resolve(val) + })) + }) + } + } +} diff --git a/package-lock.json b/package-lock.json index 256993a..8b723bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,7 +178,8 @@ "bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", + "dev": true }, "boom": { "version": "2.10.1", diff --git a/package.json b/package.json index 9e4e3e4..4e62a1a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ }, "license": "CC0-1.0", "dependencies": { - "bluebird": "^3.5.0", "dotenv": "^4.0.0", "gauge": "^2.7.4", "npm": "^5.0.3", diff --git a/parse-args.js b/parse-args.js index bcd6db6..9cc4aeb 100644 --- a/parse-args.js +++ b/parse-args.js @@ -3,64 +3,63 @@ const npa = require('npm-package-arg') const path = require('path') const yargs = require('yargs') -const Y = require('./y.js') const usage = ` -$0 [options] [@version] [command-arg]... +$0 [${Y()`options`}] <${Y()`command`}>[@${Y()`version`}] [${Y()`command-arg`}]... -$0 [options] [-p|--package ]... [command-arg]... +$0 [${Y()`options`}] [-p|--package <${Y()`package`}>]... <${Y()`command`}> [${Y()`command-arg`}]... -$0 [options] -c '' +$0 [${Y()`options`}] -c '<${Y()`command-string`}>' -$0 --shell-auto-fallback [shell] +$0 --shell-auto-fallback [${Y()`shell`}] ` module.exports = parseArgs function parseArgs (argv) { argv = argv || process.argv const parser = yargs - .usage(Y`Execute binaries from npm packages.\n${usage}`) + .usage(Y()`Execute binaries from npm packages.\n${usage}`) .option('package', { alias: 'p', type: 'string', - describe: Y`Package to be installed.` + describe: Y()`Package to be installed.` }) .option('cache', { type: 'string', - describe: Y`Location of the npm cache.` + describe: Y()`Location of the npm cache.` }) .option('install', { type: 'boolean', - describe: Y`Skip installation if a package is missing.`, + describe: Y()`Skip installation if a package is missing.`, default: true }) .option('userconfig', { type: 'string', - describe: Y`Path to user npmrc.` + describe: Y()`Path to user npmrc.` }) .option('call', { alias: 'c', type: 'string', - describe: Y`Execute string as if inside \`npm run-script\`.` + describe: Y()`Execute string as if inside \`npm run-script\`.` }) .option('shell', { alias: 's', type: 'string', - describe: Y`Shell to execute the command with, if any.`, + describe: Y()`Shell to execute the command with, if any.`, default: false }) .option('shell-auto-fallback', { choices: ['', 'bash', 'fish', 'zsh'], - describe: Y`Generate shell code to use npx as the "command not found" fallback.`, + describe: Y()`Generate shell code to use npx as the "command not found" fallback.`, requireArg: false, type: 'string' }) .option('ignore-existing', { - describe: Y`Ignores existing binaries in $PATH, or in the local project. This forces npx to do a temporary install and use the latest version.`, + describe: Y()`Ignores existing binaries in $PATH, or in the local project. This forces npx to do a temporary install and use the latest version.`, type: 'boolean' }) .option('npm', { - describe: Y`npm binary to use for internal operations.`, + describe: Y()`npm binary to use for internal operations.`, type: 'string', default: path.resolve(__dirname, 'node_modules', '.bin', 'npm') }) @@ -68,7 +67,7 @@ function parseArgs (argv) { .alias('version', 'v') .help() .alias('help', 'h') - .epilogue(Y`For the full documentation, see the manual page for npx(1).`) + .epilogue(Y()`For the full documentation, see the manual page for npx(1).`) const opts = parser.getOptions() const bools = new Set(opts.boolean) @@ -163,6 +162,12 @@ function guessCmdName (spec) { return path.basename(spec.fetchSpec, ext).replace(/-\d+\.\d+\.\d+(?:-[a-z0-9.\-+]+)?$/i, '') } - console.error(Y`Unable to guess a binary name from ${spec.raw}. Please use --package.`) + console.error(Y()`Unable to guess a binary name from ${spec.raw}. Please use --package.`) return null } + +var _y +function Y () { + if (!_y) { _y = require('./y.js') } + return _y +}