Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/devtools-window-polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ws from 'ws';
const customGlobal = global as any;

// These things must exist before importing `react-devtools-core`
// eslint-disable-next-line n/no-unsupported-features/node-builtins
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unclear why this lint error just started to appear, but should be safe to ignore.

customGlobal.WebSocket ||= ws;

customGlobal.window ||= global;
Expand Down
10 changes: 8 additions & 2 deletions src/ink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default class Ink {
// Ignore last render after unmounting a tree to prevent empty output before exit
private isUnmounted: boolean;
private lastOutput: string;
private lastOutputHeight: number;
private readonly container: FiberRoot;
private readonly rootNode: dom.DOMElement;
// This variable is used only in debug mode to store full static output
Expand Down Expand Up @@ -71,6 +72,7 @@ export default class Ink {

// Store last output to only rerender when needed
this.lastOutput = '';
this.lastOutputHeight = 0;

// This variable is used only in debug mode to store full static output
// so that it's rerendered every time, not just new static parts, like in non-debug mode
Expand Down Expand Up @@ -163,18 +165,21 @@ export default class Ink {
}

this.lastOutput = output;
this.lastOutputHeight = outputHeight;
return;
}

if (hasStaticOutput) {
this.fullStaticOutput += staticOutput;
}

if (outputHeight >= this.options.stdout.rows) {
if (this.lastOutputHeight >= this.options.stdout.rows) {
this.options.stdout.write(
ansiEscapes.clearTerminal + this.fullStaticOutput + output,
ansiEscapes.clearTerminal + this.fullStaticOutput + output + '\n',
);
this.lastOutput = output;
this.lastOutputHeight = outputHeight;
this.log.sync(output);
return;
}

Expand All @@ -190,6 +195,7 @@ export default class Ink {
}

this.lastOutput = output;
this.lastOutputHeight = outputHeight;
};

render(node: ReactNode): void {
Expand Down
7 changes: 7 additions & 0 deletions src/log-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cliCursor from 'cli-cursor';
export type LogUpdate = {
clear: () => void;
done: () => void;
sync: (str: string) => void;
(str: string): void;
};

Expand Down Expand Up @@ -45,6 +46,12 @@ const create = (stream: Writable, {showCursor = false} = {}): LogUpdate => {
}
};

render.sync = (str: string) => {
const output = str + '\n';
previousOutput = output;
previousLineCount = output.split('\n').length;
};

return render;
};

Expand Down
32 changes: 32 additions & 0 deletions test/fixtures/erase-with-state-change.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import process from 'node:process';
import React, {useEffect, useState} from 'react';
import {Box, Text, render} from '../../src/index.js';

function Erase() {
const [show, setShow] = useState(true);

useEffect(() => {
const timer = setTimeout(() => {
setShow(false);
});

return () => {
clearTimeout(timer);
};
}, []);

return (
<Box flexDirection="column">
{show && (
<>
<Text>A</Text>
<Text>B</Text>
<Text>C</Text>
</>
)}
</Box>
);
}

process.stdout.rows = Number(process.argv[2]);
render(<Erase />);
23 changes: 23 additions & 0 deletions test/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,29 @@ test.serial(
},
);

test.serial('erase screen where state changes', async t => {
const ps = term('erase-with-state-change', ['4']);
await ps.waitForExit();

const secondFrame = ps.output.split(ansiEscapes.eraseLines(3))[1];

for (const letter of ['A', 'B', 'C']) {
t.false(secondFrame?.includes(letter));
}
});

test.serial('erase screen where state changes in small viewport', async t => {
const ps = term('erase-with-state-change', ['3']);
await ps.waitForExit();

const frames = ps.output.split(ansiEscapes.clearTerminal);
const lastFrame = frames.at(-1);

for (const letter of ['A', 'B', 'C']) {
t.false(lastFrame?.includes(letter));
}
});

test.serial('clear output', async t => {
const ps = term('clear');
await ps.waitForExit();
Expand Down