Installing dependencies.
This commit is contained in:
+3
@@ -0,0 +1,3 @@
|
||||
const r = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' +
|
||||
'\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g')
|
||||
module.exports = str => str.replace(r, '')
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
const log = require('./log-shim')
|
||||
|
||||
// print an error or just nothing if the audit report has an error
|
||||
// this is called by the audit command, and by the reify-output util
|
||||
// prints a JSON version of the error if it's --json
|
||||
// returns 'true' if there was an error, false otherwise
|
||||
|
||||
const auditError = (npm, report) => {
|
||||
if (!report || !report.error) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (npm.command !== 'audit') {
|
||||
return true
|
||||
}
|
||||
|
||||
const { error } = report
|
||||
|
||||
// ok, we care about it, then
|
||||
log.warn('audit', error.message)
|
||||
const { body: errBody } = error
|
||||
const body = Buffer.isBuffer(errBody) ? errBody.toString() : errBody
|
||||
if (npm.flatOptions.json) {
|
||||
npm.output(JSON.stringify({
|
||||
message: error.message,
|
||||
method: error.method,
|
||||
uri: error.uri,
|
||||
headers: error.headers,
|
||||
statusCode: error.statusCode,
|
||||
body,
|
||||
}, null, 2))
|
||||
} else {
|
||||
npm.output(body)
|
||||
}
|
||||
|
||||
throw 'audit endpoint returned an error'
|
||||
}
|
||||
|
||||
module.exports = auditError
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
const abbrev = require('abbrev')
|
||||
|
||||
// plumbing should not have any aliases
|
||||
const aliases = {
|
||||
|
||||
// aliases
|
||||
login: 'adduser',
|
||||
author: 'owner',
|
||||
home: 'docs',
|
||||
issues: 'bugs',
|
||||
info: 'view',
|
||||
show: 'view',
|
||||
find: 'search',
|
||||
add: 'install',
|
||||
unlink: 'uninstall',
|
||||
remove: 'uninstall',
|
||||
rm: 'uninstall',
|
||||
r: 'uninstall',
|
||||
|
||||
// short names for common things
|
||||
un: 'uninstall',
|
||||
rb: 'rebuild',
|
||||
list: 'ls',
|
||||
ln: 'link',
|
||||
create: 'init',
|
||||
i: 'install',
|
||||
it: 'install-test',
|
||||
cit: 'install-ci-test',
|
||||
up: 'update',
|
||||
c: 'config',
|
||||
s: 'search',
|
||||
se: 'search',
|
||||
tst: 'test',
|
||||
t: 'test',
|
||||
ddp: 'dedupe',
|
||||
v: 'view',
|
||||
run: 'run-script',
|
||||
'clean-install': 'ci',
|
||||
'clean-install-test': 'cit',
|
||||
x: 'exec',
|
||||
why: 'explain',
|
||||
la: 'll',
|
||||
verison: 'version',
|
||||
ic: 'ci',
|
||||
|
||||
// typos
|
||||
innit: 'init',
|
||||
// manually abbrev so that install-test doesn't make insta stop working
|
||||
in: 'install',
|
||||
ins: 'install',
|
||||
inst: 'install',
|
||||
insta: 'install',
|
||||
instal: 'install',
|
||||
isnt: 'install',
|
||||
isnta: 'install',
|
||||
isntal: 'install',
|
||||
isntall: 'install',
|
||||
'install-clean': 'ci',
|
||||
'isntall-clean': 'ci',
|
||||
hlep: 'help',
|
||||
'dist-tags': 'dist-tag',
|
||||
upgrade: 'update',
|
||||
udpate: 'update',
|
||||
rum: 'run-script',
|
||||
sit: 'cit',
|
||||
urn: 'run-script',
|
||||
ogr: 'org',
|
||||
'add-user': 'adduser',
|
||||
}
|
||||
|
||||
// these are filenames in .
|
||||
// Keep these sorted so that lib/utils/npm-usage.js outputs in order
|
||||
const cmdList = [
|
||||
'access',
|
||||
'adduser',
|
||||
'audit',
|
||||
'bin',
|
||||
'bugs',
|
||||
'cache',
|
||||
'ci',
|
||||
'completion',
|
||||
'config',
|
||||
'dedupe',
|
||||
'deprecate',
|
||||
'diff',
|
||||
'dist-tag',
|
||||
'docs',
|
||||
'doctor',
|
||||
'edit',
|
||||
'exec',
|
||||
'explain',
|
||||
'explore',
|
||||
'find-dupes',
|
||||
'fund',
|
||||
'get',
|
||||
'help',
|
||||
'hook',
|
||||
'init',
|
||||
'install',
|
||||
'install-ci-test',
|
||||
'install-test',
|
||||
'link',
|
||||
'll',
|
||||
'login', // This is an alias for `adduser` but it can be confusing
|
||||
'logout',
|
||||
'ls',
|
||||
'org',
|
||||
'outdated',
|
||||
'owner',
|
||||
'pack',
|
||||
'ping',
|
||||
'pkg',
|
||||
'prefix',
|
||||
'profile',
|
||||
'prune',
|
||||
'publish',
|
||||
'query',
|
||||
'rebuild',
|
||||
'repo',
|
||||
'restart',
|
||||
'root',
|
||||
'run-script',
|
||||
'search',
|
||||
'set',
|
||||
'set-script',
|
||||
'shrinkwrap',
|
||||
'star',
|
||||
'stars',
|
||||
'start',
|
||||
'stop',
|
||||
'team',
|
||||
'test',
|
||||
'token',
|
||||
'uninstall',
|
||||
'unpublish',
|
||||
'unstar',
|
||||
'update',
|
||||
'version',
|
||||
'view',
|
||||
'whoami',
|
||||
]
|
||||
|
||||
const plumbing = ['birthday', 'help-search']
|
||||
const abbrevs = abbrev(cmdList.concat(Object.keys(aliases)))
|
||||
|
||||
module.exports = {
|
||||
abbrevs,
|
||||
aliases,
|
||||
cmdList,
|
||||
plumbing,
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
###-begin-npm-completion-###
|
||||
#
|
||||
# npm command completion script
|
||||
#
|
||||
# Installation: npm completion >> ~/.bashrc (or ~/.zshrc)
|
||||
# Or, maybe: npm completion > /usr/local/etc/bash_completion.d/npm
|
||||
#
|
||||
|
||||
if type complete &>/dev/null; then
|
||||
_npm_completion () {
|
||||
local words cword
|
||||
if type _get_comp_words_by_ref &>/dev/null; then
|
||||
_get_comp_words_by_ref -n = -n @ -n : -w words -i cword
|
||||
else
|
||||
cword="$COMP_CWORD"
|
||||
words=("${COMP_WORDS[@]}")
|
||||
fi
|
||||
|
||||
local si="$IFS"
|
||||
if ! IFS=$'\n' COMPREPLY=($(COMP_CWORD="$cword" \
|
||||
COMP_LINE="$COMP_LINE" \
|
||||
COMP_POINT="$COMP_POINT" \
|
||||
npm completion -- "${words[@]}" \
|
||||
2>/dev/null)); then
|
||||
local ret=$?
|
||||
IFS="$si"
|
||||
return $ret
|
||||
fi
|
||||
IFS="$si"
|
||||
if type __ltrim_colon_completions &>/dev/null; then
|
||||
__ltrim_colon_completions "${words[cword]}"
|
||||
fi
|
||||
}
|
||||
complete -o default -F _npm_completion npm
|
||||
elif type compdef &>/dev/null; then
|
||||
_npm_completion() {
|
||||
local si=$IFS
|
||||
compadd -- $(COMP_CWORD=$((CURRENT-1)) \
|
||||
COMP_LINE=$BUFFER \
|
||||
COMP_POINT=0 \
|
||||
npm completion -- "${words[@]}" \
|
||||
2>/dev/null)
|
||||
IFS=$si
|
||||
}
|
||||
compdef _npm_completion npm
|
||||
elif type compctl &>/dev/null; then
|
||||
_npm_completion () {
|
||||
local cword line point words si
|
||||
read -Ac words
|
||||
read -cn cword
|
||||
let cword-=1
|
||||
read -l line
|
||||
read -ln point
|
||||
si="$IFS"
|
||||
if ! IFS=$'\n' reply=($(COMP_CWORD="$cword" \
|
||||
COMP_LINE="$line" \
|
||||
COMP_POINT="$point" \
|
||||
npm completion -- "${words[@]}" \
|
||||
2>/dev/null)); then
|
||||
|
||||
local ret=$?
|
||||
IFS="$si"
|
||||
return $ret
|
||||
fi
|
||||
IFS="$si"
|
||||
}
|
||||
compctl -K _npm_completion npm
|
||||
fi
|
||||
###-end-npm-completion-###
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
const { resolve } = require('path')
|
||||
const Arborist = require('@npmcli/arborist')
|
||||
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
||||
|
||||
const installedDeep = async (npm) => {
|
||||
const {
|
||||
depth,
|
||||
global,
|
||||
prefix,
|
||||
workspacesEnabled,
|
||||
} = npm.flatOptions
|
||||
|
||||
const getValues = (tree) =>
|
||||
[...tree.inventory.values()]
|
||||
.filter(i => i.location !== '' && !i.isRoot)
|
||||
.map(i => {
|
||||
return i
|
||||
})
|
||||
.filter(i => (i.depth - 1) <= depth)
|
||||
.sort((a, b) => (a.depth - b.depth) || localeCompare(a.name, b.name))
|
||||
|
||||
const res = new Set()
|
||||
const gArb = new Arborist({
|
||||
global: true,
|
||||
path: resolve(npm.globalDir, '..'),
|
||||
workspacesEnabled,
|
||||
})
|
||||
const gTree = await gArb.loadActual({ global: true })
|
||||
|
||||
for (const node of getValues(gTree)) {
|
||||
res.add(global ? node.name : [node.name, '-g'])
|
||||
}
|
||||
|
||||
if (!global) {
|
||||
const arb = new Arborist({ global: false, path: prefix, workspacesEnabled })
|
||||
const tree = await arb.loadActual()
|
||||
for (const node of getValues(tree)) {
|
||||
res.add(node.name)
|
||||
}
|
||||
}
|
||||
|
||||
return [...res]
|
||||
}
|
||||
|
||||
module.exports = installedDeep
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
const { promisify } = require('util')
|
||||
const readdir = promisify(require('readdir-scoped-modules'))
|
||||
|
||||
const installedShallow = async (npm, opts) => {
|
||||
const names = global => readdir(global ? npm.globalDir : npm.localDir)
|
||||
const { conf: { argv: { remain } } } = opts
|
||||
if (remain.length > 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { global } = npm.flatOptions
|
||||
const locals = global ? [] : await names(false)
|
||||
const globals = (await names(true)).map(n => global ? n : `${n} -g`)
|
||||
return [...locals, ...globals]
|
||||
}
|
||||
|
||||
module.exports = installedShallow
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
// class that describes a config key we know about
|
||||
// this keeps us from defining a config key and not
|
||||
// providing a default, description, etc.
|
||||
//
|
||||
// TODO: some kind of categorization system, so we can
|
||||
// say "these are for registry access", "these are for
|
||||
// version resolution" etc.
|
||||
|
||||
const required = ['type', 'description', 'default', 'key']
|
||||
|
||||
const allowed = [
|
||||
'default',
|
||||
'defaultDescription',
|
||||
'deprecated',
|
||||
'description',
|
||||
'flatten',
|
||||
'hint',
|
||||
'key',
|
||||
'short',
|
||||
'type',
|
||||
'typeDescription',
|
||||
'usage',
|
||||
'envExport',
|
||||
]
|
||||
|
||||
const {
|
||||
typeDefs: {
|
||||
semver: { type: semver },
|
||||
Umask: { type: Umask },
|
||||
url: { type: url },
|
||||
path: { type: path },
|
||||
},
|
||||
} = require('@npmcli/config')
|
||||
|
||||
class Definition {
|
||||
constructor (key, def) {
|
||||
this.key = key
|
||||
// if it's set falsey, don't export it, otherwise we do by default
|
||||
this.envExport = true
|
||||
Object.assign(this, def)
|
||||
this.validate()
|
||||
if (!this.defaultDescription) {
|
||||
this.defaultDescription = describeValue(this.default)
|
||||
}
|
||||
if (!this.typeDescription) {
|
||||
this.typeDescription = describeType(this.type)
|
||||
}
|
||||
// hint is only used for non-boolean values
|
||||
if (!this.hint) {
|
||||
if (this.type === Number) {
|
||||
this.hint = '<number>'
|
||||
} else {
|
||||
this.hint = `<${this.key}>`
|
||||
}
|
||||
}
|
||||
if (!this.usage) {
|
||||
this.usage = describeUsage(this)
|
||||
}
|
||||
}
|
||||
|
||||
validate () {
|
||||
for (const req of required) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this, req)) {
|
||||
throw new Error(`config lacks ${req}: ${this.key}`)
|
||||
}
|
||||
}
|
||||
if (!this.key) {
|
||||
throw new Error(`config lacks key: ${this.key}`)
|
||||
}
|
||||
for (const field of Object.keys(this)) {
|
||||
if (!allowed.includes(field)) {
|
||||
throw new Error(`config defines unknown field ${field}: ${this.key}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a textual description of this config, suitable for help output
|
||||
describe () {
|
||||
const description = unindent(this.description)
|
||||
const noEnvExport = this.envExport
|
||||
? ''
|
||||
: `
|
||||
This value is not exported to the environment for child processes.
|
||||
`
|
||||
const deprecated = !this.deprecated ? '' : `* DEPRECATED: ${unindent(this.deprecated)}\n`
|
||||
return wrapAll(`#### \`${this.key}\`
|
||||
|
||||
* Default: ${unindent(this.defaultDescription)}
|
||||
* Type: ${unindent(this.typeDescription)}
|
||||
${deprecated}
|
||||
${description}
|
||||
${noEnvExport}`)
|
||||
}
|
||||
}
|
||||
|
||||
const describeUsage = def => {
|
||||
let key = ''
|
||||
|
||||
// Single type
|
||||
if (!Array.isArray(def.type)) {
|
||||
if (def.short) {
|
||||
key = `-${def.short}|`
|
||||
}
|
||||
|
||||
if (def.type === Boolean && def.default !== false) {
|
||||
key = `${key}--no-${def.key}`
|
||||
} else {
|
||||
key = `${key}--${def.key}`
|
||||
}
|
||||
|
||||
if (def.type !== Boolean) {
|
||||
key = `${key} ${def.hint}`
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
key = `--${def.key}`
|
||||
if (def.short) {
|
||||
key = `-${def.short}|--${def.key}`
|
||||
}
|
||||
|
||||
// Multiple types
|
||||
let types = def.type
|
||||
const multiple = types.includes(Array)
|
||||
const bool = types.includes(Boolean)
|
||||
|
||||
// null type means optional and doesn't currently affect usage output since
|
||||
// all non-optional params have defaults so we render everything as optional
|
||||
types = types.filter(t => t !== null && t !== Array && t !== Boolean)
|
||||
|
||||
if (!types.length) {
|
||||
return key
|
||||
}
|
||||
|
||||
let description
|
||||
if (!types.some(t => typeof t !== 'string')) {
|
||||
// Specific values, use specifics given
|
||||
description = `<${types.filter(d => d).join('|')}>`
|
||||
} else {
|
||||
// Generic values, use hint
|
||||
description = def.hint
|
||||
}
|
||||
|
||||
if (bool) {
|
||||
// Currently none of our multi-type configs with boolean values default to
|
||||
// false so all their hints should show `--no-`, if we ever add ones that
|
||||
// default to false we can branch the logic here
|
||||
key = `--no-${def.key}|${key}`
|
||||
}
|
||||
|
||||
const usage = `${key} ${description}`
|
||||
if (multiple) {
|
||||
return `${usage} [${usage} ...]`
|
||||
} else {
|
||||
return usage
|
||||
}
|
||||
}
|
||||
|
||||
const describeType = type => {
|
||||
if (Array.isArray(type)) {
|
||||
const descriptions = type.filter(t => t !== Array).map(t => describeType(t))
|
||||
|
||||
// [a] => "a"
|
||||
// [a, b] => "a or b"
|
||||
// [a, b, c] => "a, b, or c"
|
||||
// [a, Array] => "a (can be set multiple times)"
|
||||
// [a, Array, b] => "a or b (can be set multiple times)"
|
||||
const last = descriptions.length > 1 ? [descriptions.pop()] : []
|
||||
const oxford = descriptions.length > 1 ? ', or ' : ' or '
|
||||
const words = [descriptions.join(', ')].concat(last).join(oxford)
|
||||
const multiple = type.includes(Array) ? ' (can be set multiple times)' : ''
|
||||
return `${words}${multiple}`
|
||||
}
|
||||
|
||||
// Note: these are not quite the same as the description printed
|
||||
// when validation fails. In that case, we want to give the user
|
||||
// a bit more information to help them figure out what's wrong.
|
||||
switch (type) {
|
||||
case String:
|
||||
return 'String'
|
||||
case Number:
|
||||
return 'Number'
|
||||
case Umask:
|
||||
return 'Octal numeric string in range 0000..0777 (0..511)'
|
||||
case Boolean:
|
||||
return 'Boolean'
|
||||
case Date:
|
||||
return 'Date'
|
||||
case path:
|
||||
return 'Path'
|
||||
case semver:
|
||||
return 'SemVer string'
|
||||
case url:
|
||||
return 'URL'
|
||||
default:
|
||||
return describeValue(type)
|
||||
}
|
||||
}
|
||||
|
||||
// if it's a string, quote it. otherwise, just cast to string.
|
||||
const describeValue = val => (typeof val === 'string' ? JSON.stringify(val) : String(val))
|
||||
|
||||
const unindent = s => {
|
||||
// get the first \n followed by a bunch of spaces, and pluck off
|
||||
// that many spaces from the start of every line.
|
||||
const match = s.match(/\n +/)
|
||||
return !match ? s.trim() : s.split(match[0]).join('\n').trim()
|
||||
}
|
||||
|
||||
const wrap = s => {
|
||||
const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80) - 5
|
||||
return unindent(s)
|
||||
.split(/[ \n]+/)
|
||||
.reduce((left, right) => {
|
||||
const last = left.split('\n').pop()
|
||||
const join = last.length && last.length + right.length > cols ? '\n' : ' '
|
||||
return left + join + right
|
||||
})
|
||||
}
|
||||
|
||||
const wrapAll = s => {
|
||||
let inCodeBlock = false
|
||||
return s
|
||||
.split('\n\n')
|
||||
.map(block => {
|
||||
if (inCodeBlock || block.startsWith('```')) {
|
||||
inCodeBlock = !block.endsWith('```')
|
||||
return block
|
||||
}
|
||||
|
||||
if (block.charAt(0) === '*') {
|
||||
return (
|
||||
'* ' +
|
||||
block
|
||||
.slice(1)
|
||||
.trim()
|
||||
.split('\n* ')
|
||||
.map(li => {
|
||||
return wrap(li).replace(/\n/g, '\n ')
|
||||
})
|
||||
.join('\n* ')
|
||||
)
|
||||
} else {
|
||||
return wrap(block)
|
||||
}
|
||||
})
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
module.exports = Definition
|
||||
+2369
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
||||
const definitions = require('./definitions.js')
|
||||
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
||||
const describeAll = () => {
|
||||
// sort not-deprecated ones to the top
|
||||
/* istanbul ignore next - typically already sorted in the definitions file,
|
||||
* but this is here so that our help doc will stay consistent if we decide
|
||||
* to move them around. */
|
||||
const sort = ([keya, { deprecated: depa }], [keyb, { deprecated: depb }]) => {
|
||||
return depa && !depb ? 1
|
||||
: !depa && depb ? -1
|
||||
: localeCompare(keya, keyb)
|
||||
}
|
||||
return Object.entries(definitions).sort(sort)
|
||||
.map(([key, def]) => def.describe())
|
||||
.join(
|
||||
'\n\n<!-- automatically generated, do not edit manually -->\n' +
|
||||
'<!-- see lib/utils/config/definitions.js -->\n\n'
|
||||
)
|
||||
}
|
||||
module.exports = describeAll
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// use the defined flattening function, and copy over any scoped
|
||||
// registries and registry-specific "nerfdart" configs verbatim
|
||||
//
|
||||
// TODO: make these getters so that we only have to make dirty
|
||||
// the thing that changed, and then flatten the fields that
|
||||
// could have changed when a config.set is called.
|
||||
//
|
||||
// TODO: move nerfdart auth stuff into a nested object that
|
||||
// is only passed along to paths that end up calling npm-registry-fetch.
|
||||
const definitions = require('./definitions.js')
|
||||
const flatten = (obj, flat = {}) => {
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
const def = definitions[key]
|
||||
if (def && def.flatten) {
|
||||
def.flatten(key, obj, flat)
|
||||
} else if (/@.*:registry$/i.test(key) || /^\/\//.test(key)) {
|
||||
flat[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
// XXX make this the bin/npm-cli.js file explicitly instead
|
||||
// otherwise using npm programmatically is a bit of a pain.
|
||||
flat.npmBin = require.main ? require.main.filename
|
||||
: /* istanbul ignore next - not configurable property */ undefined
|
||||
flat.nodeBin = process.env.NODE || process.execPath
|
||||
|
||||
// XXX should this be sha512? is it even relevant?
|
||||
flat.hashAlgorithm = 'sha1'
|
||||
|
||||
return flat
|
||||
}
|
||||
|
||||
module.exports = flatten
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
const flatten = require('./flatten.js')
|
||||
const definitions = require('./definitions.js')
|
||||
const describeAll = require('./describe-all.js')
|
||||
|
||||
// aliases where they get expanded into a completely different thing
|
||||
// these are NOT supported in the environment or npmrc files, only
|
||||
// expanded on the CLI.
|
||||
// TODO: when we switch off of nopt, use an arg parser that supports
|
||||
// more reasonable aliasing and short opts right in the definitions set.
|
||||
const shorthands = {
|
||||
'enjoy-by': ['--before'],
|
||||
d: ['--loglevel', 'info'],
|
||||
dd: ['--loglevel', 'verbose'],
|
||||
ddd: ['--loglevel', 'silly'],
|
||||
quiet: ['--loglevel', 'warn'],
|
||||
q: ['--loglevel', 'warn'],
|
||||
s: ['--loglevel', 'silent'],
|
||||
silent: ['--loglevel', 'silent'],
|
||||
verbose: ['--loglevel', 'verbose'],
|
||||
desc: ['--description'],
|
||||
help: ['--usage'],
|
||||
local: ['--no-global'],
|
||||
n: ['--no-yes'],
|
||||
no: ['--no-yes'],
|
||||
porcelain: ['--parseable'],
|
||||
readonly: ['--read-only'],
|
||||
reg: ['--registry'],
|
||||
iwr: ['--include-workspace-root'],
|
||||
}
|
||||
|
||||
for (const [key, { short }] of Object.entries(definitions)) {
|
||||
if (!short) {
|
||||
continue
|
||||
}
|
||||
// can be either an array or string
|
||||
for (const s of [].concat(short)) {
|
||||
shorthands[s] = [`--${key}`]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get defaults () {
|
||||
// NB: 'default' is a reserved word
|
||||
return Object.entries(definitions).map(([key, { default: def }]) => {
|
||||
return [key, def]
|
||||
}).reduce((defaults, [key, def]) => {
|
||||
defaults[key] = def
|
||||
return defaults
|
||||
}, {})
|
||||
},
|
||||
definitions,
|
||||
flatten,
|
||||
shorthands,
|
||||
describeAll,
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
const { distance } = require('fastest-levenshtein')
|
||||
const readJson = require('read-package-json-fast')
|
||||
const { cmdList } = require('./cmd-list.js')
|
||||
|
||||
const didYouMean = async (npm, path, scmd) => {
|
||||
// const cmd = await npm.cmd(str)
|
||||
const close = cmdList.filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && scmd !== cmd)
|
||||
let best = []
|
||||
for (const str of close) {
|
||||
const cmd = await npm.cmd(str)
|
||||
best.push(` npm ${str} # ${cmd.description}`)
|
||||
}
|
||||
// We would already be suggesting this in `npm x` so omit them here
|
||||
const runScripts = ['stop', 'start', 'test', 'restart']
|
||||
try {
|
||||
const { bin, scripts } = await readJson(`${path}/package.json`)
|
||||
best = best.concat(
|
||||
Object.keys(scripts || {})
|
||||
.filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && !runScripts.includes(cmd))
|
||||
.map(str => ` npm run ${str} # run the "${str}" package script`),
|
||||
Object.keys(bin || {})
|
||||
.filter(cmd => distance(scmd, cmd) < scmd.length * 0.4)
|
||||
/* eslint-disable-next-line max-len */
|
||||
.map(str => ` npm exec ${str} # run the "${str}" command from either this or a remote npm package`)
|
||||
)
|
||||
} catch (_) {
|
||||
// gracefully ignore not being in a folder w/ a package.json
|
||||
}
|
||||
|
||||
if (best.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const suggestion =
|
||||
best.length === 1
|
||||
? `\n\nDid you mean this?\n${best[0]}`
|
||||
: `\n\nDid you mean one of these?\n${best.slice(0, 3).join('\n')}`
|
||||
return suggestion
|
||||
}
|
||||
module.exports = didYouMean
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
const { inspect } = require('util')
|
||||
const npmlog = require('npmlog')
|
||||
const log = require('./log-shim.js')
|
||||
const { explain } = require('./explain-eresolve.js')
|
||||
|
||||
const _logHandler = Symbol('logHandler')
|
||||
const _eresolveWarn = Symbol('eresolveWarn')
|
||||
const _log = Symbol('log')
|
||||
const _npmlog = Symbol('npmlog')
|
||||
|
||||
class Display {
|
||||
constructor () {
|
||||
// pause by default until config is loaded
|
||||
this.on()
|
||||
log.pause()
|
||||
}
|
||||
|
||||
on () {
|
||||
process.on('log', this[_logHandler])
|
||||
}
|
||||
|
||||
off () {
|
||||
process.off('log', this[_logHandler])
|
||||
// Unbalanced calls to enable/disable progress
|
||||
// will leave change listeners on the tracker
|
||||
// This pretty much only happens in tests but
|
||||
// this removes the event emitter listener warnings
|
||||
log.tracker.removeAllListeners()
|
||||
}
|
||||
|
||||
load (config) {
|
||||
const {
|
||||
color,
|
||||
timing,
|
||||
loglevel,
|
||||
unicode,
|
||||
progress,
|
||||
silent,
|
||||
heading = 'npm',
|
||||
} = config
|
||||
|
||||
// XXX: decouple timing from loglevel
|
||||
if (timing && loglevel === 'notice') {
|
||||
log.level = 'timing'
|
||||
} else {
|
||||
log.level = loglevel
|
||||
}
|
||||
|
||||
log.heading = heading
|
||||
|
||||
if (color) {
|
||||
log.enableColor()
|
||||
} else {
|
||||
log.disableColor()
|
||||
}
|
||||
|
||||
if (unicode) {
|
||||
log.enableUnicode()
|
||||
} else {
|
||||
log.disableUnicode()
|
||||
}
|
||||
|
||||
// if it's silent, don't show progress
|
||||
if (progress && !silent) {
|
||||
log.enableProgress()
|
||||
} else {
|
||||
log.disableProgress()
|
||||
}
|
||||
|
||||
// Resume displaying logs now that we have config
|
||||
log.resume()
|
||||
}
|
||||
|
||||
log (...args) {
|
||||
this[_logHandler](...args)
|
||||
}
|
||||
|
||||
[_logHandler] = (level, ...args) => {
|
||||
try {
|
||||
this[_log](level, ...args)
|
||||
} catch (ex) {
|
||||
try {
|
||||
// if it crashed once, it might again!
|
||||
this[_npmlog]('verbose', `attempt to log ${inspect(args)} crashed`, ex)
|
||||
} catch (ex2) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`attempt to log ${inspect(args)} crashed`, ex, ex2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[_log] (...args) {
|
||||
return this[_eresolveWarn](...args) || this[_npmlog](...args)
|
||||
}
|
||||
|
||||
// Explicitly call these on npmlog and not log shim
|
||||
// This is the final place we should call npmlog before removing it.
|
||||
[_npmlog] (level, ...args) {
|
||||
npmlog[level](...args)
|
||||
}
|
||||
|
||||
// Also (and this is a really inexcusable kludge), we patch the
|
||||
// log.warn() method so that when we see a peerDep override
|
||||
// explanation from Arborist, we can replace the object with a
|
||||
// highly abbreviated explanation of what's being overridden.
|
||||
[_eresolveWarn] (level, heading, message, expl) {
|
||||
if (level === 'warn' &&
|
||||
heading === 'ERESOLVE' &&
|
||||
expl && typeof expl === 'object'
|
||||
) {
|
||||
this[_npmlog](level, heading, message)
|
||||
this[_npmlog](level, '', explain(expl, log.useColor(), 2))
|
||||
// Return true to short circuit other log in chain
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Display
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
const { format } = require('util')
|
||||
const { resolve } = require('path')
|
||||
const nameValidator = require('validate-npm-package-name')
|
||||
const replaceInfo = require('./replace-info.js')
|
||||
const { report } = require('./explain-eresolve.js')
|
||||
const log = require('./log-shim')
|
||||
|
||||
module.exports = (er, npm) => {
|
||||
const short = []
|
||||
const detail = []
|
||||
|
||||
if (er.message) {
|
||||
er.message = replaceInfo(er.message)
|
||||
}
|
||||
if (er.stack) {
|
||||
er.stack = replaceInfo(er.stack)
|
||||
}
|
||||
|
||||
switch (er.code) {
|
||||
case 'ERESOLVE':
|
||||
short.push(['ERESOLVE', er.message])
|
||||
detail.push(['', ''])
|
||||
// XXX(display): error messages are logged so we use the logColor since that is based
|
||||
// on stderr. This should be handled solely by the display layer so it could also be
|
||||
// printed to stdout if necessary.
|
||||
detail.push(['', report(er, !!npm.logColor, resolve(npm.cache, 'eresolve-report.txt'))])
|
||||
break
|
||||
|
||||
case 'ENOLOCK': {
|
||||
const cmd = npm.command || ''
|
||||
short.push([cmd, 'This command requires an existing lockfile.'])
|
||||
detail.push([cmd, 'Try creating one first with: npm i --package-lock-only'])
|
||||
detail.push([cmd, `Original error: ${er.message}`])
|
||||
break
|
||||
}
|
||||
|
||||
case 'ENOAUDIT':
|
||||
short.push(['audit', er.message])
|
||||
break
|
||||
|
||||
case 'ECONNREFUSED':
|
||||
short.push(['', er])
|
||||
detail.push([
|
||||
'',
|
||||
[
|
||||
'\nIf you are behind a proxy, please make sure that the',
|
||||
"'proxy' config is set properly. See: 'npm help config'",
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EACCES':
|
||||
case 'EPERM': {
|
||||
const isCachePath =
|
||||
typeof er.path === 'string' &&
|
||||
npm.config.loaded &&
|
||||
er.path.startsWith(npm.config.get('cache'))
|
||||
const isCacheDest =
|
||||
typeof er.dest === 'string' &&
|
||||
npm.config.loaded &&
|
||||
er.dest.startsWith(npm.config.get('cache'))
|
||||
|
||||
const { isWindows } = require('./is-windows.js')
|
||||
|
||||
if (!isWindows && (isCachePath || isCacheDest)) {
|
||||
// user probably doesn't need this, but still add it to the debug log
|
||||
log.verbose(er.stack)
|
||||
short.push([
|
||||
'',
|
||||
[
|
||||
'',
|
||||
'Your cache folder contains root-owned files, due to a bug in',
|
||||
'previous versions of npm which has since been addressed.',
|
||||
'',
|
||||
'To permanently fix this problem, please run:',
|
||||
` sudo chown -R ${process.getuid()}:${process.getgid()} ${JSON.stringify(
|
||||
npm.config.get('cache')
|
||||
)}`,
|
||||
].join('\n'),
|
||||
])
|
||||
} else {
|
||||
short.push(['', er])
|
||||
detail.push([
|
||||
'',
|
||||
[
|
||||
'\nThe operation was rejected by your operating system.',
|
||||
isWindows
|
||||
/* eslint-disable-next-line max-len */
|
||||
? "It's possible that the file was already in use (by a text editor or antivirus),\n" +
|
||||
'or that you lack permissions to access it.'
|
||||
/* eslint-disable-next-line max-len */
|
||||
: 'It is likely you do not have the permissions to access this file as the current user',
|
||||
'\nIf you believe this might be a permissions issue, please double-check the',
|
||||
'permissions of the file and its containing directories, or try running',
|
||||
'the command again as root/Administrator.',
|
||||
].join('\n'),
|
||||
])
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'ENOGIT':
|
||||
short.push(['', er.message])
|
||||
detail.push([
|
||||
'',
|
||||
['', 'Failed using git.', 'Please check if you have git installed and in your PATH.'].join(
|
||||
'\n'
|
||||
),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EJSONPARSE':
|
||||
// Check whether we ran into a conflict in our own package.json
|
||||
if (er.path === resolve(npm.prefix, 'package.json')) {
|
||||
const { isDiff } = require('parse-conflict-json')
|
||||
const txt = require('fs').readFileSync(er.path, 'utf8').replace(/\r\n/g, '\n')
|
||||
if (isDiff(txt)) {
|
||||
detail.push([
|
||||
'',
|
||||
[
|
||||
'Merge conflict detected in your package.json.',
|
||||
'',
|
||||
'Please resolve the package.json conflict and retry.',
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
}
|
||||
}
|
||||
short.push(['JSON.parse', er.message])
|
||||
detail.push([
|
||||
'JSON.parse',
|
||||
[
|
||||
'Failed to parse JSON data.',
|
||||
'Note: package.json must be actual JSON, not just JavaScript.',
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EOTP':
|
||||
case 'E401':
|
||||
// E401 is for places where we accidentally neglect OTP stuff
|
||||
if (er.code === 'EOTP' || /one-time pass/.test(er.message)) {
|
||||
short.push(['', 'This operation requires a one-time password from your authenticator.'])
|
||||
detail.push([
|
||||
'',
|
||||
[
|
||||
'You can provide a one-time password by passing --otp=<code> to the command you ran.',
|
||||
'If you already provided a one-time password then it is likely that you either typoed',
|
||||
'it, or it timed out. Please try again.',
|
||||
].join('\n'),
|
||||
])
|
||||
} else {
|
||||
// npm ERR! code E401
|
||||
// npm ERR! Unable to authenticate, need: Basic
|
||||
const auth =
|
||||
!er.headers || !er.headers['www-authenticate']
|
||||
? []
|
||||
: er.headers['www-authenticate'].map(au => au.split(/[,\s]+/))[0]
|
||||
|
||||
if (auth.includes('Bearer')) {
|
||||
short.push([
|
||||
'',
|
||||
'Unable to authenticate, your authentication token seems to be invalid.',
|
||||
])
|
||||
detail.push([
|
||||
'',
|
||||
['To correct this please trying logging in again with:', ' npm login'].join('\n'),
|
||||
])
|
||||
} else if (auth.includes('Basic')) {
|
||||
short.push(['', 'Incorrect or missing password.'])
|
||||
detail.push([
|
||||
'',
|
||||
[
|
||||
'If you were trying to login, change your password, create an',
|
||||
'authentication token or enable two-factor authentication then',
|
||||
'that means you likely typed your password in incorrectly.',
|
||||
'Please try again, or recover your password at:',
|
||||
' https://www.npmjs.com/forgot',
|
||||
'',
|
||||
'If you were doing some other operation then your saved credentials are',
|
||||
'probably out of date. To correct this please try logging in again with:',
|
||||
' npm login',
|
||||
].join('\n'),
|
||||
])
|
||||
} else {
|
||||
short.push(['', er.message || er])
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'E404':
|
||||
// There's no need to have 404 in the message as well.
|
||||
short.push(['404', er.message.replace(/^404\s+/, '')])
|
||||
if (er.pkgid && er.pkgid !== '-') {
|
||||
const pkg = er.pkgid.replace(/(?!^)@.*$/, '')
|
||||
|
||||
detail.push(['404', ''])
|
||||
detail.push(['404', '', `'${replaceInfo(er.pkgid)}' is not in this registry.`])
|
||||
|
||||
const valResult = nameValidator(pkg)
|
||||
|
||||
if (!valResult.validForNewPackages) {
|
||||
detail.push(['404', 'This package name is not valid, because', ''])
|
||||
|
||||
const errorsArray = [...(valResult.errors || []), ...(valResult.warnings || [])]
|
||||
errorsArray.forEach((item, idx) => detail.push(['404', ' ' + (idx + 1) + '. ' + item]))
|
||||
}
|
||||
|
||||
detail.push(['404', '\nNote that you can also install from a'])
|
||||
detail.push(['404', 'tarball, folder, http url, or git url.'])
|
||||
}
|
||||
break
|
||||
|
||||
case 'EPUBLISHCONFLICT':
|
||||
short.push(['publish fail', 'Cannot publish over existing version.'])
|
||||
detail.push(['publish fail', "Update the 'version' field in package.json and try again."])
|
||||
detail.push(['publish fail', ''])
|
||||
detail.push(['publish fail', 'To automatically increment version numbers, see:'])
|
||||
detail.push(['publish fail', ' npm help version'])
|
||||
break
|
||||
|
||||
case 'EISGIT':
|
||||
short.push(['git', er.message])
|
||||
short.push(['git', ' ' + er.path])
|
||||
detail.push([
|
||||
'git',
|
||||
['Refusing to remove it. Update manually,', 'or move it out of the way first.'].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EBADPLATFORM': {
|
||||
const validOs =
|
||||
er.required && er.required.os && er.required.os.join
|
||||
? er.required.os.join(',')
|
||||
: er.required.os
|
||||
const validArch =
|
||||
er.required && er.required.cpu && er.required.cpu.join
|
||||
? er.required.cpu.join(',')
|
||||
: er.required.cpu
|
||||
const expected = { os: validOs, arch: validArch }
|
||||
const actual = { os: process.platform, arch: process.arch }
|
||||
short.push([
|
||||
'notsup',
|
||||
[
|
||||
format(
|
||||
'Unsupported platform for %s: wanted %j (current: %j)',
|
||||
er.pkgid,
|
||||
expected,
|
||||
actual
|
||||
),
|
||||
].join('\n'),
|
||||
])
|
||||
detail.push([
|
||||
'notsup',
|
||||
[
|
||||
'Valid OS: ' + validOs,
|
||||
'Valid Arch: ' + validArch,
|
||||
'Actual OS: ' + process.platform,
|
||||
'Actual Arch: ' + process.arch,
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
}
|
||||
|
||||
case 'EEXIST':
|
||||
short.push(['', er.message])
|
||||
short.push(['', 'File exists: ' + (er.dest || er.path)])
|
||||
detail.push(['', 'Remove the existing file and try again, or run npm'])
|
||||
detail.push(['', 'with --force to overwrite files recklessly.'])
|
||||
break
|
||||
|
||||
case 'ENEEDAUTH':
|
||||
short.push(['need auth', er.message])
|
||||
detail.push(['need auth', 'You need to authorize this machine using `npm adduser`'])
|
||||
break
|
||||
|
||||
case 'ECONNRESET':
|
||||
case 'ENOTFOUND':
|
||||
case 'ETIMEDOUT':
|
||||
case 'ERR_SOCKET_TIMEOUT':
|
||||
case 'EAI_FAIL':
|
||||
short.push(['network', er.message])
|
||||
detail.push([
|
||||
'network',
|
||||
[
|
||||
'This is a problem related to network connectivity.',
|
||||
'In most cases you are behind a proxy or have bad network settings.',
|
||||
'\nIf you are behind a proxy, please make sure that the',
|
||||
"'proxy' config is set properly. See: 'npm help config'",
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'ETARGET':
|
||||
short.push(['notarget', er.message])
|
||||
detail.push([
|
||||
'notarget',
|
||||
[
|
||||
'In most cases you or one of your dependencies are requesting',
|
||||
"a package version that doesn't exist.",
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'E403':
|
||||
short.push(['403', er.message])
|
||||
detail.push([
|
||||
'403',
|
||||
[
|
||||
'In most cases, you or one of your dependencies are requesting',
|
||||
'a package version that is forbidden by your security policy, or',
|
||||
'on a server you do not have access to.',
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EBADENGINE':
|
||||
short.push(['engine', er.message])
|
||||
short.push(['engine', 'Not compatible with your version of node/npm: ' + er.pkgid])
|
||||
detail.push([
|
||||
'notsup',
|
||||
[
|
||||
'Not compatible with your version of node/npm: ' + er.pkgid,
|
||||
'Required: ' + JSON.stringify(er.required),
|
||||
'Actual: ' +
|
||||
JSON.stringify({
|
||||
npm: npm.version,
|
||||
node: npm.config.loaded ? npm.config.get('node-version') : process.version,
|
||||
}),
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'ENOSPC':
|
||||
short.push(['nospc', er.message])
|
||||
detail.push([
|
||||
'nospc',
|
||||
[
|
||||
'There appears to be insufficient space on your system to finish.',
|
||||
'Clear up some disk space and try again.',
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EROFS':
|
||||
short.push(['rofs', er.message])
|
||||
detail.push([
|
||||
'rofs',
|
||||
[
|
||||
'Often virtualized file systems, or other file systems',
|
||||
"that don't support symlinks, give this error.",
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'ENOENT':
|
||||
short.push(['enoent', er.message])
|
||||
detail.push([
|
||||
'enoent',
|
||||
[
|
||||
'This is related to npm not being able to find a file.',
|
||||
er.file ? "\nCheck if the file '" + er.file + "' is present." : '',
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
case 'EMISSINGARG':
|
||||
case 'EUNKNOWNTYPE':
|
||||
case 'EINVALIDTYPE':
|
||||
case 'ETOOMANYARGS':
|
||||
short.push(['typeerror', er.stack])
|
||||
detail.push([
|
||||
'typeerror',
|
||||
[
|
||||
'This is an error with npm itself. Please report this error at:',
|
||||
' https://github.com/npm/cli/issues',
|
||||
].join('\n'),
|
||||
])
|
||||
break
|
||||
|
||||
default:
|
||||
short.push(['', er.message || er])
|
||||
if (er.signal) {
|
||||
detail.push(['signal', er.signal])
|
||||
}
|
||||
|
||||
if (er.cmd && Array.isArray(er.args)) {
|
||||
detail.push(['command', ...[er.cmd, ...er.args.map(replaceInfo)]])
|
||||
}
|
||||
|
||||
if (er.stdout) {
|
||||
detail.push(['', er.stdout.trim()])
|
||||
}
|
||||
|
||||
if (er.stderr) {
|
||||
detail.push(['', er.stderr.trim()])
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
return { summary: short, detail: detail }
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
const os = require('os')
|
||||
|
||||
const log = require('./log-shim.js')
|
||||
const errorMessage = require('./error-message.js')
|
||||
const replaceInfo = require('./replace-info.js')
|
||||
|
||||
const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n')
|
||||
const indent = (val) => Array.isArray(val) ? val.map(v => indent(v)) : ` ${val}`
|
||||
|
||||
let npm = null // set by the cli
|
||||
let exitHandlerCalled = false
|
||||
let showLogFileError = false
|
||||
|
||||
process.on('exit', code => {
|
||||
log.disableProgress()
|
||||
|
||||
// process.emit is synchronous, so the timeEnd handler will run before the
|
||||
// unfinished timer check below
|
||||
process.emit('timeEnd', 'npm')
|
||||
|
||||
const hasNpm = !!npm
|
||||
const hasLoadedNpm = hasNpm && npm.config.loaded
|
||||
|
||||
// Unfinished timers can be read before config load
|
||||
if (hasNpm) {
|
||||
for (const [name, timer] of npm.unfinishedTimers) {
|
||||
log.verbose('unfinished npm timer', name, timer)
|
||||
}
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
log.info('ok')
|
||||
} else {
|
||||
log.verbose('code', code)
|
||||
}
|
||||
|
||||
if (!exitHandlerCalled) {
|
||||
process.exitCode = code || 1
|
||||
log.error('', 'Exit handler never called!')
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('')
|
||||
log.error('', 'This is an error with npm itself. Please report this error at:')
|
||||
log.error('', ' <https://github.com/npm/cli/issues>')
|
||||
showLogFileError = true
|
||||
}
|
||||
|
||||
// npm must be loaded to know where the log file was written
|
||||
if (hasLoadedNpm) {
|
||||
// write the timing file now, this might do nothing based on the configs set.
|
||||
// we need to call it here in case it errors so we dont tell the user
|
||||
// about a timing file that doesn't exist
|
||||
npm.writeTimingFile()
|
||||
|
||||
const logsDir = npm.logsDir
|
||||
const logFiles = npm.logFiles
|
||||
|
||||
const timingDir = npm.timingDir
|
||||
const timingFile = npm.timingFile
|
||||
|
||||
const timing = npm.config.get('timing')
|
||||
const logsMax = npm.config.get('logs-max')
|
||||
|
||||
// Determine whether to show log file message and why it is
|
||||
// being shown since in timing mode we always show the log file message
|
||||
const logMethod = showLogFileError ? 'error' : timing ? 'info' : null
|
||||
|
||||
if (logMethod) {
|
||||
if (!npm.silent) {
|
||||
// just a line break if not in silent mode
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('')
|
||||
}
|
||||
|
||||
const message = []
|
||||
|
||||
if (timingFile) {
|
||||
message.push('Timing info written to:', indent(timingFile))
|
||||
} else if (timing) {
|
||||
message.push(
|
||||
`The timing file was not written due to an error writing to the directory: ${timingDir}`
|
||||
)
|
||||
}
|
||||
|
||||
if (logFiles.length) {
|
||||
message.push('A complete log of this run can be found in:', ...indent(logFiles))
|
||||
} else if (logsMax <= 0) {
|
||||
// user specified no log file
|
||||
message.push(`Log files were not written due to the config logs-max=${logsMax}`)
|
||||
} else {
|
||||
// could be an error writing to the directory
|
||||
message.push(
|
||||
`Log files were not written due to an error writing to the directory: ${logsDir}`,
|
||||
'You can rerun the command with `--loglevel=verbose` to see the logs in your terminal'
|
||||
)
|
||||
}
|
||||
|
||||
log[logMethod]('', message.join('\n'))
|
||||
}
|
||||
|
||||
// This removes any listeners npm setup, mostly for tests to avoid max listener warnings
|
||||
npm.unload()
|
||||
}
|
||||
|
||||
// these are needed for the tests to have a clean slate in each test case
|
||||
exitHandlerCalled = false
|
||||
showLogFileError = false
|
||||
})
|
||||
|
||||
const exitHandler = err => {
|
||||
exitHandlerCalled = true
|
||||
|
||||
log.disableProgress()
|
||||
|
||||
const hasNpm = !!npm
|
||||
const hasLoadedNpm = hasNpm && npm.config.loaded
|
||||
|
||||
if (!hasNpm) {
|
||||
err = err || new Error('Exit prior to setting npm in exit handler')
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err.stack || err.message)
|
||||
return process.exit(1)
|
||||
}
|
||||
|
||||
if (!hasLoadedNpm) {
|
||||
err = err || new Error('Exit prior to config file resolving.')
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err.stack || err.message)
|
||||
}
|
||||
|
||||
// only show the notification if it finished.
|
||||
if (typeof npm.updateNotification === 'string') {
|
||||
const { level } = log
|
||||
log.level = 'notice'
|
||||
log.notice('', npm.updateNotification)
|
||||
log.level = level
|
||||
}
|
||||
|
||||
let exitCode
|
||||
let noLogMessage
|
||||
|
||||
if (err) {
|
||||
exitCode = 1
|
||||
// if we got a command that just shells out to something else, then it
|
||||
// will presumably print its own errors and exit with a proper status
|
||||
// code if there's a problem. If we got an error with a code=0, then...
|
||||
// something else went wrong along the way, so maybe an npm problem?
|
||||
const isShellout = npm.commandInstance && npm.commandInstance.constructor.isShellout
|
||||
const quietShellout = isShellout && typeof err.code === 'number' && err.code
|
||||
if (quietShellout) {
|
||||
exitCode = err.code
|
||||
noLogMessage = true
|
||||
} else if (typeof err === 'string') {
|
||||
// XXX: we should stop throwing strings
|
||||
log.error('', err)
|
||||
noLogMessage = true
|
||||
} else if (!(err instanceof Error)) {
|
||||
log.error('weird error', err)
|
||||
noLogMessage = true
|
||||
} else {
|
||||
if (!err.code) {
|
||||
const matchErrorCode = err.message.match(/^(?:Error: )?(E[A-Z]+)/)
|
||||
err.code = matchErrorCode && matchErrorCode[1]
|
||||
}
|
||||
|
||||
for (const k of ['type', 'stack', 'statusCode', 'pkgid']) {
|
||||
const v = err[k]
|
||||
if (v) {
|
||||
log.verbose(k, replaceInfo(v))
|
||||
}
|
||||
}
|
||||
|
||||
log.verbose('cwd', process.cwd())
|
||||
log.verbose('', os.type() + ' ' + os.release())
|
||||
log.verbose('node', process.version)
|
||||
log.verbose('npm ', 'v' + npm.version)
|
||||
|
||||
for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) {
|
||||
const v = err[k]
|
||||
if (v) {
|
||||
log.error(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
const msg = errorMessage(err, npm)
|
||||
for (const errline of [...msg.summary, ...msg.detail]) {
|
||||
log.error(...errline)
|
||||
}
|
||||
|
||||
if (hasLoadedNpm && npm.config.get('json')) {
|
||||
const error = {
|
||||
error: {
|
||||
code: err.code,
|
||||
summary: messageText(msg.summary),
|
||||
detail: messageText(msg.detail),
|
||||
},
|
||||
}
|
||||
npm.outputError(JSON.stringify(error, null, 2))
|
||||
}
|
||||
|
||||
if (typeof err.errno === 'number') {
|
||||
exitCode = err.errno
|
||||
} else if (typeof err.code === 'number') {
|
||||
exitCode = err.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.verbose('exit', exitCode || 0)
|
||||
|
||||
showLogFileError = (hasLoadedNpm && npm.silent) || noLogMessage
|
||||
? false
|
||||
: !!exitCode
|
||||
|
||||
// explicitly call process.exit now so we don't hang on things like the
|
||||
// update notifier, also flush stdout/err beforehand because process.exit doesn't
|
||||
// wait for that to happen.
|
||||
let flushed = 0
|
||||
const flush = [process.stderr, process.stdout]
|
||||
const exit = () => ++flushed === flush.length && process.exit(exitCode)
|
||||
flush.forEach((f) => f.write('', exit))
|
||||
}
|
||||
|
||||
module.exports = exitHandler
|
||||
module.exports.setNpm = n => (npm = n)
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
const chalk = require('chalk')
|
||||
const nocolor = {
|
||||
bold: s => s,
|
||||
dim: s => s,
|
||||
red: s => s,
|
||||
yellow: s => s,
|
||||
cyan: s => s,
|
||||
magenta: s => s,
|
||||
blue: s => s,
|
||||
green: s => s,
|
||||
}
|
||||
|
||||
const { relative } = require('path')
|
||||
|
||||
const explainNode = (node, depth, color) =>
|
||||
printNode(node, color) +
|
||||
explainDependents(node, depth, color) +
|
||||
explainLinksIn(node, depth, color)
|
||||
|
||||
const colorType = (type, color) => {
|
||||
const { red, yellow, cyan, magenta, blue, green } = color ? chalk : nocolor
|
||||
const style = type === 'extraneous' ? red
|
||||
: type === 'dev' ? yellow
|
||||
: type === 'optional' ? cyan
|
||||
: type === 'peer' ? magenta
|
||||
: type === 'bundled' ? blue
|
||||
: type === 'workspace' ? green
|
||||
: /* istanbul ignore next */ s => s
|
||||
return style(type)
|
||||
}
|
||||
|
||||
const printNode = (node, color) => {
|
||||
const {
|
||||
name,
|
||||
version,
|
||||
location,
|
||||
extraneous,
|
||||
dev,
|
||||
optional,
|
||||
peer,
|
||||
bundled,
|
||||
isWorkspace,
|
||||
} = node
|
||||
const { bold, dim, green } = color ? chalk : nocolor
|
||||
const extra = []
|
||||
if (extraneous) {
|
||||
extra.push(' ' + bold(colorType('extraneous', color)))
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
extra.push(' ' + bold(colorType('dev', color)))
|
||||
}
|
||||
|
||||
if (optional) {
|
||||
extra.push(' ' + bold(colorType('optional', color)))
|
||||
}
|
||||
|
||||
if (peer) {
|
||||
extra.push(' ' + bold(colorType('peer', color)))
|
||||
}
|
||||
|
||||
if (bundled) {
|
||||
extra.push(' ' + bold(colorType('bundled', color)))
|
||||
}
|
||||
|
||||
const pkgid = isWorkspace
|
||||
? green(`${name}@${version}`)
|
||||
: `${bold(name)}@${bold(version)}`
|
||||
|
||||
return `${pkgid}${extra.join('')}` +
|
||||
(location ? dim(`\n${location}`) : '')
|
||||
}
|
||||
|
||||
const explainLinksIn = ({ linksIn }, depth, color) => {
|
||||
if (!linksIn || !linksIn.length || depth <= 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const messages = linksIn.map(link => explainNode(link, depth - 1, color))
|
||||
const str = '\n' + messages.join('\n')
|
||||
return str.split('\n').join('\n ')
|
||||
}
|
||||
|
||||
const explainDependents = ({ name, dependents }, depth, color) => {
|
||||
if (!dependents || !dependents.length || depth <= 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const max = Math.ceil(depth / 2)
|
||||
const messages = dependents.slice(0, max)
|
||||
.map(edge => explainEdge(edge, depth, color))
|
||||
|
||||
// show just the names of the first 5 deps that overflowed the list
|
||||
if (dependents.length > max) {
|
||||
let len = 0
|
||||
const maxLen = 50
|
||||
const showNames = []
|
||||
for (let i = max; i < dependents.length; i++) {
|
||||
const { from: { name = 'the root project' } } = dependents[i]
|
||||
len += name.length
|
||||
if (len >= maxLen && i < dependents.length - 1) {
|
||||
showNames.push('...')
|
||||
break
|
||||
}
|
||||
showNames.push(name)
|
||||
}
|
||||
const show = `(${showNames.join(', ')})`
|
||||
messages.push(`${dependents.length - max} more ${show}`)
|
||||
}
|
||||
|
||||
const str = '\n' + messages.join('\n')
|
||||
return str.split('\n').join('\n ')
|
||||
}
|
||||
|
||||
const explainEdge = ({ name, type, bundled, from, spec }, depth, color) => {
|
||||
const { bold } = color ? chalk : nocolor
|
||||
const dep = type === 'workspace'
|
||||
? bold(relative(from.location, spec.slice('file:'.length)))
|
||||
: `${bold(name)}@"${bold(spec)}"`
|
||||
const fromMsg = ` from ${explainFrom(from, depth, color)}`
|
||||
|
||||
return (type === 'prod' ? '' : `${colorType(type, color)} `) +
|
||||
(bundled ? `${colorType('bundled', color)} ` : '') +
|
||||
`${dep}${fromMsg}`
|
||||
}
|
||||
|
||||
const explainFrom = (from, depth, color) => {
|
||||
if (!from.name && !from.version) {
|
||||
return 'the root project'
|
||||
}
|
||||
|
||||
return printNode(from, color) +
|
||||
explainDependents(from, depth - 1, color) +
|
||||
explainLinksIn(from, depth - 1, color)
|
||||
}
|
||||
|
||||
module.exports = { explainNode, printNode, explainEdge }
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// this is called when an ERESOLVE error is caught in the exit-handler,
|
||||
// or when there's a log.warn('eresolve', msg, explanation), to turn it
|
||||
// into a human-intelligible explanation of what's wrong and how to fix.
|
||||
const { writeFileSync } = require('fs')
|
||||
const { explainEdge, explainNode, printNode } = require('./explain-dep.js')
|
||||
|
||||
// expl is an explanation object that comes from Arborist. It looks like:
|
||||
// Depth is how far we want to want to descend into the object making a report.
|
||||
// The full report (ie, depth=Infinity) is always written to the cache folder
|
||||
// at ${cache}/eresolve-report.txt along with full json.
|
||||
const explain = (expl, color, depth) => {
|
||||
const { edge, dep, current, peerConflict, currentEdge } = expl
|
||||
|
||||
const out = []
|
||||
const whileInstalling = dep && dep.whileInstalling ||
|
||||
current && current.whileInstalling ||
|
||||
edge && edge.from && edge.from.whileInstalling
|
||||
if (whileInstalling) {
|
||||
out.push('While resolving: ' + printNode(whileInstalling, color))
|
||||
}
|
||||
|
||||
// it "should" be impossible for an ERESOLVE explanation to lack both
|
||||
// current and currentEdge, but better to have a less helpful error
|
||||
// than a crashing failure.
|
||||
if (current) {
|
||||
out.push('Found: ' + explainNode(current, depth, color))
|
||||
} else if (peerConflict && peerConflict.current) {
|
||||
out.push('Found: ' + explainNode(peerConflict.current, depth, color))
|
||||
} else if (currentEdge) {
|
||||
out.push('Found: ' + explainEdge(currentEdge, depth, color))
|
||||
} else /* istanbul ignore else - should always have one */ if (edge) {
|
||||
out.push('Found: ' + explainEdge(edge, depth, color))
|
||||
}
|
||||
|
||||
out.push('\nCould not resolve dependency:\n' +
|
||||
explainEdge(edge, depth, color))
|
||||
|
||||
if (peerConflict) {
|
||||
const heading = '\nConflicting peer dependency:'
|
||||
const pc = explainNode(peerConflict.peer, depth, color)
|
||||
out.push(heading + ' ' + pc)
|
||||
}
|
||||
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
// generate a full verbose report and tell the user how to fix it
|
||||
const report = (expl, color, fullReport) => {
|
||||
const orNoStrict = expl.strictPeerDeps ? '--no-strict-peer-deps, ' : ''
|
||||
const fix = `Fix the upstream dependency conflict, or retry
|
||||
this command with ${orNoStrict}--force, or --legacy-peer-deps
|
||||
to accept an incorrect (and potentially broken) dependency resolution.`
|
||||
|
||||
writeFileSync(fullReport, `# npm resolution error report
|
||||
|
||||
${new Date().toISOString()}
|
||||
|
||||
${explain(expl, false, Infinity)}
|
||||
|
||||
${fix}
|
||||
|
||||
Raw JSON explanation object:
|
||||
|
||||
${JSON.stringify(expl, null, 2)}
|
||||
`, 'utf8')
|
||||
|
||||
return explain(expl, color, 4) +
|
||||
`\n\n${fix}\n\nSee ${fullReport} for a full report.`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
explain,
|
||||
report,
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Convert bytes to printable output, for file reporting in tarballs
|
||||
// Only supports up to GB because that's way larger than anything the registry
|
||||
// supports anyways.
|
||||
|
||||
const formatBytes = (bytes, space = true) => {
|
||||
let spacer = ''
|
||||
if (space) {
|
||||
spacer = ' '
|
||||
}
|
||||
|
||||
if (bytes < 1000) {
|
||||
// B
|
||||
return `${bytes}${spacer}B`
|
||||
}
|
||||
|
||||
if (bytes < 1000000) {
|
||||
// kB
|
||||
return `${(bytes / 1000).toFixed(1)}${spacer}kB`
|
||||
}
|
||||
|
||||
if (bytes < 1000000000) {
|
||||
// MB
|
||||
return `${(bytes / 1000000).toFixed(1)}${spacer}MB`
|
||||
}
|
||||
|
||||
// GB
|
||||
return `${(bytes / 1000000000).toFixed(1)}${spacer}GB`
|
||||
}
|
||||
|
||||
module.exports = formatBytes
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
const Minipass = require('minipass')
|
||||
const columnify = require('columnify')
|
||||
|
||||
// This module consumes package data in the following format:
|
||||
//
|
||||
// {
|
||||
// name: String,
|
||||
// description: String,
|
||||
// maintainers: [{ username: String, email: String }],
|
||||
// keywords: String | [String],
|
||||
// version: String,
|
||||
// date: Date // can be null,
|
||||
// }
|
||||
//
|
||||
// The returned stream will format this package data
|
||||
// into a byte stream of formatted, displayable output.
|
||||
|
||||
module.exports = (opts) => {
|
||||
return opts.json ? new JSONOutputStream() : new TextOutputStream(opts)
|
||||
}
|
||||
|
||||
class JSONOutputStream extends Minipass {
|
||||
#didFirst = false
|
||||
|
||||
write (obj) {
|
||||
if (!this.#didFirst) {
|
||||
super.write('[\n')
|
||||
this.#didFirst = true
|
||||
} else {
|
||||
super.write('\n,\n')
|
||||
}
|
||||
|
||||
return super.write(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
end () {
|
||||
super.write(this.#didFirst ? ']\n' : '\n[]\n')
|
||||
super.end()
|
||||
}
|
||||
}
|
||||
|
||||
class TextOutputStream extends Minipass {
|
||||
constructor (opts) {
|
||||
super()
|
||||
this._opts = opts
|
||||
this._line = 0
|
||||
}
|
||||
|
||||
write (pkg) {
|
||||
return super.write(prettify(pkg, ++this._line, this._opts))
|
||||
}
|
||||
}
|
||||
|
||||
function prettify (data, num, opts) {
|
||||
var truncate = !opts.long
|
||||
|
||||
var pkg = normalizePackage(data, opts)
|
||||
|
||||
var columns = ['name', 'description', 'author', 'date', 'version', 'keywords']
|
||||
|
||||
if (opts.parseable) {
|
||||
return columns.map(function (col) {
|
||||
return pkg[col] && ('' + pkg[col]).replace(/\t/g, ' ')
|
||||
}).join('\t')
|
||||
}
|
||||
|
||||
// stdout in tap is never a tty
|
||||
/* istanbul ignore next */
|
||||
const maxWidth = process.stdout.isTTY ? process.stdout.getWindowSize()[0] : Infinity
|
||||
let output = columnify(
|
||||
[pkg],
|
||||
{
|
||||
include: columns,
|
||||
showHeaders: num <= 1,
|
||||
columnSplitter: ' | ',
|
||||
truncate: truncate,
|
||||
config: {
|
||||
name: { minWidth: 25, maxWidth: 25, truncate: false, truncateMarker: '' },
|
||||
description: { minWidth: 20, maxWidth: 20 },
|
||||
author: { minWidth: 15, maxWidth: 15 },
|
||||
date: { maxWidth: 11 },
|
||||
version: { minWidth: 8, maxWidth: 8 },
|
||||
keywords: { maxWidth: Infinity },
|
||||
},
|
||||
}
|
||||
).split('\n').map(line => line.slice(0, maxWidth)).join('\n')
|
||||
|
||||
if (opts.color) {
|
||||
output = highlightSearchTerms(output, opts.args)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
var colors = [31, 33, 32, 36, 34, 35]
|
||||
var cl = colors.length
|
||||
|
||||
function addColorMarker (str, arg, i) {
|
||||
var m = i % cl + 1
|
||||
var markStart = String.fromCharCode(m)
|
||||
var markEnd = String.fromCharCode(0)
|
||||
|
||||
if (arg.charAt(0) === '/') {
|
||||
return str.replace(
|
||||
new RegExp(arg.slice(1, -1), 'gi'),
|
||||
bit => markStart + bit + markEnd
|
||||
)
|
||||
}
|
||||
|
||||
// just a normal string, do the split/map thing
|
||||
var pieces = str.toLowerCase().split(arg.toLowerCase())
|
||||
var p = 0
|
||||
|
||||
return pieces.map(function (piece) {
|
||||
piece = str.slice(p, p + piece.length)
|
||||
var mark = markStart +
|
||||
str.slice(p + piece.length, p + piece.length + arg.length) +
|
||||
markEnd
|
||||
p += piece.length + arg.length
|
||||
return piece + mark
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function colorize (line) {
|
||||
for (var i = 0; i < cl; i++) {
|
||||
var m = i + 1
|
||||
var color = '\u001B[' + colors[i] + 'm'
|
||||
line = line.split(String.fromCharCode(m)).join(color)
|
||||
}
|
||||
var uncolor = '\u001B[0m'
|
||||
return line.split('\u0000').join(uncolor)
|
||||
}
|
||||
|
||||
function highlightSearchTerms (str, terms) {
|
||||
terms.forEach(function (arg, i) {
|
||||
str = addColorMarker(str, arg, i)
|
||||
})
|
||||
|
||||
return colorize(str).trim()
|
||||
}
|
||||
|
||||
function normalizePackage (data, opts) {
|
||||
return {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
author: data.maintainers.map((m) => `=${m.username}`).join(' '),
|
||||
keywords: Array.isArray(data.keywords)
|
||||
? data.keywords.join(' ')
|
||||
: typeof data.keywords === 'string'
|
||||
? data.keywords.replace(/[,\s]+/, ' ')
|
||||
: '',
|
||||
version: data.version,
|
||||
date: (data.date &&
|
||||
(data.date.toISOString() // remove time
|
||||
.split('T').join(' ')
|
||||
.replace(/:[0-9]{2}\.[0-9]{3}Z$/, ''))
|
||||
.slice(0, -5)) ||
|
||||
'prehistoric',
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
const npmFetch = require('npm-registry-fetch')
|
||||
|
||||
module.exports = async (npm, opts) => {
|
||||
const { registry } = opts
|
||||
|
||||
// First, check if we have a user/pass-based auth
|
||||
const creds = npm.config.getCredentialsByURI(registry)
|
||||
if (creds.username) {
|
||||
return creds.username
|
||||
}
|
||||
|
||||
// No username, but we have other credentials; fetch the username from registry
|
||||
if (creds.token || creds.certfile && creds.keyfile) {
|
||||
const registryData = await npmFetch.json('/-/whoami', { ...opts })
|
||||
return registryData.username
|
||||
}
|
||||
|
||||
// At this point, even if they have a credentials object, it doesn't have a
|
||||
// valid token.
|
||||
throw Object.assign(
|
||||
new Error('This command requires you to be logged in.'),
|
||||
{ code: 'ENEEDAUTH' }
|
||||
)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
const isWindows = process.platform === 'win32'
|
||||
const isWindowsShell = isWindows &&
|
||||
!/^MINGW(32|64)$/.test(process.env.MSYSTEM) && process.env.TERM !== 'cygwin'
|
||||
|
||||
exports.isWindows = isWindows
|
||||
exports.isWindowsShell = isWindowsShell
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { format, promisify } = require('util')
|
||||
const rimraf = promisify(require('rimraf'))
|
||||
const glob = promisify(require('glob'))
|
||||
const MiniPass = require('minipass')
|
||||
const fsMiniPass = require('fs-minipass')
|
||||
const fs = require('@npmcli/fs')
|
||||
const log = require('./log-shim')
|
||||
|
||||
const padZero = (n, length) => n.toString().padStart(length.toString().length, '0')
|
||||
const globify = pattern => pattern.split('\\').join('/')
|
||||
|
||||
const _logHandler = Symbol('logHandler')
|
||||
const _formatLogItem = Symbol('formatLogItem')
|
||||
const _getLogFilePath = Symbol('getLogFilePath')
|
||||
const _openLogFile = Symbol('openLogFile')
|
||||
const _cleanLogs = Symbol('cleanlogs')
|
||||
const _endStream = Symbol('endStream')
|
||||
const _isBuffered = Symbol('isBuffered')
|
||||
|
||||
class LogFiles {
|
||||
// If we write multiple log files we want them all to have the same
|
||||
// identifier for sorting and matching purposes
|
||||
#logId = null
|
||||
|
||||
// Default to a plain minipass stream so we can buffer
|
||||
// initial writes before we know the cache location
|
||||
#logStream = null
|
||||
|
||||
// We cap log files at a certain number of log events per file.
|
||||
// Note that each log event can write more than one line to the
|
||||
// file. Then we rotate log files once this number of events is reached
|
||||
#MAX_LOGS_PER_FILE = null
|
||||
|
||||
// Now that we write logs continuously we need to have a backstop
|
||||
// here for infinite loops that still log. This is also partially handled
|
||||
// by the config.get('max-files') option, but this is a failsafe to
|
||||
// prevent runaway log file creation
|
||||
#MAX_FILES_PER_PROCESS = null
|
||||
|
||||
#fileLogCount = 0
|
||||
#totalLogCount = 0
|
||||
#dir = null
|
||||
#logsMax = null
|
||||
#files = []
|
||||
|
||||
constructor ({
|
||||
maxLogsPerFile = 50_000,
|
||||
maxFilesPerProcess = 5,
|
||||
} = {}) {
|
||||
this.#logId = LogFiles.logId(new Date())
|
||||
this.#MAX_LOGS_PER_FILE = maxLogsPerFile
|
||||
this.#MAX_FILES_PER_PROCESS = maxFilesPerProcess
|
||||
this.on()
|
||||
}
|
||||
|
||||
static logId (d) {
|
||||
return d.toISOString().replace(/[.:]/g, '_')
|
||||
}
|
||||
|
||||
static format (count, level, title, ...args) {
|
||||
let prefix = `${count} ${level}`
|
||||
if (title) {
|
||||
prefix += ` ${title}`
|
||||
}
|
||||
|
||||
return format(...args)
|
||||
.split(/\r?\n/)
|
||||
.reduce((lines, line) =>
|
||||
lines += prefix + (line ? ' ' : '') + line + os.EOL,
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
on () {
|
||||
this.#logStream = new MiniPass()
|
||||
process.on('log', this[_logHandler])
|
||||
}
|
||||
|
||||
off () {
|
||||
process.off('log', this[_logHandler])
|
||||
this[_endStream]()
|
||||
}
|
||||
|
||||
load ({ dir, logsMax = Infinity } = {}) {
|
||||
// dir is user configurable and is required to exist so
|
||||
// this can error if the dir is missing or not configured correctly
|
||||
this.#dir = dir
|
||||
this.#logsMax = logsMax
|
||||
|
||||
// Log stream has already ended
|
||||
if (!this.#logStream) {
|
||||
return
|
||||
}
|
||||
|
||||
log.verbose('logfile', `logs-max:${logsMax} dir:${dir}`)
|
||||
|
||||
// Pipe our initial stream to our new file stream and
|
||||
// set that as the new log logstream for future writes
|
||||
// if logs max is 0 then the user does not want a log file
|
||||
if (this.#logsMax > 0) {
|
||||
const initialFile = this[_openLogFile]()
|
||||
if (initialFile) {
|
||||
this.#logStream = this.#logStream.pipe(initialFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Kickoff cleaning process, even if we aren't writing a logfile.
|
||||
// This is async but it will always ignore the current logfile
|
||||
// Return the result so it can be awaited in tests
|
||||
return this[_cleanLogs]()
|
||||
}
|
||||
|
||||
log (...args) {
|
||||
this[_logHandler](...args)
|
||||
}
|
||||
|
||||
get files () {
|
||||
return this.#files
|
||||
}
|
||||
|
||||
get [_isBuffered] () {
|
||||
return this.#logStream instanceof MiniPass
|
||||
}
|
||||
|
||||
[_endStream] (output) {
|
||||
if (this.#logStream) {
|
||||
this.#logStream.end(output)
|
||||
this.#logStream = null
|
||||
}
|
||||
}
|
||||
|
||||
[_logHandler] = (level, ...args) => {
|
||||
// Ignore pause and resume events since we
|
||||
// write everything to the log file
|
||||
if (level === 'pause' || level === 'resume') {
|
||||
return
|
||||
}
|
||||
|
||||
// If the stream is ended then do nothing
|
||||
if (!this.#logStream) {
|
||||
return
|
||||
}
|
||||
|
||||
const logOutput = this[_formatLogItem](level, ...args)
|
||||
|
||||
if (this[_isBuffered]) {
|
||||
// Cant do anything but buffer the output if we dont
|
||||
// have a file stream yet
|
||||
this.#logStream.write(logOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Open a new log file if we've written too many logs to this one
|
||||
if (this.#fileLogCount >= this.#MAX_LOGS_PER_FILE) {
|
||||
// Write last chunk to the file and close it
|
||||
this[_endStream](logOutput)
|
||||
if (this.#files.length >= this.#MAX_FILES_PER_PROCESS) {
|
||||
// but if its way too many then we just stop listening
|
||||
this.off()
|
||||
} else {
|
||||
// otherwise we are ready for a new file for the next event
|
||||
this.#logStream = this[_openLogFile]()
|
||||
}
|
||||
} else {
|
||||
this.#logStream.write(logOutput)
|
||||
}
|
||||
}
|
||||
|
||||
[_formatLogItem] (...args) {
|
||||
this.#fileLogCount += 1
|
||||
return LogFiles.format(this.#totalLogCount++, ...args)
|
||||
}
|
||||
|
||||
[_getLogFilePath] (count = '') {
|
||||
return path.resolve(this.#dir, `${this.#logId}-debug-${count}.log`)
|
||||
}
|
||||
|
||||
[_openLogFile] () {
|
||||
// Count in filename will be 0 indexed
|
||||
const count = this.#files.length
|
||||
|
||||
try {
|
||||
// Pad with zeros so that our log files are always sorted properly
|
||||
// We never want to write files ending in `-9.log` and `-10.log` because
|
||||
// log file cleaning is done by deleting the oldest so in this example
|
||||
// `-10.log` would be deleted next
|
||||
const f = this[_getLogFilePath](padZero(count, this.#MAX_FILES_PER_PROCESS))
|
||||
// Some effort was made to make the async, but we need to write logs
|
||||
// during process.on('exit') which has to be synchronous. So in order
|
||||
// to never drop log messages, it is easiest to make it sync all the time
|
||||
// and this was measured to be about 1.5% slower for 40k lines of output
|
||||
const logStream = fs.withOwnerSync(
|
||||
f,
|
||||
() => new fsMiniPass.WriteStreamSync(f, { flags: 'a' }),
|
||||
{ owner: 'inherit' }
|
||||
)
|
||||
if (count > 0) {
|
||||
// Reset file log count if we are opening
|
||||
// after our first file
|
||||
this.#fileLogCount = 0
|
||||
}
|
||||
this.#files.push(logStream.path)
|
||||
return logStream
|
||||
} catch (e) {
|
||||
// If the user has a readonly logdir then we don't want to
|
||||
// warn this on every command so it should be verbose
|
||||
log.verbose('logfile', `could not be created: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
async [_cleanLogs] () {
|
||||
// module to clean out the old log files
|
||||
// this is a best-effort attempt. if a rm fails, we just
|
||||
// log a message about it and move on. We do return a
|
||||
// Promise that succeeds when we've tried to delete everything,
|
||||
// just for the benefit of testing this function properly.
|
||||
|
||||
try {
|
||||
const logPath = this[_getLogFilePath]()
|
||||
const logGlob = path.join(path.dirname(logPath), path.basename(logPath)
|
||||
// tell glob to only match digits
|
||||
.replace(/\d/g, '[0123456789]')
|
||||
// Handle the old (prior to 8.2.0) log file names which did not have a
|
||||
// counter suffix
|
||||
.replace(/-\.log$/, '*.log')
|
||||
)
|
||||
|
||||
// Always ignore the currently written files
|
||||
const files = await glob(globify(logGlob), { ignore: this.#files.map(globify), silent: true })
|
||||
const toDelete = files.length - this.#logsMax
|
||||
|
||||
if (toDelete <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
log.silly('logfile', `start cleaning logs, removing ${toDelete} files`)
|
||||
|
||||
for (const file of files.slice(0, toDelete)) {
|
||||
try {
|
||||
await rimraf(file, { glob: false })
|
||||
} catch (e) {
|
||||
log.silly('logfile', 'error removing log file', file, e)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn('logfile', 'error cleaning log files', e)
|
||||
} finally {
|
||||
log.silly('logfile', 'done cleaning log files')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogFiles
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
const NPMLOG = require('npmlog')
|
||||
const PROCLOG = require('proc-log')
|
||||
|
||||
// Sets getter and optionally a setter
|
||||
// otherwise setting should throw
|
||||
const accessors = (obj, set) => (k) => ({
|
||||
get: () => obj[k],
|
||||
set: set ? (v) => (obj[k] = v) : () => {
|
||||
throw new Error(`Cant set ${k}`)
|
||||
},
|
||||
})
|
||||
|
||||
// Set the value to a bound function on the object
|
||||
const value = (obj) => (k) => ({
|
||||
value: (...args) => obj[k].apply(obj, args),
|
||||
})
|
||||
|
||||
const properties = {
|
||||
// npmlog getters/setters
|
||||
level: accessors(NPMLOG, true),
|
||||
heading: accessors(NPMLOG, true),
|
||||
levels: accessors(NPMLOG),
|
||||
gauge: accessors(NPMLOG),
|
||||
stream: accessors(NPMLOG),
|
||||
tracker: accessors(NPMLOG),
|
||||
progressEnabled: accessors(NPMLOG),
|
||||
// npmlog methods
|
||||
useColor: value(NPMLOG),
|
||||
enableColor: value(NPMLOG),
|
||||
disableColor: value(NPMLOG),
|
||||
enableUnicode: value(NPMLOG),
|
||||
disableUnicode: value(NPMLOG),
|
||||
enableProgress: value(NPMLOG),
|
||||
disableProgress: value(NPMLOG),
|
||||
clearProgress: value(NPMLOG),
|
||||
showProgress: value(NPMLOG),
|
||||
newItem: value(NPMLOG),
|
||||
newGroup: value(NPMLOG),
|
||||
// proclog methods
|
||||
notice: value(PROCLOG),
|
||||
error: value(PROCLOG),
|
||||
warn: value(PROCLOG),
|
||||
info: value(PROCLOG),
|
||||
verbose: value(PROCLOG),
|
||||
http: value(PROCLOG),
|
||||
silly: value(PROCLOG),
|
||||
pause: value(PROCLOG),
|
||||
resume: value(PROCLOG),
|
||||
}
|
||||
|
||||
const descriptors = Object.entries(properties).reduce((acc, [k, v]) => {
|
||||
acc[k] = { enumerable: true, ...v(k) }
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Create an object with the allowed properties rom npm log and all
|
||||
// the logging methods from proc log
|
||||
// XXX: this should go away and requires of this should be replaced with proc-log + new display
|
||||
module.exports = Object.freeze(Object.defineProperties({}, descriptors))
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
const { dirname } = require('path')
|
||||
const { cmdList } = require('./cmd-list')
|
||||
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
||||
|
||||
module.exports = async (npm) => {
|
||||
const usesBrowser = npm.config.get('viewer') === 'browser'
|
||||
? ' (in a browser)' : ''
|
||||
return `npm <command>
|
||||
|
||||
Usage:
|
||||
|
||||
npm install install all the dependencies in your project
|
||||
npm install <foo> add the <foo> dependency to your project
|
||||
npm test run this project's tests
|
||||
npm run <foo> run the script named <foo>
|
||||
npm <command> -h quick help on <command>
|
||||
npm -l display usage info for all commands
|
||||
npm help <term> search for help on <term>${usesBrowser}
|
||||
npm help npm more involved overview${usesBrowser}
|
||||
|
||||
All commands:
|
||||
${await allCommands(npm)}
|
||||
|
||||
Specify configs in the ini-formatted file:
|
||||
${npm.config.get('userconfig')}
|
||||
or on the command line via: npm <command> --key=value
|
||||
|
||||
More configuration info: npm help config
|
||||
Configuration fields: npm help 7 config
|
||||
|
||||
npm@${npm.version} ${dirname(dirname(__dirname))}`
|
||||
}
|
||||
|
||||
const allCommands = async (npm) => {
|
||||
if (npm.config.get('long')) {
|
||||
return usages(npm)
|
||||
}
|
||||
return ('\n ' + wrap(cmdList))
|
||||
}
|
||||
|
||||
const wrap = (arr) => {
|
||||
const out = ['']
|
||||
|
||||
const line = !process.stdout.columns ? 60
|
||||
: Math.min(60, Math.max(process.stdout.columns - 16, 24))
|
||||
|
||||
let l = 0
|
||||
for (const c of arr) {
|
||||
if (out[l].length + c.length + 2 < line) {
|
||||
out[l] += ', ' + c
|
||||
} else {
|
||||
out[l++] += ','
|
||||
out[l] = c
|
||||
}
|
||||
}
|
||||
return out.join('\n ').slice(2)
|
||||
}
|
||||
|
||||
const usages = async (npm) => {
|
||||
// return a string of <command>: <usage>
|
||||
let maxLen = 0
|
||||
const set = []
|
||||
for (const c of cmdList) {
|
||||
const cmd = await npm.cmd(c)
|
||||
set.push([c, cmd.usage])
|
||||
maxLen = Math.max(maxLen, c.length)
|
||||
}
|
||||
return set.sort(([a], [b]) => localeCompare(a, b))
|
||||
.map(([c, usage]) => `\n ${c}${' '.repeat(maxLen - c.length + 1)}${
|
||||
(usage.split('\n').join('\n' + ' '.repeat(maxLen + 5)))}`)
|
||||
.join('\n')
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
const readline = require('readline')
|
||||
const opener = require('opener')
|
||||
|
||||
function print (npm, title, url) {
|
||||
const json = npm.config.get('json')
|
||||
|
||||
const message = json ? JSON.stringify({ title, url }) : `${title}:\n${url}`
|
||||
|
||||
npm.output(message)
|
||||
}
|
||||
|
||||
// Prompt to open URL in browser if possible
|
||||
const promptOpen = async (npm, url, title, prompt, emitter) => {
|
||||
const browser = npm.config.get('browser')
|
||||
const isInteractive = process.stdin.isTTY === true && process.stdout.isTTY === true
|
||||
|
||||
try {
|
||||
if (!/^https?:$/.test(new URL(url).protocol)) {
|
||||
throw new Error()
|
||||
}
|
||||
} catch (_) {
|
||||
throw new Error('Invalid URL: ' + url)
|
||||
}
|
||||
|
||||
print(npm, title, url)
|
||||
|
||||
if (browser === false || !isInteractive) {
|
||||
return
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
const tryOpen = await new Promise(resolve => {
|
||||
rl.on('SIGINT', () => {
|
||||
rl.close()
|
||||
resolve('SIGINT')
|
||||
})
|
||||
|
||||
rl.question(prompt, () => {
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
if (emitter && emitter.addListener) {
|
||||
emitter.addListener('abort', () => {
|
||||
rl.close()
|
||||
|
||||
// clear the prompt line
|
||||
npm.output('')
|
||||
|
||||
resolve(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (tryOpen === 'SIGINT') {
|
||||
throw new Error('canceled')
|
||||
}
|
||||
|
||||
if (!tryOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
const command = browser === true ? null : browser
|
||||
await new Promise((resolve, reject) => {
|
||||
opener(url, { command }, err => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
return resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = promptOpen
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
const opener = require('opener')
|
||||
|
||||
const { URL } = require('url')
|
||||
|
||||
// attempt to open URL in web-browser, print address otherwise:
|
||||
const open = async (npm, url, errMsg, isFile) => {
|
||||
url = encodeURI(url)
|
||||
const browser = npm.config.get('browser')
|
||||
|
||||
function printAlternateMsg () {
|
||||
const json = npm.config.get('json')
|
||||
const alternateMsg = json
|
||||
? JSON.stringify({
|
||||
title: errMsg,
|
||||
url,
|
||||
}, null, 2)
|
||||
: `${errMsg}:\n ${url}\n`
|
||||
|
||||
npm.output(alternateMsg)
|
||||
}
|
||||
|
||||
if (browser === false) {
|
||||
printAlternateMsg()
|
||||
return
|
||||
}
|
||||
|
||||
// We pass this in as true from the help command so we know we don't have to
|
||||
// check the protocol
|
||||
if (!isFile) {
|
||||
try {
|
||||
if (!/^https?:$/.test(new URL(url).protocol)) {
|
||||
throw new Error()
|
||||
}
|
||||
} catch (_) {
|
||||
throw new Error('Invalid URL: ' + url)
|
||||
}
|
||||
}
|
||||
|
||||
const command = browser === true ? null : browser
|
||||
await new Promise((resolve, reject) => {
|
||||
opener(url, { command }, (err) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
printAlternateMsg()
|
||||
} else {
|
||||
return reject(err)
|
||||
}
|
||||
}
|
||||
return resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = open
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
async function otplease (npm, opts, fn) {
|
||||
try {
|
||||
return await fn(opts)
|
||||
} catch (err) {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
throw err
|
||||
}
|
||||
|
||||
if (isWebOTP(err)) {
|
||||
const webAuth = require('./web-auth')
|
||||
const openUrlPrompt = require('./open-url-prompt')
|
||||
|
||||
const openerPromise = (url, emitter) =>
|
||||
openUrlPrompt(
|
||||
npm,
|
||||
url,
|
||||
'Authenticate your account at',
|
||||
'Press ENTER to open in the browser...',
|
||||
emitter
|
||||
)
|
||||
const otp = await webAuth(openerPromise, err.body.authUrl, err.body.doneUrl, opts)
|
||||
return await fn({ ...opts, otp })
|
||||
}
|
||||
|
||||
if (isClassicOTP(err)) {
|
||||
const readUserInfo = require('./read-user-info.js')
|
||||
const otp = await readUserInfo.otp('This operation requires a one-time password.\nEnter OTP:')
|
||||
return await fn({ ...opts, otp })
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function isWebOTP (err) {
|
||||
if (!err.code === 'EOTP' || !err.body) {
|
||||
return false
|
||||
}
|
||||
return err.body.authUrl && err.body.doneUrl
|
||||
}
|
||||
|
||||
function isClassicOTP (err) {
|
||||
return err.code === 'EOTP' || (err.code === 'E401' && /one-time pass/.test(err.body))
|
||||
}
|
||||
|
||||
module.exports = otplease
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// ping the npm registry
|
||||
// used by the ping and doctor commands
|
||||
const fetch = require('npm-registry-fetch')
|
||||
module.exports = async (flatOptions) => {
|
||||
const res = await fetch('/-/ping?write=true', flatOptions)
|
||||
return res.json().catch(() => ({}))
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
const log = require('./log-shim.js')
|
||||
|
||||
let pulseTimer = null
|
||||
const withPromise = async (promise) => {
|
||||
pulseStart()
|
||||
try {
|
||||
return await promise
|
||||
} finally {
|
||||
pulseStop()
|
||||
}
|
||||
}
|
||||
|
||||
const pulseStart = () => {
|
||||
pulseTimer = pulseTimer || setInterval(() => {
|
||||
log.gauge.pulse('')
|
||||
}, 150)
|
||||
}
|
||||
|
||||
const pulseStop = () => {
|
||||
clearInterval(pulseTimer)
|
||||
pulseTimer = null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
withPromise,
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
const util = require('util')
|
||||
const _data = Symbol('data')
|
||||
const _delete = Symbol('delete')
|
||||
const _append = Symbol('append')
|
||||
|
||||
const sqBracketsMatcher = str => str.match(/(.+)\[([^\]]+)\]\.?(.*)$/)
|
||||
|
||||
// replaces any occurence of an empty-brackets (e.g: []) with a special
|
||||
// Symbol(append) to represent it, this is going to be useful for the setter
|
||||
// method that will push values to the end of the array when finding these
|
||||
const replaceAppendSymbols = str => {
|
||||
const matchEmptyBracket = str.match(/^(.*)\[\]\.?(.*)$/)
|
||||
|
||||
if (matchEmptyBracket) {
|
||||
const [, pre, post] = matchEmptyBracket
|
||||
return [...replaceAppendSymbols(pre), _append, post].filter(Boolean)
|
||||
}
|
||||
|
||||
return [str]
|
||||
}
|
||||
|
||||
const parseKeys = key => {
|
||||
const sqBracketItems = new Set()
|
||||
sqBracketItems.add(_append)
|
||||
const parseSqBrackets = str => {
|
||||
const index = sqBracketsMatcher(str)
|
||||
|
||||
// once we find square brackets, we recursively parse all these
|
||||
if (index) {
|
||||
const preSqBracketPortion = index[1]
|
||||
|
||||
// we want to have a `new String` wrapper here in order to differentiate
|
||||
// between multiple occurences of the same string, e.g:
|
||||
// foo.bar[foo.bar] should split into { foo: { bar: { 'foo.bar': {} } }
|
||||
/* eslint-disable-next-line no-new-wrappers */
|
||||
const foundKey = new String(index[2])
|
||||
const postSqBracketPortion = index[3]
|
||||
|
||||
// we keep track of items found during this step to make sure
|
||||
// we don't try to split-separate keys that were defined within
|
||||
// square brackets, since the key name itself might contain dots
|
||||
sqBracketItems.add(foundKey)
|
||||
|
||||
// returns an array that contains either dot-separate items (that will
|
||||
// be splitted appart during the next step OR the fully parsed keys
|
||||
// read from square brackets, e.g:
|
||||
// foo.bar[1.0.0].a.b -> ['foo.bar', '1.0.0', 'a.b']
|
||||
return [
|
||||
...parseSqBrackets(preSqBracketPortion),
|
||||
foundKey,
|
||||
...(postSqBracketPortion ? parseSqBrackets(postSqBracketPortion) : []),
|
||||
]
|
||||
}
|
||||
|
||||
// at the end of parsing, any usage of the special empty-bracket syntax
|
||||
// (e.g: foo.array[]) has not yet been parsed, here we'll take care
|
||||
// of parsing it and adding a special symbol to represent it in
|
||||
// the resulting list of keys
|
||||
return replaceAppendSymbols(str)
|
||||
}
|
||||
|
||||
const res = []
|
||||
// starts by parsing items defined as square brackets, those might be
|
||||
// representing properties that have a dot in the name or just array
|
||||
// indexes, e.g: foo[1.0.0] or list[0]
|
||||
const sqBracketKeys = parseSqBrackets(key.trim())
|
||||
|
||||
for (const k of sqBracketKeys) {
|
||||
// keys parsed from square brackets should just be added to list of
|
||||
// resulting keys as they might have dots as part of the key
|
||||
if (sqBracketItems.has(k)) {
|
||||
res.push(k)
|
||||
} else {
|
||||
// splits the dot-sep property names and add them to the list of keys
|
||||
/* eslint-disable-next-line no-new-wrappers */
|
||||
for (const splitKey of k.split('.')) {
|
||||
res.push(String(splitKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns an ordered list of strings in which each entry
|
||||
// represents a key in an object defined by the previous entry
|
||||
return res
|
||||
}
|
||||
|
||||
const getter = ({ data, key }) => {
|
||||
// keys are a list in which each entry represents the name of
|
||||
// a property that should be walked through the object in order to
|
||||
// return the final found value
|
||||
const keys = parseKeys(key)
|
||||
let _data = data
|
||||
let label = ''
|
||||
|
||||
for (const k of keys) {
|
||||
// empty-bracket-shortcut-syntax is not supported on getter
|
||||
if (k === _append) {
|
||||
throw Object.assign(new Error('Empty brackets are not valid syntax for retrieving values.'), {
|
||||
code: 'EINVALIDSYNTAX',
|
||||
})
|
||||
}
|
||||
|
||||
// extra logic to take into account printing array, along with its
|
||||
// special syntax in which using a dot-sep property name after an
|
||||
// arry will expand it's results, e.g:
|
||||
// arr.name -> arr[0].name=value, arr[1].name=value, ...
|
||||
const maybeIndex = Number(k)
|
||||
if (Array.isArray(_data) && !Number.isInteger(maybeIndex)) {
|
||||
_data = _data.reduce((acc, i, index) => {
|
||||
acc[`${label}[${index}].${k}`] = i[k]
|
||||
return acc
|
||||
}, {})
|
||||
return _data
|
||||
} else {
|
||||
// if can't find any more values, it means it's just over
|
||||
// and there's nothing to return
|
||||
if (!_data[k]) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// otherwise sets the next value
|
||||
_data = _data[k]
|
||||
}
|
||||
|
||||
label += k
|
||||
}
|
||||
|
||||
// these are some legacy expectations from
|
||||
// the old API consumed by lib/view.js
|
||||
if (Array.isArray(_data) && _data.length <= 1) {
|
||||
_data = _data[0]
|
||||
}
|
||||
|
||||
return {
|
||||
[key]: _data,
|
||||
}
|
||||
}
|
||||
|
||||
const setter = ({ data, key, value, force }) => {
|
||||
// setter goes to recursively transform the provided data obj,
|
||||
// setting properties from the list of parsed keys, e.g:
|
||||
// ['foo', 'bar', 'baz'] -> { foo: { bar: { baz: {} } }
|
||||
const keys = parseKeys(key)
|
||||
const setKeys = (_data, _key) => {
|
||||
// handles array indexes, converting valid integers to numbers,
|
||||
// note that occurences of Symbol(append) will throw,
|
||||
// so we just ignore these for now
|
||||
let maybeIndex = Number.NaN
|
||||
try {
|
||||
maybeIndex = Number(_key)
|
||||
} catch (err) {}
|
||||
if (!Number.isNaN(maybeIndex)) {
|
||||
_key = maybeIndex
|
||||
}
|
||||
|
||||
// creates new array in case key is an index
|
||||
// and the array obj is not yet defined
|
||||
const keyIsAnArrayIndex = _key === maybeIndex || _key === _append
|
||||
const dataHasNoItems = !Object.keys(_data).length
|
||||
if (keyIsAnArrayIndex && dataHasNoItems && !Array.isArray(_data)) {
|
||||
_data = []
|
||||
}
|
||||
|
||||
// converting from array to an object is also possible, in case the
|
||||
// user is using force mode, we should also convert existing arrays
|
||||
// to an empty object if the current _data is an array
|
||||
if (force && Array.isArray(_data) && !keyIsAnArrayIndex) {
|
||||
_data = { ..._data }
|
||||
}
|
||||
|
||||
// the _append key is a special key that is used to represent
|
||||
// the empty-bracket notation, e.g: arr[] -> arr[arr.length]
|
||||
if (_key === _append) {
|
||||
if (!Array.isArray(_data)) {
|
||||
throw Object.assign(new Error(`Can't use append syntax in non-Array element`), {
|
||||
code: 'ENOAPPEND',
|
||||
})
|
||||
}
|
||||
_key = _data.length
|
||||
}
|
||||
|
||||
// retrieves the next data object to recursively iterate on,
|
||||
// throws if trying to override a literal value or add props to an array
|
||||
const next = () => {
|
||||
const haveContents = !force && _data[_key] != null && value !== _delete
|
||||
const shouldNotOverrideLiteralValue = !(typeof _data[_key] === 'object')
|
||||
// if the next obj to recurse is an array and the next key to be
|
||||
// appended to the resulting obj is not an array index, then it
|
||||
// should throw since we can't append arbitrary props to arrays
|
||||
const shouldNotAddPropsToArrays =
|
||||
typeof keys[0] !== 'symbol' && Array.isArray(_data[_key]) && Number.isNaN(Number(keys[0]))
|
||||
|
||||
const overrideError = haveContents && shouldNotOverrideLiteralValue
|
||||
if (overrideError) {
|
||||
throw Object.assign(
|
||||
new Error(`Property ${_key} already exists and is not an Array or Object.`),
|
||||
{ code: 'EOVERRIDEVALUE' }
|
||||
)
|
||||
}
|
||||
|
||||
const addPropsToArrayError = haveContents && shouldNotAddPropsToArrays
|
||||
if (addPropsToArrayError) {
|
||||
throw Object.assign(new Error(`Can't add property ${key} to an Array.`), {
|
||||
code: 'ENOADDPROP',
|
||||
})
|
||||
}
|
||||
|
||||
return typeof _data[_key] === 'object' ? _data[_key] || {} : {}
|
||||
}
|
||||
|
||||
// sets items from the parsed array of keys as objects, recurses to
|
||||
// setKeys in case there are still items to be handled, otherwise it
|
||||
// just sets the original value set by the user
|
||||
if (keys.length) {
|
||||
_data[_key] = setKeys(next(), keys.shift())
|
||||
} else {
|
||||
// handles special deletion cases for obj props / array items
|
||||
if (value === _delete) {
|
||||
if (Array.isArray(_data)) {
|
||||
_data.splice(_key, 1)
|
||||
} else {
|
||||
delete _data[_key]
|
||||
}
|
||||
} else {
|
||||
// finally, sets the value in its right place
|
||||
_data[_key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return _data
|
||||
}
|
||||
|
||||
setKeys(data, keys.shift())
|
||||
}
|
||||
|
||||
class Queryable {
|
||||
constructor (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
throw Object.assign(new Error('Queryable needs an object to query properties from.'), {
|
||||
code: 'ENOQUERYABLEOBJ',
|
||||
})
|
||||
}
|
||||
|
||||
this[_data] = obj
|
||||
}
|
||||
|
||||
query (queries) {
|
||||
// this ugly interface here is meant to be a compatibility layer
|
||||
// with the legacy API lib/view.js is consuming, if at some point
|
||||
// we refactor that command then we can revisit making this nicer
|
||||
if (queries === '') {
|
||||
return { '': this[_data] }
|
||||
}
|
||||
|
||||
const q = query =>
|
||||
getter({
|
||||
data: this[_data],
|
||||
key: query,
|
||||
})
|
||||
|
||||
if (Array.isArray(queries)) {
|
||||
let res = {}
|
||||
for (const query of queries) {
|
||||
res = { ...res, ...q(query) }
|
||||
}
|
||||
return res
|
||||
} else {
|
||||
return q(queries)
|
||||
}
|
||||
}
|
||||
|
||||
// return the value for a single query if found, otherwise returns undefined
|
||||
get (query) {
|
||||
const obj = this.query(query)
|
||||
if (obj) {
|
||||
return obj[query]
|
||||
}
|
||||
}
|
||||
|
||||
// creates objects along the way for the provided `query` parameter
|
||||
// and assigns `value` to the last property of the query chain
|
||||
set (query, value, { force } = {}) {
|
||||
setter({
|
||||
data: this[_data],
|
||||
key: query,
|
||||
value,
|
||||
force,
|
||||
})
|
||||
}
|
||||
|
||||
// deletes the value of the property found at `query`
|
||||
delete (query) {
|
||||
setter({
|
||||
data: this[_data],
|
||||
key: query,
|
||||
value: _delete,
|
||||
})
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return this[_data]
|
||||
}
|
||||
|
||||
[util.inspect.custom] () {
|
||||
return this.toJSON()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Queryable
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
const { promisify } = require('util')
|
||||
const readAsync = promisify(require('read'))
|
||||
const userValidate = require('npm-user-validate')
|
||||
const log = require('./log-shim.js')
|
||||
|
||||
exports.otp = readOTP
|
||||
exports.password = readPassword
|
||||
exports.username = readUsername
|
||||
exports.email = readEmail
|
||||
|
||||
const otpPrompt = `This command requires a one-time password (OTP) from your authenticator app.
|
||||
Enter one below. You can also pass one on the command line by appending --otp=123456.
|
||||
For more information, see:
|
||||
https://docs.npmjs.com/getting-started/using-two-factor-authentication
|
||||
Enter OTP: `
|
||||
const passwordPrompt = 'npm password: '
|
||||
const usernamePrompt = 'npm username: '
|
||||
const emailPrompt = 'email (this IS public): '
|
||||
|
||||
function read (opts) {
|
||||
log.clearProgress()
|
||||
return readAsync(opts).finally(() => log.showProgress())
|
||||
}
|
||||
|
||||
function readOTP (msg = otpPrompt, otp, isRetry) {
|
||||
if (isRetry && otp && /^[\d ]+$|^[A-Fa-f0-9]{64,64}$/.test(otp)) {
|
||||
return otp.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
return read({ prompt: msg, default: otp || '' })
|
||||
.then((otp) => readOTP(msg, otp, true))
|
||||
}
|
||||
|
||||
function readPassword (msg = passwordPrompt, password, isRetry) {
|
||||
if (isRetry && password) {
|
||||
return password
|
||||
}
|
||||
|
||||
return read({ prompt: msg, silent: true, default: password || '' })
|
||||
.then((password) => readPassword(msg, password, true))
|
||||
}
|
||||
|
||||
function readUsername (msg = usernamePrompt, username, isRetry) {
|
||||
if (isRetry && username) {
|
||||
const error = userValidate.username(username)
|
||||
if (error) {
|
||||
log.warn(error.message)
|
||||
} else {
|
||||
return Promise.resolve(username.trim())
|
||||
}
|
||||
}
|
||||
|
||||
return read({ prompt: msg, default: username || '' })
|
||||
.then((username) => readUsername(msg, username, true))
|
||||
}
|
||||
|
||||
function readEmail (msg = emailPrompt, email, isRetry) {
|
||||
if (isRetry && email) {
|
||||
const error = userValidate.email(email)
|
||||
if (error) {
|
||||
log.warn(error.message)
|
||||
} else {
|
||||
return email.trim()
|
||||
}
|
||||
}
|
||||
|
||||
return read({ prompt: msg, default: email || '' })
|
||||
.then((username) => readEmail(msg, username, true))
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
const reifyOutput = require('./reify-output.js')
|
||||
const ini = require('ini')
|
||||
const { writeFile } = require('fs').promises
|
||||
const { resolve } = require('path')
|
||||
|
||||
const reifyFinish = async (npm, arb) => {
|
||||
await saveBuiltinConfig(npm, arb)
|
||||
reifyOutput(npm, arb)
|
||||
}
|
||||
|
||||
const saveBuiltinConfig = async (npm, arb) => {
|
||||
const { options: { global }, actualTree } = arb
|
||||
if (!global) {
|
||||
return
|
||||
}
|
||||
|
||||
// if we are using a builtin config, and just installed npm as
|
||||
// a top-level global package, we have to preserve that config.
|
||||
const npmNode = actualTree.inventory.get('node_modules/npm')
|
||||
if (!npmNode) {
|
||||
return
|
||||
}
|
||||
|
||||
const builtinConf = npm.config.data.get('builtin')
|
||||
if (builtinConf.loadError) {
|
||||
return
|
||||
}
|
||||
|
||||
const content = ini.stringify(builtinConf.raw).trim() + '\n'
|
||||
await writeFile(resolve(npmNode.path, 'npmrc'), content)
|
||||
}
|
||||
|
||||
module.exports = reifyFinish
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
// pass in an arborist object, and it'll output the data about what
|
||||
// was done, what was audited, etc.
|
||||
//
|
||||
// added ## packages, removed ## packages, and audited ## packages in 19.157s
|
||||
//
|
||||
// 1 package is looking for funding
|
||||
// run `npm fund` for details
|
||||
//
|
||||
// found 37 vulnerabilities (5 low, 7 moderate, 25 high)
|
||||
// run `npm audit fix` to fix them, or `npm audit` for details
|
||||
|
||||
const log = require('./log-shim.js')
|
||||
const { depth } = require('treeverse')
|
||||
const ms = require('ms')
|
||||
const auditReport = require('npm-audit-report')
|
||||
const { readTree: getFundingInfo } = require('libnpmfund')
|
||||
const auditError = require('./audit-error.js')
|
||||
|
||||
// TODO: output JSON if flatOptions.json is true
|
||||
const reifyOutput = (npm, arb) => {
|
||||
const { diff, actualTree } = arb
|
||||
|
||||
// note: fails and crashes if we're running audit fix and there was an error
|
||||
// which is a good thing, because there's no point printing all this other
|
||||
// stuff in that case!
|
||||
const auditReport = auditError(npm, arb.auditReport) ? null : arb.auditReport
|
||||
|
||||
// don't print any info in --silent mode, but we still need to
|
||||
// set the exitCode properly from the audit report, if we have one.
|
||||
if (npm.silent) {
|
||||
getAuditReport(npm, auditReport)
|
||||
return
|
||||
}
|
||||
|
||||
const summary = {
|
||||
added: 0,
|
||||
removed: 0,
|
||||
changed: 0,
|
||||
audited: auditReport && !auditReport.error ? actualTree.inventory.size : 0,
|
||||
funding: 0,
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
depth({
|
||||
tree: diff,
|
||||
visit: d => {
|
||||
switch (d.action) {
|
||||
case 'REMOVE':
|
||||
summary.removed++
|
||||
break
|
||||
case 'ADD':
|
||||
actualTree.inventory.has(d.ideal) && summary.added++
|
||||
break
|
||||
case 'CHANGE':
|
||||
summary.changed++
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
const node = d.actual || d.ideal
|
||||
log.silly(d.action, node.location)
|
||||
},
|
||||
getChildren: d => d.children,
|
||||
})
|
||||
}
|
||||
|
||||
if (npm.flatOptions.fund) {
|
||||
const fundingInfo = getFundingInfo(actualTree, { countOnly: true })
|
||||
summary.funding = fundingInfo.length
|
||||
}
|
||||
|
||||
if (npm.flatOptions.json) {
|
||||
if (auditReport) {
|
||||
// call this to set the exit code properly
|
||||
getAuditReport(npm, auditReport)
|
||||
summary.audit = npm.command === 'audit' ? auditReport
|
||||
: auditReport.toJSON().metadata
|
||||
}
|
||||
npm.output(JSON.stringify(summary, 0, 2))
|
||||
} else {
|
||||
packagesChangedMessage(npm, summary)
|
||||
packagesFundingMessage(npm, summary)
|
||||
printAuditReport(npm, auditReport)
|
||||
}
|
||||
}
|
||||
|
||||
// if we're running `npm audit fix`, then we print the full audit report
|
||||
// at the end if there's still stuff, because it's silly for `npm audit`
|
||||
// to tell you to run `npm audit` for details. otherwise, use the summary
|
||||
// report. if we get here, we know it's not quiet or json.
|
||||
// If the loglevel is silent, then we just run the report
|
||||
// to get the exitCode set appropriately.
|
||||
const printAuditReport = (npm, report) => {
|
||||
const res = getAuditReport(npm, report)
|
||||
if (!res || !res.report) {
|
||||
return
|
||||
}
|
||||
npm.output(`\n${res.report}`)
|
||||
}
|
||||
|
||||
const getAuditReport = (npm, report) => {
|
||||
if (!report) {
|
||||
return
|
||||
}
|
||||
|
||||
// when in silent mode, we print nothing. the JSON output is
|
||||
// going to just JSON.stringify() the report object.
|
||||
const reporter = npm.silent ? 'quiet'
|
||||
: npm.flatOptions.json ? 'quiet'
|
||||
: npm.command !== 'audit' ? 'install'
|
||||
: 'detail'
|
||||
const defaultAuditLevel = npm.command !== 'audit' ? 'none' : 'low'
|
||||
const auditLevel = npm.flatOptions.auditLevel || defaultAuditLevel
|
||||
|
||||
const res = auditReport(report, {
|
||||
reporter,
|
||||
...npm.flatOptions,
|
||||
auditLevel,
|
||||
})
|
||||
if (npm.command === 'audit') {
|
||||
process.exitCode = process.exitCode || res.exitCode
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const packagesChangedMessage = (npm, { added, removed, changed, audited }) => {
|
||||
const msg = ['\n']
|
||||
if (added === 0 && removed === 0 && changed === 0) {
|
||||
msg.push('up to date')
|
||||
if (audited) {
|
||||
msg.push(', ')
|
||||
}
|
||||
} else {
|
||||
if (added) {
|
||||
msg.push(`added ${added} package${added === 1 ? '' : 's'}`)
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
if (added) {
|
||||
msg.push(', ')
|
||||
}
|
||||
|
||||
if (added && !audited && !changed) {
|
||||
msg.push('and ')
|
||||
}
|
||||
|
||||
msg.push(`removed ${removed} package${removed === 1 ? '' : 's'}`)
|
||||
}
|
||||
if (changed) {
|
||||
if (added || removed) {
|
||||
msg.push(', ')
|
||||
}
|
||||
|
||||
if (!audited && (added || removed)) {
|
||||
msg.push('and ')
|
||||
}
|
||||
|
||||
msg.push(`changed ${changed} package${changed === 1 ? '' : 's'}`)
|
||||
}
|
||||
if (audited) {
|
||||
msg.push(', and ')
|
||||
}
|
||||
}
|
||||
if (audited) {
|
||||
msg.push(`audited ${audited} package${audited === 1 ? '' : 's'}`)
|
||||
}
|
||||
|
||||
msg.push(` in ${ms(Date.now() - npm.started)}`)
|
||||
npm.output(msg.join(''))
|
||||
}
|
||||
|
||||
const packagesFundingMessage = (npm, { funding }) => {
|
||||
if (!funding) {
|
||||
return
|
||||
}
|
||||
|
||||
npm.output('')
|
||||
const pkg = funding === 1 ? 'package' : 'packages'
|
||||
const is = funding === 1 ? 'is' : 'are'
|
||||
npm.output(`${funding} ${pkg} ${is} looking for funding`)
|
||||
npm.output(' run `npm fund` for details')
|
||||
}
|
||||
|
||||
module.exports = reifyOutput
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
const { cleanUrl } = require('npm-registry-fetch')
|
||||
const isString = (v) => typeof v === 'string'
|
||||
|
||||
// split on \s|= similar to how nopt parses options
|
||||
const splitAndReplace = (str) => {
|
||||
// stateful regex, don't move out of this scope
|
||||
const splitChars = /[\s=]/g
|
||||
|
||||
let match = null
|
||||
let result = ''
|
||||
let index = 0
|
||||
while (match = splitChars.exec(str)) {
|
||||
result += cleanUrl(str.slice(index, match.index)) + match[0]
|
||||
index = splitChars.lastIndex
|
||||
}
|
||||
|
||||
return result + cleanUrl(str.slice(index))
|
||||
}
|
||||
|
||||
// replaces auth info in an array of arguments or in a strings
|
||||
function replaceInfo (arg) {
|
||||
if (isString(arg)) {
|
||||
return splitAndReplace(arg)
|
||||
} else if (Array.isArray(arg)) {
|
||||
return arg.map((a) => isString(a) ? splitAndReplace(a) : a)
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
||||
|
||||
module.exports = replaceInfo
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
const tar = require('tar')
|
||||
const ssri = require('ssri')
|
||||
const log = require('./log-shim')
|
||||
const formatBytes = require('./format-bytes.js')
|
||||
const columnify = require('columnify')
|
||||
const localeCompare = require('@isaacs/string-locale-compare')('en', {
|
||||
sensitivity: 'case',
|
||||
numeric: true,
|
||||
})
|
||||
|
||||
const logTar = (tarball, opts = {}) => {
|
||||
const { unicode = false } = opts
|
||||
log.notice('')
|
||||
log.notice('', `${unicode ? '📦 ' : 'package:'} ${tarball.name}@${tarball.version}`)
|
||||
log.notice('=== Tarball Contents ===')
|
||||
if (tarball.files.length) {
|
||||
log.notice(
|
||||
'',
|
||||
columnify(
|
||||
tarball.files
|
||||
.map(f => {
|
||||
const bytes = formatBytes(f.size, false)
|
||||
return /^node_modules\//.test(f.path) ? null : { path: f.path, size: `${bytes}` }
|
||||
})
|
||||
.filter(f => f),
|
||||
{
|
||||
include: ['size', 'path'],
|
||||
showHeaders: false,
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (tarball.bundled.length) {
|
||||
log.notice('=== Bundled Dependencies ===')
|
||||
tarball.bundled.forEach(name => log.notice('', name))
|
||||
}
|
||||
log.notice('=== Tarball Details ===')
|
||||
log.notice(
|
||||
'',
|
||||
columnify(
|
||||
[
|
||||
{ name: 'name:', value: tarball.name },
|
||||
{ name: 'version:', value: tarball.version },
|
||||
tarball.filename && { name: 'filename:', value: tarball.filename },
|
||||
{ name: 'package size:', value: formatBytes(tarball.size) },
|
||||
{ name: 'unpacked size:', value: formatBytes(tarball.unpackedSize) },
|
||||
{ name: 'shasum:', value: tarball.shasum },
|
||||
{
|
||||
name: 'integrity:',
|
||||
value:
|
||||
tarball.integrity.toString().slice(0, 20) +
|
||||
'[...]' +
|
||||
tarball.integrity.toString().slice(80),
|
||||
},
|
||||
tarball.bundled.length && { name: 'bundled deps:', value: tarball.bundled.length },
|
||||
tarball.bundled.length && {
|
||||
name: 'bundled files:',
|
||||
value: tarball.entryCount - tarball.files.length,
|
||||
},
|
||||
tarball.bundled.length && { name: 'own files:', value: tarball.files.length },
|
||||
{ name: 'total files:', value: tarball.entryCount },
|
||||
].filter(x => x),
|
||||
{
|
||||
include: ['name', 'value'],
|
||||
showHeaders: false,
|
||||
}
|
||||
)
|
||||
)
|
||||
log.notice('', '')
|
||||
}
|
||||
|
||||
const getContents = async (manifest, tarball) => {
|
||||
const files = []
|
||||
const bundled = new Set()
|
||||
let totalEntries = 0
|
||||
let totalEntrySize = 0
|
||||
|
||||
// reads contents of tarball
|
||||
const stream = tar.t({
|
||||
onentry (entry) {
|
||||
totalEntries++
|
||||
totalEntrySize += entry.size
|
||||
const p = entry.path
|
||||
if (p.startsWith('package/node_modules/')) {
|
||||
const name = p.match(/^package\/node_modules\/((?:@[^/]+\/)?[^/]+)/)[1]
|
||||
bundled.add(name)
|
||||
}
|
||||
files.push({
|
||||
path: entry.path.replace(/^package\//, ''),
|
||||
size: entry.size,
|
||||
mode: entry.mode,
|
||||
})
|
||||
},
|
||||
})
|
||||
stream.end(tarball)
|
||||
|
||||
const integrity = await ssri.fromData(tarball, {
|
||||
algorithms: ['sha1', 'sha512'],
|
||||
})
|
||||
|
||||
const comparator = ({ path: a }, { path: b }) => localeCompare(a, b)
|
||||
|
||||
const isUpper = str => {
|
||||
const ch = str.charAt(0)
|
||||
return ch === ch.toUpperCase()
|
||||
}
|
||||
|
||||
const uppers = files.filter(file => isUpper(file.path))
|
||||
const others = files.filter(file => !isUpper(file.path))
|
||||
|
||||
uppers.sort(comparator)
|
||||
others.sort(comparator)
|
||||
|
||||
const shasum = integrity.sha1[0].hexDigest()
|
||||
return {
|
||||
id: manifest._id || `${manifest.name}@${manifest.version}`,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
size: tarball.length,
|
||||
unpackedSize: totalEntrySize,
|
||||
shasum,
|
||||
integrity: ssri.parse(integrity.sha512[0]),
|
||||
filename: `${manifest.name}-${manifest.version}.tgz`,
|
||||
files: uppers.concat(others),
|
||||
entryCount: totalEntries,
|
||||
bundled: Array.from(bundled),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { logTar, getContents }
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
const EE = require('events')
|
||||
const { resolve } = require('path')
|
||||
const fs = require('@npmcli/fs')
|
||||
const log = require('./log-shim')
|
||||
|
||||
const _timeListener = Symbol('timeListener')
|
||||
const _timeEndListener = Symbol('timeEndListener')
|
||||
const _init = Symbol('init')
|
||||
|
||||
// This is an event emiiter but on/off
|
||||
// only listen on a single internal event that gets
|
||||
// emitted whenever a timer ends
|
||||
class Timers extends EE {
|
||||
file = null
|
||||
|
||||
#unfinished = new Map()
|
||||
#finished = {}
|
||||
#onTimeEnd = Symbol('onTimeEnd')
|
||||
#initialListener = null
|
||||
#initialTimer = null
|
||||
|
||||
constructor ({ listener = null, start = 'npm' } = {}) {
|
||||
super()
|
||||
this.#initialListener = listener
|
||||
this.#initialTimer = start
|
||||
this[_init]()
|
||||
}
|
||||
|
||||
get unfinished () {
|
||||
return this.#unfinished
|
||||
}
|
||||
|
||||
get finished () {
|
||||
return this.#finished
|
||||
}
|
||||
|
||||
[_init] () {
|
||||
this.on()
|
||||
if (this.#initialListener) {
|
||||
this.on(this.#initialListener)
|
||||
}
|
||||
process.emit('time', this.#initialTimer)
|
||||
this.started = this.#unfinished.get(this.#initialTimer)
|
||||
}
|
||||
|
||||
on (listener) {
|
||||
if (listener) {
|
||||
super.on(this.#onTimeEnd, listener)
|
||||
} else {
|
||||
process.on('time', this[_timeListener])
|
||||
process.on('timeEnd', this[_timeEndListener])
|
||||
}
|
||||
}
|
||||
|
||||
off (listener) {
|
||||
if (listener) {
|
||||
super.off(this.#onTimeEnd, listener)
|
||||
} else {
|
||||
this.removeAllListeners(this.#onTimeEnd)
|
||||
process.off('time', this[_timeListener])
|
||||
process.off('timeEnd', this[_timeEndListener])
|
||||
}
|
||||
}
|
||||
|
||||
time (name, fn) {
|
||||
process.emit('time', name)
|
||||
const end = () => process.emit('timeEnd', name)
|
||||
if (typeof fn === 'function') {
|
||||
const res = fn()
|
||||
return res && res.finally ? res.finally(end) : (end(), res)
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
load ({ dir } = {}) {
|
||||
if (dir) {
|
||||
this.file = resolve(dir, '_timing.json')
|
||||
}
|
||||
}
|
||||
|
||||
writeFile (fileData) {
|
||||
if (!this.file) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const globalStart = this.started
|
||||
const globalEnd = this.#finished.npm || Date.now()
|
||||
const content = {
|
||||
...fileData,
|
||||
...this.#finished,
|
||||
// add any unfinished timers with their relative start/end
|
||||
unfinished: [...this.#unfinished.entries()].reduce((acc, [name, start]) => {
|
||||
acc[name] = [start - globalStart, globalEnd - globalStart]
|
||||
return acc
|
||||
}, {}),
|
||||
}
|
||||
// we append line delimited json to this file...forever
|
||||
// XXX: should we also write a process specific timing file?
|
||||
// with similar rules to the debug log (max files, etc)
|
||||
fs.withOwnerSync(
|
||||
this.file,
|
||||
() => fs.appendFileSync(this.file, JSON.stringify(content) + '\n'),
|
||||
{ owner: 'inherit' }
|
||||
)
|
||||
} catch (e) {
|
||||
this.file = null
|
||||
log.warn('timing', `could not write timing file: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
[_timeListener] = (name) => {
|
||||
this.#unfinished.set(name, Date.now())
|
||||
}
|
||||
|
||||
[_timeEndListener] = (name) => {
|
||||
if (this.#unfinished.has(name)) {
|
||||
const ms = Date.now() - this.#unfinished.get(name)
|
||||
this.#finished[name] = ms
|
||||
this.#unfinished.delete(name)
|
||||
this.emit(this.#onTimeEnd, name, ms)
|
||||
} else {
|
||||
log.silly('timing', "Tried to end timer that doesn't exist:", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Timers
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// print a banner telling the user to upgrade npm to latest
|
||||
// but not in CI, and not if we're doing that already.
|
||||
// Check daily for betas, and weekly otherwise.
|
||||
|
||||
const pacote = require('pacote')
|
||||
const ciDetect = require('@npmcli/ci-detect')
|
||||
const semver = require('semver')
|
||||
const chalk = require('chalk')
|
||||
const { promisify } = require('util')
|
||||
const stat = promisify(require('fs').stat)
|
||||
const writeFile = promisify(require('fs').writeFile)
|
||||
const { resolve } = require('path')
|
||||
|
||||
const SKIP = Symbol('SKIP')
|
||||
|
||||
const isGlobalNpmUpdate = npm => {
|
||||
return npm.flatOptions.global &&
|
||||
['install', 'update'].includes(npm.command) &&
|
||||
npm.argv.some(arg => /^npm(@|$)/.test(arg))
|
||||
}
|
||||
|
||||
// update check frequency
|
||||
const DAILY = 1000 * 60 * 60 * 24
|
||||
const WEEKLY = DAILY * 7
|
||||
|
||||
// don't put it in the _cacache folder, just in npm's cache
|
||||
const lastCheckedFile = npm =>
|
||||
resolve(npm.flatOptions.cache, '../_update-notifier-last-checked')
|
||||
|
||||
const checkTimeout = async (npm, duration) => {
|
||||
const t = new Date(Date.now() - duration)
|
||||
const f = lastCheckedFile(npm)
|
||||
// if we don't have a file, then definitely check it.
|
||||
const st = await stat(f).catch(() => ({ mtime: t - 1 }))
|
||||
return t > st.mtime
|
||||
}
|
||||
|
||||
const updateNotifier = async (npm, spec = 'latest') => {
|
||||
// never check for updates in CI, when updating npm already, or opted out
|
||||
if (!npm.config.get('update-notifier') ||
|
||||
isGlobalNpmUpdate(npm) ||
|
||||
ciDetect()) {
|
||||
return SKIP
|
||||
}
|
||||
|
||||
// if we're on a prerelease train, then updates are coming fast
|
||||
// check for a new one daily. otherwise, weekly.
|
||||
const { version } = npm
|
||||
const current = semver.parse(version)
|
||||
|
||||
// if we're on a beta train, always get the next beta
|
||||
if (current.prerelease.length) {
|
||||
spec = `^${version}`
|
||||
}
|
||||
|
||||
// while on a beta train, get updates daily
|
||||
const duration = spec !== 'latest' ? DAILY : WEEKLY
|
||||
|
||||
// if we've already checked within the specified duration, don't check again
|
||||
if (!(await checkTimeout(npm, duration))) {
|
||||
return null
|
||||
}
|
||||
|
||||
// if they're currently using a prerelease, nudge to the next prerelease
|
||||
// otherwise, nudge to latest.
|
||||
const useColor = npm.logColor
|
||||
|
||||
const mani = await pacote.manifest(`npm@${spec}`, {
|
||||
// always prefer latest, even if doing --tag=whatever on the cmd
|
||||
defaultTag: 'latest',
|
||||
...npm.flatOptions,
|
||||
}).catch(() => null)
|
||||
|
||||
// if pacote failed, give up
|
||||
if (!mani) {
|
||||
return null
|
||||
}
|
||||
|
||||
const latest = mani.version
|
||||
|
||||
// if the current version is *greater* than latest, we're on a 'next'
|
||||
// and should get the updates from that release train.
|
||||
// Note that this isn't another http request over the network, because
|
||||
// the packument will be cached by pacote from previous request.
|
||||
if (semver.gt(version, latest) && spec === 'latest') {
|
||||
return updateNotifier(npm, `^${version}`)
|
||||
}
|
||||
|
||||
// if we already have something >= the desired spec, then we're done
|
||||
if (semver.gte(version, latest)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// ok! notify the user about this update they should get.
|
||||
// The message is saved for printing at process exit so it will not get
|
||||
// lost in any other messages being printed as part of the command.
|
||||
const update = semver.parse(mani.version)
|
||||
const type = update.major !== current.major ? 'major'
|
||||
: update.minor !== current.minor ? 'minor'
|
||||
: update.patch !== current.patch ? 'patch'
|
||||
: 'prerelease'
|
||||
const typec = !useColor ? type
|
||||
: type === 'major' ? chalk.red(type)
|
||||
: type === 'minor' ? chalk.yellow(type)
|
||||
: chalk.green(type)
|
||||
const oldc = !useColor ? current : chalk.red(current)
|
||||
const latestc = !useColor ? latest : chalk.green(latest)
|
||||
const changelog = `https://github.com/npm/cli/releases/tag/v${latest}`
|
||||
const changelogc = !useColor ? `<${changelog}>` : chalk.cyan(changelog)
|
||||
const cmd = `npm install -g npm@${latest}`
|
||||
const cmdc = !useColor ? `\`${cmd}\`` : chalk.green(cmd)
|
||||
const message = `\nNew ${typec} version of npm available! ` +
|
||||
`${oldc} -> ${latestc}\n` +
|
||||
`Changelog: ${changelogc}\n` +
|
||||
`Run ${cmdc} to update!\n`
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// only update the notification timeout if we actually finished checking
|
||||
module.exports = async npm => {
|
||||
const notification = await updateNotifier(npm)
|
||||
|
||||
// dont write the file if we skipped checking altogether
|
||||
if (notification === SKIP) {
|
||||
return null
|
||||
}
|
||||
|
||||
// intentional. do not await this. it's a best-effort update. if this
|
||||
// fails, it's ok. might be using /dev/null as the cache or something weird
|
||||
// like that.
|
||||
writeFile(lastCheckedFile(npm), '').catch(() => {})
|
||||
return notification
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// compares the inventory of package items in the tree
|
||||
// that is about to be installed (idealTree) with the inventory
|
||||
// of items stored in the package-lock file (virtualTree)
|
||||
//
|
||||
// Returns empty array if no errors found or an array populated
|
||||
// with an entry for each validation error found.
|
||||
function validateLockfile (virtualTree, idealTree) {
|
||||
const errors = []
|
||||
|
||||
// loops through the inventory of packages resulted by ideal tree,
|
||||
// for each package compares the versions with the version stored in the
|
||||
// package-lock and adds an error to the list in case of mismatches
|
||||
for (const [key, entry] of idealTree.entries()) {
|
||||
const lock = virtualTree.get(key)
|
||||
|
||||
if (!lock) {
|
||||
errors.push(`Missing: ${entry.name}@${entry.version} from lock file`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.version !== lock.version) {
|
||||
errors.push(`Invalid: lock file's ${lock.name}@${lock.version} does ` +
|
||||
`not satisfy ${entry.name}@${entry.version}`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
module.exports = validateLockfile
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
const EventEmitter = require('events')
|
||||
const { webAuthCheckLogin } = require('npm-profile')
|
||||
|
||||
async function webAuth (opener, initialUrl, doneUrl, opts) {
|
||||
const doneEmitter = new EventEmitter()
|
||||
|
||||
const openPromise = opener(initialUrl, doneEmitter)
|
||||
const webAuthCheckPromise = webAuthCheckLogin(doneUrl, { ...opts, cache: false })
|
||||
.then(authResult => {
|
||||
// cancel open prompt if it's present
|
||||
doneEmitter.emit('abort')
|
||||
|
||||
return authResult.token
|
||||
})
|
||||
|
||||
await openPromise
|
||||
return await webAuthCheckPromise
|
||||
}
|
||||
|
||||
module.exports = webAuth
|
||||
Reference in New Issue
Block a user