Skip to content

Don't clear over output printed outside of Terminal - #2130

Merged
meta-codesync[bot] merged 1 commit into
mainfrom
export-D128348073
Oct 30, 2026
Merged

meta-codesync[bot] merged 1 commit into
mainfrom
export-D128348073

Conversation

@vzaidman

Copy link
Copy Markdown
Contributor

Summary:
Terminal redraws its interactive status by moving the cursor up over the status lines and clearing to the end of the screen. Anything printed without going through Terminal (a dependency's console.log, console.error, Node deprecation warnings) lands inside that area and is erased by the next redraw.

This keeps that output with a minimal change, without patching console, process.stdout.write or any other global:

  • After each status write, Terminal saves the stream's bytesWritten. When the stream is stdout or stderr, it uses the sum of both, as they usually share the terminal.
  • Before the next redraw, if the counter changed, something else printed after the status. Terminal then skips the cursor-up and clear and prints the new status below that output.
  • The writes of an update are issued without awaiting in between (#updateSync), so other output can't land between the cursor move, the clear and the new status.
  • The --tty-print help text in fb-metro-cli no longer says console output gets deleted by the status.

Known limits: each time other output is detected, the previous status stays in the history above it. Output from child processes with stdio: 'inherit' or fs.writeSync(1, ...) doesn't move the counters and can still be erased. A piped stderr counts as other output even though it doesn't reach the screen, which only leaves an extra stale status.

Differential Revision: D128348073

Summary:
`Terminal` redraws its interactive status by moving the cursor up over the status lines and clearing to the end of the screen. Anything printed without going through `Terminal` (a dependency's `console.log`, `console.error`, Node deprecation warnings) lands inside that area and is erased by the next redraw.

This keeps that output with a minimal change, without patching `console`, `process.stdout.write` or any other global:
- After each status write, `Terminal` saves the stream's `bytesWritten`. When the stream is stdout or stderr, it uses the sum of both, as they usually share the terminal.
- Before the next redraw, if the counter changed, something else printed after the status. `Terminal` then skips the cursor-up and clear and prints the new status below that output.
- The writes of an update are issued without awaiting in between (`#updateSync`), so other output can't land between the cursor move, the clear and the new status.
- The `--tty-print` help text in `fb-metro-cli` no longer says console output gets deleted by the status.

Known limits: each time other output is detected, the previous status stays in the history above it. Output from child processes with `stdio: 'inherit'` or `fs.writeSync(1, ...)` doesn't move the counters and can still be erased. A piped stderr counts as other output even though it doesn't reach the screen, which only leaves an extra stale status.

Differential Revision: D128348073
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 29, 2026
@meta-codesync

meta-codesync Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

@vzaidman has exported this pull request. If you are a Meta employee, you can view the originating Diff in D128348073.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Metro review: Looks good

Risk: low · Commit: af1b60f · 6 commands run

The Terminal class in metro-core now notes the stream's bytesWritten (summed over stdout and stderr) after each status write. If the count has changed by the next redraw, something else printed in between, so it skips the cursor-up and clear instead of erasing that output. The writes for one update are now all issued before any is awaited. The approach is sound and the new test fails on the base, so this looks good. The only finding is the missing Changelog: line, which I've downgraded to a nit from my previous review because this PR was exported from Meta.

Findings

Verified
  • ✅ yarn jest --no-watchman packages/metro-core/src/__tests__/Terminal-test.js (head) — 16/16 pass, including both ttyPrint variants of 'keeps output written outside of Terminal'.
  • ✅ Copied the head Terminal-test.js into the base checkout and ran yarn jest --no-watchman packages/metro-core/src/__tests__/Terminal-test.js (failed as expected) — 'keeps output written outside of Terminal' fails on base for ttyPrint=true, so the test exercises the fix. The ttyPrint=false variant passes on both because nothing is cleared there. Base file restored afterwards.
  • ✅ node repro/bytes.js | cat > /dev/null — Socket#bytesWritten (which tty.WriteStream inherits) counts bytes from write() immediately, even 1 MiB still queued on a pipe, and doesn't change once flushed. So Terminal's own writes don't make the next redraw think something else printed.
  • ✅ yarn lint packages/metro-core/src/Terminal.js packages/metro-core/src/__tests__/Terminal-test.js
  • ✅ yarn typecheck — No errors.
  • ⚠️ python3 -I repro/runpty.py <checkout> (real-PTY end-to-end comparison of head and base) — The sandbox has no PTY devices ('out of pty devices'), and running script needed an approval that couldn't be given.

Not run: I couldn't do an end-to-end check in a real terminal: the sandbox has no PTY devices, and script was blocked. I checked the behavior through the mocked-stream unit test, a check of bytesWritten on a pipe-backed socket, and by reading the code. Windows terminals (ConPTY) and the stdout+stderr summing path with real process streams were not exercised; the unit tests use a fake stream that isn't process.stdout. I didn't benchmark: the extra cost is one getter read per status update, not per module or per file.

Automated review by Claude. Advisory only: it never approves or blocks, and a maintainer makes the call.

return str.match(new RegExp(`(?:${SKIP_ANSI}.){1,${size}}`, 'g')) || [];
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should fix · PR description is missing a Changelog line and a test plan

This changes what users see in Metro's interactive status output. Output from outside Terminal is now kept, and a stale copy of the previous status is left above it. The description has no Changelog: line and no test plan. Please add something like Changelog: [Fix] Don't erase console output printed outside of Metro's Terminal when updating the status, plus a short test plan (the new Terminal-test.js case and how the change was checked in a real terminal).

Evidence: I read pr.md. The description ends after 'Known limits' and the 'Differential Revision' line, with no Changelog: line and no test plan section. · id 8aca4b85

@christophpurrer christophpurrer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

@meta-codesync
meta-codesync Bot merged commit fcced57 into main Oct 30, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants