cli icon indicating copy to clipboard operation
cli copied to clipboard

[BUG] `npm run` erases script output after the final newline

Open mkantor opened this issue 3 months ago • 5 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

This issue exists in the latest npm version

  • [x] I am using the latest npm

Current Behavior

Any script which writes to standard output but does not emit a \n as its final character will have output erased when npm run exits.

Expected Behavior

npm run should never interfere with the output of scripts.

Steps To Reproduce

  1. Create a package.json file like this:
    {
      "name": "",
      "version": "0.0.0",
      "scripts": {
        "start": "printf 'this output will vanish'; sleep 3"
      }
    }
    
  2. Run npm run start.
  3. Observe that the output vanishes after three seconds.

Environment

  • npm -v: 11.6.0
  • node -v: v22.18.0
  • OS Name: macOS 15.6
  • System Model Name: MacBook Pro
  • npm config:
    //localhost:4873/:_authToken = (protected)
    //registry.npmjs.org/:_authToken = (protected)
    

mkantor avatar Sep 19 '25 15:09 mkantor

The problem is related to progress output. The behavior does not occur when the --no-progress or --silent options are provided to npm run.

mkantor avatar Sep 19 '25 15:09 mkantor

Here are the relevant implementation details of an npm run execution as I've managed to understand them so far:

  1. Npm's constructor instantiates a Display object
  2. The user's script is executed and may produce output
  3. Something (I haven't tracked this down) emits an 'input' event with the payload 'end', which causes Display's #inputHandler method to be called
  4. Display's #inputHandler method calls Progress's resume method
  5. Progress's resume method calls its #render method without arguments
  6. Since #render was called without arguments, #renderSpinner is immediately called
  7. Progress's #renderSpinner proceeds to call #renderFrame, because #rendering is true
  8. Progress's #renderFrame calls #clearSpinner
  9. Progress's #clearSpinner deletes the entire current line

It strikes me as odd that #rendering in Progress is true in this case, as no progress indicator is visible at the time when #renderSpinner is called (but I might be misinterpreting what #rendering is supposed to mean).

mkantor avatar Sep 19 '25 15:09 mkantor

Are there any real scenarios where npm run would display a progress indicator after the script finishes?

mkantor avatar Sep 19 '25 15:09 mkantor

The input event comes from @npmcli/run-script https://github.com/npm/run-script/blob/b26e154be73f82ecd693b5c4395d3f3da716ca1c/lib/run-script-pkg.js#L63

It was added in https://github.com/npm/run-script/pull/202 and was meant to be a way to tell npm to stop its own output (including the spinner) while userland scripts ran.

Are there any real scenarios where npm run would display a progress indicator after the script finishes?

The progress indicator is meant to signal to users that npm is still running and hasn't frozen. If some part of the npm process after npm run takes a while to run, the spinner would show that things haven't frozen. I don't know that the use case there is as important as npm install which is where this is much more likely to happen.

There is no way for npm to know what was displayed during npm run as it does not pass through npm's display layer. I believe at one time an extra newline was output at the end of npm run to mitigate things like this. If we were to re-add this today it would probably want to go in the display layer's #inputHandler.

To add to the complexity here, there is another PR in flight that is on my plate to review and iterate on that solves a problem in the same domain space: https://github.com/npm/cli/pull/8322. There is a race condition when prompting for multiple inputs that turns the spinner back on. I don't think the solutions overlap specifically but the code is likely to intersect at least in some way. That PR already has conflicts w/ latest and will have even more once https://github.com/npm/cli/pull/8633 lands (which is some cleanup based on researching this issue).

wraithgar avatar Oct 02 '25 16:10 wraithgar

at one time an extra newline was output at the end of npm run to mitigate things like this

The context has mostly left my brain by now so I don't know if this actually makes sense, but while debugging this I recall thinking that maybe a simple "fix" would be for npm to print a newline before the spinner.

mkantor avatar Oct 02 '25 17:10 mkantor