Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 5 additions & 2 deletions lib/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ function add (args, where) {
log.verbose('cache add', 'spec', spec)
if (!spec) return BB.reject(new Error(usage))
log.silly('cache add', 'parsed spec', spec)
return finished(pacote.tarball.stream(spec, npmConfig({where})).resume())
const opts = npmConfig({ where })
const plain = opts.toJSON()
return finished(pacote.tarball.stream(spec, plain).resume())
}

cache.verify = verify
Expand All @@ -132,6 +134,7 @@ cache.unpack = unpack
function unpack (pkg, ver, unpackTarget, dmode, fmode, uid, gid) {
return unbuild([unpackTarget], true).then(() => {
const opts = npmConfig({dmode, fmode, uid, gid, offline: true})
return pacote.extract(npa.resolve(pkg, ver), unpackTarget, opts)
const plain = opts.toJSON()
return pacote.extract(npa.resolve(pkg, ver), unpackTarget, plain)
})
}
4 changes: 4 additions & 0 deletions lib/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
'ham-it-up': false,
heading: 'npm',
'if-present': false,
include: [],
'ignore-prepublish': false,
'ignore-scripts': false,
'init-module': path.resolve(home, '.npm-init.js'),
Expand All @@ -185,6 +186,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
'node-options': null,
'node-version': process.version,
'offline': false,
omit: [],
'onload-script': false,
only: null,
optional: true,
Expand Down Expand Up @@ -304,6 +306,7 @@ exports.types = {
'ham-it-up': Boolean,
'heading': String,
'if-present': Boolean,
include: [Array, 'dev', 'optional', 'peer'],
'ignore-prepublish': Boolean,
'ignore-scripts': Boolean,
'init-module': path,
Expand All @@ -328,6 +331,7 @@ exports.types = {
'node-version': [null, semver],
'noproxy': [null, String, Array],
offline: Boolean,
omit: [Array, 'dev', 'optional', 'peer'],
'onload-script': [null, String],
only: [null, 'dev', 'development', 'prod', 'production'],
optional: Boolean,
Expand Down
216 changes: 216 additions & 0 deletions lib/config/flat-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// return a flattened config object with canonical names suitable for
// passing to dependencies like arborist, pacote, npm-registry-fetch, etc.

const log = require('npmlog')
const crypto = require('crypto')
const npmSession = crypto.randomBytes(8).toString('hex')
log.verbose('npm-session', npmSession)
const {join} = require('path')

const buildOmitList = npm => {
const include = npm.config.get('include') || []
const omit = new Set((npm.config.get('omit') || [])
.filter(type => !include.includes(type)))
const only = npm.config.get('only')

if (/^prod(uction)?$/.test(only) || npm.config.get('production')) {
omit.add('dev')
}

if (/dev/.test(npm.config.get('also'))) {
omit.delete('dev')
}

if (npm.config.get('dev')) {
omit.delete('dev')
}

if (npm.config.get('optional') === false) {
omit.add('optional')
}

npm.config.set('omit', [...omit])
return [...omit]
}

const flatOptions = npm => npm.flatOptions || Object.freeze({
// Note that many of these do not come from configs or cli flags
// per se, though they may be implied or defined by them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking as someone who maintains a CLI (lerna) that implements npm publishing using current libnpm* packages (with an alarming amount of copypasta from npm's source) and attempts to stay as consistent as possible with npm CLI patterns, extracting "How npm reads and serializes its configuration" into a separate package would be a great boon.

I realize it's extremely low on the priority list, but I just wanted to register the sentiment while it's top-of-mind, and before I inevitably feel the urge to make more copypasta...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would be really nice, I agree. (Also speaking as someone who maintains and uses a lot of npm deps ;)

We had once upon a time a package called npmconf that housed all of the config reading and management as well as all the default values. The problem was that every time we wanted to add or modify a config value, it meant an update in a bunch of disparate places, and it was kind of a pain to manage, so we folded it back in. This gets more convoluted when we have a bunch of other modules expecting to get a preferOnline option and need to know to convert that from config.get('prefer-online') (or even config.get('cache-min')<=0). The goal of figgy-pudding was to make this easier, but in practice, has made it much harder, which is why we're moving to the simplest possible approach, and from there can build up something a bit more maintainable. Just taking an inventory of all our configs as part of this upgrade process has been really enlightening. The clever technical solution didn't actually get at the root of the problem, and doing the work to dredge it all up and clean the actual mess has been sorely needed.

Some day, I'd really like to consider dropping nopt in favor of something a bit more simple and declarative, like jackspeak or even yargs, and then have a package that doesn't handle defaults, but just reads and updates config files, so that the set of defaults and expected types can still live in npm/cli maybe. It's a weird sort of animal to cut in half, though, because I do want to be able to provide some feedback to the user that --umask=notanumber is an invalid config value. So I'm not sure exactly what that'll look like. We may be in this "create a pojo and share it" mode for a while, most likely at least through the v7 release line. It just makes it so much easier to manage our deps if they can get an options object like any other JavaScript module out there does, without the extra translation layer of fp.

log,
npmSession,
dmode: npm.modes.exec,
fmode: npm.modes.file,
umask: npm.modes.umask,
hashAlgorithm: 'sha1', // XXX should this be sha512?
color: !!npm.color,
includeDeprecated: false,

projectScope: npm.projectScope,
npmVersion: npm.version,
nodeVersion: npm.config.get('node-version'),
npmCommand: npm.command,

tmp: npm.tmp,
cache: join(npm.config.get('cache'), '_cacache'),
prefix: npm.prefix,
globalPrefix: npm.globalPrefix,
localPrefix: npm.localPrefix,
global: npm.config.get('global'),

metricsRegistry: npm.config.get('metrics-registry'),
sendMetrics: npm.config.get('send-metrics'),
registry: npm.config.get('registry'),
get scope () {
log.warn('FIXME', 'using opts.scope instead of opts.projectScope')
return npm.projectScope
},
access: npm.config.get('access'),
alwaysAuth: npm.config.get('always-auth'),
audit: npm.config.get('audit'),
auditLevel: npm.config.get('audit-level'),
authType: npm.config.get('auth-type'),
before: npm.config.get('before'),
browser: npm.config.get('browser'),
ca: npm.config.get('ca'),
cafile: npm.config.get('cafile'),
cert: npm.config.get('cert'),
key: npm.config.get('key'),

// XXX remove these when we don't use lockfile any more, once
// arborist is handling the installation process
cacheLockRetries: npm.config.get('cache-lock-retries'),
cacheLockStale: npm.config.get('cache-lock-stale'),
cacheLockWait: npm.config.get('cache-lock-wait'),
lockFile: {
retries: npm.config.get('cache-lock-retries'),
stale: npm.config.get('cache-lock-stale'),
wait: npm.config.get('cache-lock-wait')
},

// XXX remove these once no longer used
get cacheMax () {
log.warn('FIXME', 'using deprecated cacheMax option, should use offline/preferOffline/preferOnline')
return npm.config.get('cache-max')
},
get cacheMin () {
log.warn('FIXME', 'using deprecated cacheMin option, should use offline/preferOffline/preferOnline')
return npm.config.get('cache-min')
},

// token creation options
cidr: npm.config.get('cidr'),
readOnly: npm.config.get('read-only'),

// npm version options
preid: npm.config.get('preid'),
tagVersionPrefix: npm.config.get('tag-version-prefix'),
allowSameVersion: npm.config.get('allow-same-version'),

// npm version git options
message: npm.config.get('message'),
commitHooks: npm.config.get('commit-hooks'),
gitTagVersion: npm.config.get('git-tag-version'),
signGitCommit: npm.config.get('sign-git-commit'),
signGitTag: npm.config.get('sign-git-tag'),

// only used for npm ls in v7, not update
depth: npm.config.get('depth'),

// options for npm search
// XXX need to refactor these to a cleaner search: { ... } block,
// since libnpmsearch needs them in a different format anyway, or
// maybe just not include them here at all, and construct an options
// pojo in lib/search.js instead.
description: npm.config.get('description'),
searchexclude: npm.config.get('searchexclude'),
searchlimit: npm.config.get('searchlimit'),
searchopts: npm.config.get('searchopts'),
searchstaleness: npm.config.get('searchstaleness'),

dryRun: npm.config.get('dry-run'),
engineStrict: npm.config.get('engine-strict'),

retry: {
retries: npm.config.get('fetch-retries'),
factor: npm.config.get('fetch-retry-factor'),
maxTimeout: npm.config.get('fetch-retry-maxtimeout'),
minTimeout: npm.config.get('fetch-retry-mintimeout')
},

force: npm.config.get('force'),

formatPackageLock: npm.config.get('format-package-lock'),
fund: npm.config.get('fund'),

// binary locators
git: npm.config.get('git'),
npmBin: require.main.filename,
nodeBin: process.env.NODE || process.execPath,
viewer: npm.config.get('viewer'),
editor: npm.config.get('editor'),

// configs that affect how we build trees
binLinks: npm.config.get('bin-links'),
rebuildBundle: npm.config.get('rebuild-bundle'),
packageLock: npm.config.get('package-lock'),
packageLockOnly: npm.config.get('package-lock-only'),
globalStyle: npm.config.get('global-style'),
legacyBundling: npm.config.get('legacy-bundling'),
omit: buildOmitList(npm),

// used to build up the appropriate {add:{...}} options to Arborist.reify
save: npm.config.get('save'),
saveBundle: npm.config.get('save-bundle'),
saveDev: npm.config.get('save-dev'),
saveOptional: npm.config.get('save-optional'),
savePeer: npm.config.get('save-peer'),
saveProd: npm.config.get('save-prod'),
saveExact: npm.config.get('save-exact'),
savePrefix: npm.config.get('save-prefix'),

// configs for npm-registry-fetch
otp: npm.config.get('otp'),
offline: npm.config.get('offline'),
preferOffline: getPreferOffline(npm),
preferOnline: getPreferOnline(npm),
strictSSL: npm.config.get('strict-ssl'),
defaultTag: npm.config.get('tag'),
get tag () {
log.warn('FIXME', 'using tag option, should be defaultTag')
return npm.config.get('tag')
},
userAgent: npm.config.get('user-agent'),

...getScopesAndAuths(npm)
})

const getPreferOnline = npm => {
const po = npm.config.get('prefer-online')
if (po !== undefined) {
return po
}
return npm.config.get('cache-max') <= 0
}

const getPreferOffline = npm => {
const po = npm.config.get('prefer-offline')
if (po !== undefined) {
return po
}
return npm.config.get('cache-min') >= 9999
}

// pull out all the @scope:<key> and //host:key config fields
// these are used by npm-registry-fetch for authing against registries
const getScopesAndAuths = npm => {
const scopesAndAuths = {}
// pull out all the @scope:... configs into a flat object.
for (const key in npm.config.list[0]) {
if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) {
scopesAndAuths[key] = npm.config.get(key)
}
}
return scopesAndAuths
}

module.exports = flatOptions
42 changes: 39 additions & 3 deletions lib/fetch-package-metadata.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const figgyPudding = require('figgy-pudding')
const deprCheck = require('./utils/depr-check')
const path = require('path')
const log = require('npmlog')
Expand Down Expand Up @@ -32,6 +33,38 @@ const CACHE = new LRUCache({
length: (p) => p._contentLength
})

const FetchConfig = figgyPudding({
annotate: {},
before: 'enjoyBy',
cache: {},
defaultIntegrityAlgorithm: {},
dmode: {},
'enjoy-by': 'enjoyBy',
enjoyBy: {},
fmode: {},
fullMetadata: 'full-metadata',
'full-metadata': {},
headers: {},
integrity: {},
log: { default: log },
npmBin: { default: 'npm' },
npmCliConfig: {},
npmInstallCmd: {},
npmRunCmd: { default: 'run' },
offline: {},
pkgid: {},
preferOffline: 'prefer-offline',
'prefer-offline': {},
preferOnline: 'prefer-online',
'prefer-online': {},
registry: { default: 'https://registry.npmjs.org' },
resolved: {},
tag: { default: 'latest' },
umask: {},
'user-agent': {},
where: {}
})

module.exports = limit(fetchPackageMetadata, npm.limit.fetch)
function fetchPackageMetadata (spec, where, opts, done) {
validate('SSOF|SSFZ|OSOF|OSFZ', [spec, where, opts, done])
Expand All @@ -56,13 +89,15 @@ function fetchPackageMetadata (spec, where, opts, done) {
if (!npmConfig) {
npmConfig = require('./config/figgy-config.js')
}
pacote.manifest(dep, npmConfig({
const conf = FetchConfig(npmConfig({
annotate: true,
fullMetadata: opts.fullMetadata,
log: tracker || npmlog,
memoize: CACHE,
where: where
})).then(
}))
const plain = conf.toJSON()
pacote.manifest(dep, plain).then(
(pkg) => logAndFinish(null, deprCheck(pkg)),
(err) => {
if (dep.type !== 'directory') return logAndFinish(err)
Expand Down Expand Up @@ -103,7 +138,8 @@ function addBundled (pkg, next) {
npmConfig = require('./config/figgy-config.js')
}
const opts = npmConfig({integrity: pkg._integrity})
pacote.extract(pkg._resolved || pkg._requested || npa.resolve(pkg.name, pkg.version), target, opts).then(() => {
const plain = opts.toJSON()
pacote.extract(pkg._resolved || pkg._requested || npa.resolve(pkg.name, pkg.version), target, plain).then(() => {
log.silly('addBundled', 'read tarball')
readPackageTree(target, (err, tree) => {
if (err) { return next(err) }
Expand Down
6 changes: 4 additions & 2 deletions lib/install/action/extract-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const BB = require('bluebird')

const extract = require('pacote/extract')
const pacote = require('pacote')
// const npmlog = require('npmlog')

module.exports = (args, cb) => {
Expand All @@ -14,5 +14,7 @@ module.exports = (args, cb) => {
// opts.log = npmlog
// }
// opts.log.level = opts.loglevel || opts.log.level
BB.resolve(extract(spec, extractTo, opts)).nodeify(cb)
// NOTE: this might be naive; we might need to check if `toJSON` exists
const plain = opts.toJSON()
BB.resolve(pacote.extract(spec, extractTo, plain)).nodeify(cb)
}
3 changes: 2 additions & 1 deletion lib/install/action/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = fetch
function fetch (staging, pkg, log, next) {
log.silly('fetch', packageId(pkg))
const opts = npmConfig({integrity: pkg.package._integrity})
return finished(pacote.tarball.stream(pkg.package._requested, opts))
const plain = opts.toJSON()
return finished(pacote.tarball.stream(pkg.package._requested, plain))
.then(() => next(), next)
}
Loading