Skip to content
This repository has been archived by the owner on May 10, 2021. It is now read-only.

Commit

Permalink
feat(opts): support full range of relevant CLI opts (#19)
Browse files Browse the repository at this point in the history
Fixes: #17
  • Loading branch information
zkat authored Oct 4, 2017
1 parent 9682a3c commit 6f2bd51
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 38 deletions.
2 changes: 1 addition & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function cliMain () {
details.runTime / 1000
}s.`)
}, err => {
console.error(`Error!\n${err.message}`)
console.error(`Error!\n${err.message}\n${err.stack}`)
})
}

Expand Down
17 changes: 9 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const readFileAsync = BB.promisify(fs.readFile)

class Installer {
constructor (opts) {
// Config
this.opts = opts

// Stats
Expand Down Expand Up @@ -47,6 +46,9 @@ class Installer {
}
)
}).then(() => {
return config(this.prefix, process.argv, this.pkg)
}).then(conf => {
this.config = conf
return BB.join(
this.checkLock(),
rimraf(path.join(this.prefix, 'node_modules'))
Expand Down Expand Up @@ -96,13 +98,14 @@ class Installer {
return BB.map(Object.keys(deps || {}), name => {
const child = deps[name]
const childPath = path.join(modPath, name)
return extract.child(name, child, childPath).then(() => {
return extract.child(name, child, childPath, this.config).then(() => {
return readJson(childPath, 'package.json')
}).tap(pkg => {
return this.runScript('preinstall', pkg, childPath)
}).then(pkg => {
return this.extractDeps(path.join(childPath, 'node_modules'), child.dependencies)
.then(dependencies => {
return this.extractDeps(
path.join(childPath, 'node_modules'), child.dependencies
).then(dependencies => {
return {
name,
package: pkg,
Expand All @@ -124,12 +127,10 @@ class Installer {
}

runScript (stage, pkg, pkgPath) {
if (!this.opts.ignoreScripts && pkg.scripts && pkg.scripts[stage]) {
if (!this.config.lifecycleOpts.ignoreScripts && pkg.scripts && pkg.scripts[stage]) {
// TODO(mikesherov): remove pkg._id when npm-lifecycle no longer relies on it
pkg._id = pkg.name + '@' + pkg.version
return config(this.prefix).then(config => {
return lifecycle(pkg, stage, pkgPath, config)
})
return lifecycle(pkg, stage, pkgPath, this.config.lifecycleOpts)
}
return BB.resolve()
}
Expand Down
50 changes: 33 additions & 17 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@

const BB = require('bluebird')
const log = require('npmlog')
const cipmPkg = require('../package.json')
const spawn = require('child_process').spawn

module.exports = getConfig
module.exports._resetConfig = _resetConfig

let _config

function readConfig () {
// Right now, we're leaning on npm itself to give us a config and do all the
// usual npm config logic. In the future, we'll have a standalone package that
// does compatible config loading -- but this version just runs a child
// process.
function readConfig (argv) {
return new BB((resolve, reject) => {
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'
const child = spawn(npmBin, ['config', 'ls', '--json'], {
const child = spawn(npmBin, [
'config', 'ls', '--json', '-l'
// We add argv here to get npm to parse those options for us :D
].concat(argv || []), {
env: process.env,
cwd: process.cwd(),
stdio: [0, 'pipe', 2]
Expand Down Expand Up @@ -47,25 +55,33 @@ function _resetConfig () {
_config = undefined
}

function getConfig (dir) {
function getConfig (dir, argv, rootPkg) {
if (_config) return BB.resolve(_config)
return readConfig().then(config => {
return readConfig(argv).then(config => {
log.level = config['loglevel']
config['user-agent'] = config['user-agent'] || `${cipmPkg.name}@${cipmPkg.version} ${process.release.name}@${process.version.replace(/^v/, '')} ${process.platform} ${process.arch}`
_config = {
config,
dir,
failOk: false,
force: config.force,
group: config.group,
ignorePrepublish: config['ignore-prepublish'],
ignoreScripts: config['ignore-scripts'],
prefix: dir,
log,
production: config.production,
scriptShell: config['script-shell'],
scriptsPrependNodePath: config['scripts-prepend-node-path'],
unsafePerm: config['unsafe-perm'],
user: config['user']
rootPkg,
config,
// These are opts for `npm-lifecycle`
lifecycleOpts: {
config,
scriptShell: config['script-shell'],
force: config.force,
user: config.user,
group: config.group,
ignoreScripts: config['ignore-scripts'],
ignorePrepublish: config['ignore-prepublish'],
scriptsPrependNodePath: config['scripts-prepend-node-path'],
unsafePerm: config['unsafe-perm'],
log,
dir,
failOk: false,
production: config.production
}
}

return _config
})
}
14 changes: 7 additions & 7 deletions lib/extract.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict'

const BB = require('bluebird')

const npa = require('npm-package-arg')
const path = require('path')
const pacoteOpts = require('./pacote-opts.js')
const workerFarm = require('worker-farm')

const extractionWorker = require('./worker.js')
Expand All @@ -11,7 +12,7 @@ const WORKER_PATH = require.resolve('./worker.js')
module.exports = {
startWorkers () {
this._workers = workerFarm({
maxConcurrentCallsPerWorker: 30,
maxConcurrentCallsPerWorker: 20,
maxRetries: 1
}, WORKER_PATH)
},
Expand All @@ -20,15 +21,14 @@ module.exports = {
workerFarm.end(this._workers)
},

child (name, child, childPath) {
child (name, child, childPath, opts) {
if (child.bundled) return BB.resolve()

const spec = npa.resolve(name, child.resolved || child.version)
const opts = {
cache: path.resolve(process.env.HOME, '.npm/_cacache'),
const childOpts = pacoteOpts(opts, {
integrity: child.integrity
}
const args = [spec, childPath, opts]
})
const args = [spec, childPath, childOpts]
return BB.fromNode((cb) => {
let launcher = extractionWorker
let msg = args
Expand Down
136 changes: 136 additions & 0 deletions lib/pacote-opts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use strict'

const Buffer = require('safe-buffer').Buffer

const crypto = require('crypto')
const path = require('path')

let effectiveOwner

const npmSession = crypto.randomBytes(8).toString('hex')

module.exports = pacoteOpts
function pacoteOpts (npmOpts, moreOpts) {
const conf = npmOpts.config
const ownerStats = calculateOwner()
const opts = {
cache: path.join(conf['cache'], '_cacache'),
ca: conf['ca'],
cert: conf['cert'],
git: conf['git'],
key: conf['key'],
localAddress: conf['local-address'],
loglevel: conf['loglevel'],
maxSockets: +(conf['maxsockets'] || 15),
npmSession: npmSession,
offline: conf['offline'],
projectScope: getProjectScope((npmOpts.rootPkg || moreOpts.rootPkg).name),
proxy: conf['https-proxy'] || conf['proxy'],
refer: 'cipm',
registry: conf['registry'],
retry: {
retries: conf['fetch-retries'],
factor: conf['fetch-retry-factor'],
minTimeout: conf['fetch-retry-mintimeout'],
maxTimeout: conf['fetch-retry-maxtimeout']
},
strictSSL: conf['strict-ssl'],
userAgent: conf['user-agent'],

dmode: parseInt('0777', 8) & (~conf['umask']),
fmode: parseInt('0666', 8) & (~conf['umask']),
umask: conf['umask']
}

if (ownerStats.uid != null || ownerStats.gid != null) {
Object.assign(opts, ownerStats)
}

Object.keys(conf).forEach(k => {
const authMatchGlobal = k.match(
/^(_authToken|username|_password|password|email|always-auth|_auth)$/
)
const authMatchScoped = k[0] === '/' && k.match(
/(.*):(_authToken|username|_password|password|email|always-auth|_auth)$/
)

// if it matches scoped it will also match global
if (authMatchGlobal || authMatchScoped) {
let nerfDart = null
let key = null
let val = null

if (!opts.auth) { opts.auth = {} }

if (authMatchScoped) {
nerfDart = authMatchScoped[1]
key = authMatchScoped[2]
val = conf[k]
if (!opts.auth[nerfDart]) {
opts.auth[nerfDart] = {
alwaysAuth: !!conf['always-auth']
}
}
} else {
key = authMatchGlobal[1]
val = conf[k]
opts.auth.alwaysAuth = !!conf['always-auth']
}

const auth = authMatchScoped ? opts.auth[nerfDart] : opts.auth
if (key === '_authToken') {
auth.token = val
} else if (key.match(/password$/i)) {
auth.password =
// the config file stores password auth already-encoded. pacote expects
// the actual username/password pair.
Buffer.from(val, 'base64').toString('utf8')
} else if (key === 'always-auth') {
auth.alwaysAuth = val === 'false' ? false : !!val
} else {
auth[key] = val
}
}

if (k[0] === '@') {
if (!opts.scopeTargets) { opts.scopeTargets = {} }
opts.scopeTargets[k.replace(/:registry$/, '')] = conf[k]
}
})

Object.keys(moreOpts || {}).forEach((k) => {
opts[k] = moreOpts[k]
})

return opts
}

function calculateOwner () {
if (!effectiveOwner) {
effectiveOwner = { uid: 0, gid: 0 }

// Pretty much only on windows
if (!process.getuid) {
return effectiveOwner
}

effectiveOwner.uid = +process.getuid()
effectiveOwner.gid = +process.getgid()

if (effectiveOwner.uid === 0) {
if (process.env.SUDO_UID) effectiveOwner.uid = +process.env.SUDO_UID
if (process.env.SUDO_GID) effectiveOwner.gid = +process.env.SUDO_GID
}
}

return effectiveOwner
}

function getProjectScope (pkgName) {
const sep = pkgName.indexOf('/')
if (sep === -1) {
return ''
} else {
return pkgName.slice(0, sep)
}
}
1 change: 1 addition & 0 deletions lib/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = (args, cb) => {
const extractTo = parsed[1]
const opts = parsed[2]
opts.log = log
log.level = opts.loglevel
return rimraf(extractTo, {ignore: 'node_modules'}).then(() => {
return pacote.extract(spec, extractTo, opts)
}).nodeify(cb)
Expand Down
3 changes: 1 addition & 2 deletions test/specs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ test('runs lifecycle hooks of packages with env variables', t => {
}
})

new Installer({prefix}).run().then(details => {
return new Installer({prefix}).run().then(details => {
t.equal(details.pkgCount, 1)
t.match(fixtureHelper.read(prefix, 'preinstall'), 'preinstall')
t.match(fixtureHelper.read(prefix, 'install'), 'install')
Expand All @@ -241,7 +241,6 @@ test('runs lifecycle hooks of packages with env variables', t => {

fixtureHelper.teardown()
console.log = originalConsoleLog
t.end()
})
})

Expand Down
6 changes: 3 additions & 3 deletions test/specs/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ test('config: parses configs from npm', t => {
const expectedConfig = { a: 1, b: 2 }

config(dir).then(config => {
t.same(config.config.a, expectedConfig.a)
t.same(config.config.b, expectedConfig.b)
t.same(config.dir, dir)
t.same(config.lifecycleOpts.config.a, expectedConfig.a)
t.same(config.lifecycleOpts.config.b, expectedConfig.b)
t.same(config.prefix, dir)
t.same(config.log, npmlog)
t.end()
})
Expand Down
36 changes: 36 additions & 0 deletions test/specs/lib/pacote-opts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

const test = require('tap').test

const pacoteOpts = require('../../../lib/pacote-opts.js')

test('returns a config object usable by pacote', t => {
const opts = pacoteOpts({
config: {
ca: 'idk',
cache: '/foo',
'maxsockets': '10',
'fetch-retries': 23,
_authToken: 'deadbeef',
'//registry.npmjs.org:_authToken': 'c0ffee',
'@myscope:registry': 'https://my-other.registry.internet/'
}
}, {
rootPkg: require('../../../package.json')
})

t.equal(opts.ca, 'idk', 'ca passed through as-is')
t.equal(opts.cache.replace(/[\\]/g, '/'), '/foo/_cacache', 'cache path has _cacache appended')
t.equal(opts.maxSockets, 10, 'maxSockets converted to number')
t.equal(opts.retry.retries, 23, 'retries put into object')
t.similar(opts.auth, {
token: 'deadbeef',
'//registry.npmjs.org': {
token: 'c0ffee'
}
})
t.deepEqual(opts.scopeTargets, {
'@myscope': 'https://my-other.registry.internet/'
}, 'scope target included')
t.done()
})

0 comments on commit 6f2bd51

Please sign in to comment.