Skip to content

Commit

Permalink
fix(view): output full json object with errors with workspaces
Browse files Browse the repository at this point in the history
Fixes: #5444
  • Loading branch information
lukekarrys committed May 10, 2024
1 parent 8add914 commit 5556147
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 232 deletions.
12 changes: 7 additions & 5 deletions lib/commands/run-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ class RunScript extends BaseCommand {
return
}

const didYouMean = require('../utils/did-you-mean.js')
const suggestions = await didYouMean(path, event)
throw new Error(
`Missing script: "${event}"${suggestions}\n\nTo see a list of scripts, run:\n npm run`
)
const suggestions = require('../utils/did-you-mean.js')(pkg, event)
throw new Error([
`Missing script: "${event}"`,
...suggestions,
'To see a list of scripts, run:',
' npm run',
].join('\n'))
}

// positional args only added to the main event, not pre/post
Expand Down
155 changes: 77 additions & 78 deletions lib/commands/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { inspect } = require('util')
const { packument } = require('pacote')
const Queryable = require('../utils/queryable.js')
const BaseCommand = require('../base-cmd.js')
const { getError, jsonError, outputError } = require('../utils/error-message.js')

const readJson = async file => jsonParse(await readFile(file, 'utf8'))

Expand Down Expand Up @@ -77,11 +78,7 @@ class View extends BaseCommand {
}

