Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
exit-handler.js169 linesDownload Raw Back to cli
1const { log, output, META } = require('proc-log')
2const { errorMessage, getExitCodeFromError } = require('../utils/error-message.js')
3
4class ExitHandler {
5  #npm = null
6  #process = null
7  #exited = false
8  #exitErrorMessage = false
9
10  #noNpmError = false
11
12  get #hasNpm () {
13    return !!this.#npm
14  }
15
16  get #loaded () {
17    return !!this.#npm?.loaded
18  }
19
20  get #showExitErrorMessage () {
21    if (!this.#loaded) {
22      return false
23    }
24    if (!this.#exited) {
25      return true
26    }
27    return this.#exitErrorMessage
28  }
29
30  get #notLoadedOrExited () {
31    return !this.#loaded && !this.#exited
32  }
33
34  setNpm (npm) {
35    this.#npm = npm
36  }
37
38  constructor ({ process }) {
39    this.#process = process
40    this.#process.on('exit', this.#handleProcessExitAndReset)
41  }
42
43  registerUncaughtHandlers () {
44    this.#process.on('uncaughtException', this.#handleExit)
45    this.#process.on('unhandledRejection', this.#handleExit)
46  }
47
48  exit (err) {
49    this.#handleExit(err)
50  }
51
52  #handleProcessExitAndReset = (code) => {
53    this.#handleProcessExit(code)
54
55    // Reset all the state. This is only relevant for tests since in reality the process fully exits here.
56    this.#process.off('exit', this.#handleProcessExitAndReset)
57    this.#process.off('uncaughtException', this.#handleExit)
58    this.#process.off('unhandledRejection', this.#handleExit)
59    if (this.#loaded) {
60      this.#npm.unload()
61    }
62    this.#npm = null
63    this.#exited = false
64    this.#exitErrorMessage = false
65  }
66
67  #handleProcessExit (code) {
68    const numCode = Number(code) || 0
69    // Always exit w/ a non-zero code if exit handler was not called
70    const exitCode = this.#exited ? numCode : (numCode || 1)
71    this.#process.exitCode = exitCode
72
73    if (this.#notLoadedOrExited) {
74      // Exit handler was not called and npm was not loaded so we have to log something
75      this.#logConsoleError(new Error(`Process exited unexpectedly with code: ${exitCode}`))
76      return
77    }
78
79    if (this.#logNoNpmError()) {
80      return
81    }
82
83    const os = require('node:os')
84    log.verbose('cwd', this.#process.cwd())
85    log.verbose('os', `${os.type()} ${os.release()}`)
86    log.verbose('node', this.#process.version)
87    log.verbose('npm ', `v${this.#npm.version}`)
88
89    // only show the notification if it finished
90    if (typeof this.#npm.updateNotification === 'string') {
91      log.notice('', this.#npm.updateNotification, { [META]: true, force: true })
92    }
93
94    if (!this.#exited) {
95      log.error('', 'Exit handler never called!')
96      log.error('', 'This is an error with npm itself. Please report this error at:')
97      log.error('', '  <https://github.com/npm/cli/issues>')
98      if (this.#npm.silent) {
99        output.error('')
100      }
101    }
102
103    log.verbose('exit', exitCode)
104
105    if (exitCode) {
106      log.verbose('code', exitCode)
107    } else {
108      log.info('ok')
109    }
110
111    if (this.#showExitErrorMessage) {
112      log.error('', this.#npm.exitErrorMessage())
113    }
114  }
115
116  #logConsoleError (err) {
117    // Run our error message formatters on all errors even if we have no npm or an unloaded npm.
118    // This will clean the error and possible return a formatted message about EACCESS or something.
119    const { summary, detail } = errorMessage(err, this.#npm)
120    const formatted = [...new Set([...summary, ...detail].flat().filter(Boolean))].join('\n')
121    // If we didn't get anything from the formatted message then just display the full stack
122    // eslint-disable-next-line no-console
123    console.error(formatted === err.message ? err.stack : formatted)
124  }
125
126  #logNoNpmError (err) {
127    if (this.#hasNpm) {
128      return false
129    }
130    // Make sure we only log this error once
131    if (!this.#noNpmError) {
132      this.#noNpmError = true
133      this.#logConsoleError(
134        new Error(`Exit prior to setting npm in exit handler`, err ? { cause: err } : {})
135      )
136    }
137    return true
138  }
139
140  #handleExit = (err) => {
141    this.#exited = true
142
143    // No npm at all
144    if (this.#logNoNpmError(err)) {
145      return this.#process.exit(this.#process.exitCode || getExitCodeFromError(err) || 1)
146    }
147
148    // npm was never loaded but we still might have a config loading error or something similar that we can run through the error message formatter to give the user a clue as to what happened.
149    if (!this.#loaded) {
150      this.#logConsoleError(new Error('Exit prior to config file resolving', { cause: err }))
151      return this.#process.exit(this.#process.exitCode || getExitCodeFromError(err) || 1)
152    }
153
154    this.#exitErrorMessage = err?.suppressError === true ? false : !!err
155
156    // Prefer the exit code of the error, then the current process exit code, then set it to 1 if we still have an error.
157    // Otherwise, we call process.exit with undefined so that it can determine the final exit code
158    const exitCode = err?.exitCode ?? this.#process.exitCode ?? (err ? 1 : undefined)
159
160    // explicitly call process.exit now so we don't hang on things like the update notifier
161    // also flush stdout/err beforehand because process.exit doesn't wait for that to happen.
162    this.#process.stderr.write('', () => this.#process.stdout.write('', () => {
163      this.#process.exit(exitCode)
164    }))
165  }
166}
167
168module.exports = ExitHandler
169 
codekingpro/portable-devtools · Team Ai