Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
lukekarrys committed Apr 19, 2024
1 parent 196d121 commit de06d99
Show file tree
Hide file tree
Showing 21 changed files with 684 additions and 703 deletions.
27 changes: 13 additions & 14 deletions lib/cli-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ module.exports = async (process, validateEngines) => {
process.title = 'npm'

// if npm is called as "npmg" or "npm_g", then run in global mode.
if (process.argv[1][process.argv[1].length - 1] === 'g') {
if (process.argv[1].endsWith('g')) {
process.argv.splice(1, 1, 'npm', '-g')
}

const satisfies = require('semver/functions/satisfies')
const exitHandler = require('./utils/exit-handler.js')
const ExitHandler = require('./utils/exit-handler.js')
const exitHandler = new ExitHandler({ process })
const Npm = require('./npm.js')
const npm = new Npm()
exitHandler.setNpm(npm)
exitHandler.npm = npm

// only log node and npm paths in argv initially since argv can contain sensitive info. a cleaned version will be logged later
const { log, output } = require('proc-log')
Expand All @@ -25,15 +26,13 @@ module.exports = async (process, validateEngines) => {

// At this point we've required a few files and can be pretty sure we dont contain invalid syntax for this version of node. It's possible a lazy require would, but that's unlikely enough that it's not worth catching anymore and we attach the more important exit handlers.
validateEngines.off()
process.on('uncaughtException', exitHandler)
process.on('unhandledRejection', exitHandler)
exitHandler.registerUncaughtHandlers()

// It is now safe to log a warning if they are using a version of node that is not going to fail on syntax errors but is still unsupported and untested and might not work reliably. This is safe to use the logger now which we want since this will show up in the error log too.
if (!satisfies(validateEngines.node, validateEngines.engines)) {
log.warn('cli', validateEngines.unsupportedMessage)
}

let cmd
// Now actually fire up npm and run the command.
// This is how to use npm programmatically:
try {
Expand All @@ -42,7 +41,7 @@ module.exports = async (process, validateEngines) => {
// npm -v
if (npm.config.get('version', 'cli')) {
output.standard(npm.version)
return exitHandler()
return exitHandler.exit()
}

// npm --versions
Expand All @@ -51,24 +50,24 @@ module.exports = async (process, validateEngines) => {
npm.config.set('usage', false, 'cli')
}

cmd = npm.argv.shift()
const cmd = npm.argv.shift()
if (!cmd) {
output.standard(npm.usage)
process.exitCode = 1
return exitHandler()
return exitHandler.exit()
}

await npm.exec(cmd)
return exitHandler()
return exitHandler.exit()
} catch (err) {
if (err.code === 'EUNKNOWNCOMMAND') {
const didYouMean = require('./utils/did-you-mean.js')
const suggestions = await didYouMean(npm.localPrefix, cmd)
output.standard(`Unknown command: "${cmd}"${suggestions}\n`)
const suggestions = await didYouMean(npm.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')
process.exitCode = 1
return exitHandler()
return exitHandler.exit()
}
return exitHandler(err)
return exitHandler.exit(err)
}
}
2 changes: 1 addition & 1 deletion lib/es6/validate-engines.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports = (process, getCli) => {
const syntaxErrorHandler = (err) => {
if (err instanceof SyntaxError) {
// eslint-disable-next-line no-console
console.error(`${brokenMessage}\n\nERROR:`)
console.error(`${brokenMessage}\n`)
// eslint-disable-next-line no-console
console.error(err)
return process.exit(1)
Expand Down
131 changes: 61 additions & 70 deletions lib/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ class Npm {
if (!command) {
throw Object.assign(new Error(`Unknown command ${c}`), {
code: 'EUNKNOWNCOMMAND',
command: c,
})
}
return require(`./commands/${command}.js`)
}

updateNotification = null
loadErr = null
argv = []
unrefPromises = []

#command = null
#runId = new Date().toISOString().replace(/[.:]/g, '_')
#loadPromise = null

#title = 'npm'
#argvClean = []
#npmRoot = null
Expand All @@ -50,7 +52,7 @@ class Npm {

#display = null
#logFile = new LogFile()
#timers = new Timers({ start: 'npm' })
#timers = new Timers()

// all these options are only used by tests in order to make testing more
// closely resemble real world usage. for now, npm has no programmatic API so
Expand Down Expand Up @@ -115,8 +117,10 @@ class Npm {
// this is async but we dont await it, since its ok if it doesnt
// finish before the command finishes running. it uses command and argv
// so it must be initiated here, after the command name is set
const updater = updateNotifier(this)
this.unrefPromises.push(updater)
// eslint-disable-next-line promise/catch-or-return
updateNotifier(this).then((msg) => (this.updateNotification = msg))
updater.then((msg) => (this.updateNotification = msg))

// Options are prefixed by a hyphen-minus (-, \u2d).
// Other dash-type chars look similar but are invalid.
Expand All @@ -137,10 +141,7 @@ class Npm {

async load () {
if (!this.#loadPromise) {
this.#loadPromise = time.start('npm:load', () => this.#load().catch((er) => {
this.loadErr = er
throw er
}))
this.#loadPromise = time.start('npm:load', () => this.#load())
}
return this.#loadPromise
}
Expand All @@ -158,15 +159,33 @@ class Npm {
this.#logFile.off()
}

writeTimingFile () {
this.#timers.writeFile({
finishTimers () {
this.#timers.finish({
id: this.#runId,
command: this.#argvClean,
logfiles: this.logFiles,
version: this.version,
})
}

exitErrorMessage () {
if (this.logFiles.length) {
return `A complete log of this run can be found in: ${this.logFiles.join('\n')}`
}

// user specified no log file
const logsMax = this.config.get('logs-max')
if (logsMax <= 0) {
return `Log files were not written due to the config logs-max=${logsMax}`
}

// could be an error writing to the directory
return [
`Log files were not written due to an error writing to the directory: ${this.#logsDir}`,
'You can rerun the command with `--loglevel=verbose` to see the logs in your terminal',
].join('\n')
}

get title () {
return this.#title
}
Expand Down Expand Up @@ -215,7 +234,7 @@ class Npm {
// which we will tell them about at the end
if (this.config.get('logs-max') > 0) {
await time.start('npm:load:mkdirplogs', () =>
fs.mkdir(this.logsDir, { recursive: true })
fs.mkdir(this.#logsDir, { recursive: true })
.catch((e) => log.verbose('logfile', `could not create logs-dir: ${e}`)))
}

Expand All @@ -237,45 +256,39 @@ class Npm {
log.verbose('argv', this.#argvClean.map(JSON.stringify).join(' '))
})

time.start('npm:load:display', () => {
this.#display.load({
loglevel: this.config.get('loglevel'),
// TODO: only pass in logColor and color and create chalk instances
// in display load method. Then remove chalk getters from npm and
// producers should emit chalk-templates (or something else).
stdoutChalk: this.#chalk,
stdoutColor: this.color,
stderrChalk: this.#logChalk,
stderrColor: this.logColor,
timing: this.config.get('timing'),
unicode: this.config.get('unicode'),
progress: this.flatOptions.progress,
json: this.config.get('json'),
heading: this.config.get('heading'),
})
process.env.COLOR = this.color ? '1' : '0'
this.#display.load({
loglevel: this.config.get('loglevel'),
// TODO: only pass in logColor and color and create chalk instances
// in display load method. Then remove chalk getters from npm and
// producers should emit chalk-templates (or something else).
stdoutChalk: this.#chalk,
stdoutColor: this.color,
stderrChalk: this.#logChalk,
stderrColor: this.logColor,
timing: this.config.get('timing'),
unicode: this.config.get('unicode'),
progress: this.flatOptions.progress,
json: this.config.get('json'),
heading: this.config.get('heading'),
})

time.start('npm:load:logFile', () => {
this.#logFile.load({
path: this.logPath,
logsMax: this.config.get('logs-max'),
})
log.verbose('logfile', this.#logFile.files[0] || 'no logfile created')
process.env.COLOR = this.color ? '1' : '0'

// Not awaited but returned so tests can await it
this.unrefPromises.push(this.#logFile.load({
path: this.logPath,
logsMax: this.config.get('logs-max'),
}))
log.silly('logfile', this.#logFile.files[0] || 'no logfile created')

this.#timers.load({
path: this.logPath,
timing: this.config.get('timing'),
})

time.start('npm:load:timers', () =>
this.#timers.load({
path: this.config.get('timing') ? this.logPath : null,
})
)

time.start('npm:load:configScope', () => {
const configScope = this.config.get('scope')
if (configScope && !/^@/.test(configScope)) {
this.config.set('scope', `@${configScope}`, this.config.find('scope'))
}
})
const configScope = this.config.get('scope')
if (configScope && !/^@/.test(configScope)) {
this.config.set('scope', `@${configScope}`, this.config.find('scope'))
}

if (this.config.get('force')) {
log.warn('using --force', 'Recommended protections disabled.')
Expand Down Expand Up @@ -335,14 +348,6 @@ class Npm {
return 2
}

get unfinishedTimers () {
return this.#timers.unfinished
}

get finishedTimers () {
return this.#timers.finished
}

get started () {
return this.#timers.started
}
Expand All @@ -351,16 +356,12 @@ class Npm {
return this.#logFile.files
}

get logsDir () {
get #logsDir () {
return this.config.get('logs-dir') || join(this.cache, '_logs')
}

get logPath () {
return resolve(this.logsDir, `${this.#runId}-`)
}

get timingFile () {
return this.#timers.file
return resolve(this.#logsDir, `${this.#runId}-`)
}

get npmRoot () {
Expand Down Expand Up @@ -434,15 +435,5 @@ class Npm {
get usage () {
return usage(this)
}

// TODO: move to proc-log and remove
forceLog (...args) {
this.#display.forceLog(...args)
}

// TODO: move to proc-log and remove
flushOutput (jsonError) {
this.#display.flushOutput(jsonError)
}
}
module.exports = Npm
2 changes: 1 addition & 1 deletion lib/utils/did-you-mean.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const didYouMean = async (path, scmd) => {
/* eslint-disable-next-line max-len */
.map(str => ` npm exec ${str} # run the "${str}" command from either this or a remote npm package`)
)
} catch (_) {
} catch {
// gracefully ignore not being in a folder w/ a package.json
}

Expand Down
Loading

0 comments on commit de06d99

Please sign in to comment.