async exec (args) {
if (!args.length) {
args = ['.']
}
let pkg = args.shift()
const local = /^\.@/.test(pkg) || pkg === '.'
let { pkg, local, rest } = parseArgs(args)

if (local) {
if (this.npm.global) {
Expand All @@ -96,85 +93,81 @@ class View extends BaseCommand {
pkg = `${manifest.name}${pkg.slice(1)}`
}

let wholePackument = false
if (!args.length) {
args = ['']
wholePackument = true
await this.#viewPackage(pkg, rest)
}

async execWorkspaces (args) {
const { pkg, local, rest } = parseArgs(args)

if (!local) {
log.warn('Ignoring workspaces for specified package(s)')
return this.exec([pkg, ...rest])
}
const [pckmnt, data] = await this.getData(pkg, args)

if (!this.npm.config.get('json') && wholePackument) {
// pretty view (entire packument)
data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]]['']))
} else {
// JSON formatted output (JSON or specific attributes from packument)
let reducedData = data.reduce(reducer, {})
if (wholePackument) {
// No attributes
reducedData = cleanBlanks(reducedData)
log.silly('view', reducedData)
}
const json = this.npm.config.get('json')
await this.setWorkspaces()

const msg = await this.jsonData(reducedData, pckmnt._id)
if (msg !== '') {
output.standard(msg)
let hasError = false
for (const name of this.workspaceNames) {
try {
await this.#viewPackage(`${name}${pkg.slice(1)}`, rest, { multiple: true })
} catch (e) {
hasError = true
const err = getError(e)
if (json) {
output.buffer({ [name]: jsonError(err, this.npm) })
} else {
outputError(err)
}
}
}
}

async execWorkspaces (args) {
if (!args.length) {
args = ['.']
if (hasError) {
process.exitCode = 1
}
}

const pkg = args.shift()
async #viewPackage (name, args, { multiple = false } = {}) {
const wholePackument = !args.length
const json = this.npm.config.get('json')

const local = /^\.@/.test(pkg) || pkg === '.'
if (!local) {
log.warn('Ignoring workspaces for specified package(s)')
return this.exec([pkg, ...args])
// If we are viewing many packages output the name before doing
// any async activity
if (!json && multiple) {
output.standard(`${name}:`)
}
let wholePackument = false
if (!args.length) {
wholePackument = true
args = [''] // getData relies on this

const [pckmnt, data, version] = await this.#getData(name, wholePackument ? [''] : args)
if (wholePackument) {
pckmnt.version = version
}
const results = {}
await this.setWorkspaces()
for (const name of this.workspaceNames) {
const wsPkg = `${name}${pkg.slice(1)}`
const [pckmnt, data] = await this.getData(wsPkg, args)

let reducedData = data.reduce(reducer, {})
if (wholePackument) {
// No attributes
reducedData = cleanBlanks(reducedData)
log.silly('view', reducedData)
}

if (!this.npm.config.get('json')) {
if (wholePackument) {
data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]]['']))
} else {
output.standard(`${name}:`)
const msg = await this.jsonData(reducedData, pckmnt._id)
if (msg !== '') {
output.standard(msg)
}
}
} else {
const msg = await this.jsonData(reducedData, pckmnt._id)
if (msg !== '') {
results[name] = JSON.parse(msg)
}
// This already outputs to the terminal
if (!json && wholePackument) {
// pretty view (entire packument)
for (const v of data) {
this.#prettyView(pckmnt, v[Object.keys(v)[0]][''])
}
return
}

// JSON formatted output (JSON or specific attributes from packument)
let reducedData = data.reduce(reducer, {})
if (wholePackument) {
// No attributes
reducedData = cleanBlanks(reducedData)
log.silly('view', reducedData)
}
if (Object.keys(results).length > 0) {
output.standard(JSON.stringify(results, null, 2))

const msg = this.#packageOutput(reducedData, pckmnt._id)
if (json) {
output.buffer({ [name]: msg ? JSON.parse(msg) : null })
} else if (msg) {
output.standard(msg)
}
}

async getData (pkg, args) {
async #getData (pkg, args = []) {
const opts = {
...this.npm.flatOptions,
preferOnline: true,
Expand Down Expand Up @@ -242,18 +235,10 @@ class View extends BaseCommand {
throw er
}

if (
!this.npm.config.get('json') &&
args.length === 1 &&
args[0] === ''
) {
pckmnt.version = version
}

return [pckmnt, data]
return [pckmnt, data, version]
}

async jsonData (data, name) {
#packageOutput (data, name) {
const versions = Object.keys(data)
let msg = ''
let msgJson = []
Expand Down Expand Up @@ -313,7 +298,7 @@ class View extends BaseCommand {
return msg.trim()
}

prettyView (packu, manifest) {
#prettyView (packu, manifest) {
// More modern, pretty printing of default view
const unicode = this.npm.config.get('unicode')
const chalk = this.npm.chalk
Expand Down Expand Up @@ -409,6 +394,20 @@ class View extends BaseCommand {

module.exports = View

function parseArgs (args) {
if (!args.length) {
args = ['.']
}

const pkg = args.shift()

return {
pkg,
local: /^\.@/.test(pkg) || pkg === '.',
rest: args,
}
}

function cleanBlanks (obj) {
const clean = {}
Object.keys(obj).forEach((version) => {
Expand Down
98 changes: 25 additions & 73 deletions lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,109 +287,61 @@ class Npm {

async #handleError (err) {
if (err) {
Object.assign(err, await this.#getError(err))
// Get the local package if it exists for a more helpful error message
const localPkg = await require('@npmcli/package-json')
.normalize(this.localPrefix)
.then(p => p.content)
.catch(() => null)
Object.assign(err, this.#getError(err, { pkg: localPkg }))
}

// TODO: make this not need to be public
this.finish()

output.flush({
[META]: true,
jsonError: err && this.loaded && this.config.get('json') ? {
code: err.code,
summary: (err.summary || []).map(l => l.slice(1).join(' ')).join('\n'),
detail: (err.detail || []).map(l => l.slice(1).join(' ')).join('\n'),
} : null,
...require('./utils/error-message.js').jsonError(err, this),
})

if (err) {
throw err
}
}

async #getError (err) {
const { errorMessage, getExitCodeFromError } = require('./utils/error-message.js')

// 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?
if (this.#command?.constructor?.isShellout && typeof err.code === 'number' && err.code) {
return {
exitCode: err.code,
suppressError: true,
}
}

// XXX: we should stop throwing strings
if (typeof err === 'string') {
log.error('', err)
return {
exitCode: 1,
suppressError: true,
}
}

// XXX: we should stop throwing other non-errors
if (!(err instanceof Error)) {
log.error('weird error', err)
return {
exitCode: 1,
suppressError: true,
}
}

if (err.code === 'EUNKNOWNCOMMAND') {
const didYouMean = require('./utils/did-you-mean.js')
const suggestions = await didYouMean(this.localPrefix, err.command)
output.standard(`Unknown command: "${err.command}"${suggestions}\n`)
output.standard('To see a list of supported npm commands, run:\n npm help')
return {
exitCode: 1,
suppressError: true,
}
}

err.code ??= err.message.match(/^(?:Error: )?(E[A-Z]+)/)?.[1]

for (const k of ['type', 'stack', 'statusCode', 'pkgid']) {
const v = err[k]
if (v) {
log.verbose(k, replaceInfo(v))
}
}

const exitCode = getExitCodeFromError(err) || 1
const { summary, detail, files } = errorMessage(err, this)
#getError (rawErr, opts = {}) {
const { getError, outputError } = require('./utils/error-message.js')
const {
summary = [],
detail = [],
files = [],
standard = [],
verbose = [],
exitCode,
suppressError,
} = getError(rawErr, {
npm: this,
command: this.#command,
...opts,
})

const { writeFileSync } = require('node:fs')
for (const [file, content] of files) {
const filePath = `${this.logPath}${file}`
const fileContent = `'Log files:\n${this.logFiles.join('\n')}\n\n${content.trim()}\n`
try {
writeFileSync(filePath, fileContent)
require('node:fs').writeFileSync(filePath, fileContent)
detail.push(['', `\n\nFor a full report see:\n${filePath}`])
} catch (fileErr) {
log.warn('', `Could not write error message to ${file} due to ${fileErr}`)
}
}

for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) {
const v = err[k]
if (v) {
log.error(k, v)
}
}

for (const errline of [...summary, ...detail]) {
log.error(...errline)
}
outputError({ standard, verbose, summary, detail })

return {
exitCode,
summary,
detail,
suppressError: false,
suppressError,
}
}

Expand Down
Loading

0 comments on commit 5556147

Please sign in to comment.