Skip to content

Commit fd33490

Browse files
committed
Merge remote-tracking branch 'origin/main' into chore/upgrade-to-rn-83.3
2 parents 64dfb0d + b4178c2 commit fd33490

4 files changed

Lines changed: 169 additions & 62 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@callstack/repack": patch
3+
---
4+
5+
Preserve non-terminal stdout logs while interactive status is active, so plugin output lines are not cleared by status redraws.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,10 @@
3434
"is-in-ci": "^1.0.0",
3535
"turbo": "^2.8.10",
3636
"typescript": "catalog:"
37+
},
38+
"pnpm": {
39+
"overrides": {
40+
"caniuse-lite": "^1.0.30001774"
41+
}
3742
}
3843
}

packages/repack/src/logging/internal/terminal.ts

Lines changed: 152 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,16 @@ import util from 'node:util';
1818
import throttle from 'throttleit';
1919

2020
type UnderlyingStream = NodeJS.WritableStream;
21+
type StreamChunk = Buffer | Uint8Array | string;
2122

2223
const moveCursor = util.promisify(readline.moveCursor);
2324
const clearScreenDown = util.promisify(readline.clearScreenDown);
24-
const streamWrite = util.promisify(
25-
(
26-
stream: UnderlyingStream,
27-
chunk: Buffer | Uint8Array | string,
28-
callback?: (data: any) => void
29-
) => {
30-
return stream.write(chunk, callback);
31-
}
32-
);
25+
type WriteCallback = (error?: Error | null) => void;
26+
type ExternalWrite = {
27+
chunk: StreamChunk;
28+
encoding?: BufferEncoding;
29+
callback?: WriteCallback;
30+
};
3331

3432
/**
3533
* Cut a string into an array of string of the specific maximum size. A newline
@@ -94,8 +92,16 @@ class Terminal {
9492
_statusStr: string;
9593
_stream: UnderlyingStream;
9694
_ttyStream: tty.WriteStream | null;
95+
// Bound reference to the original stream.write. We keep this so our
96+
// interception layer can still delegate to the real writer.
97+
_rawStreamWrite: (...args: Array<any>) => boolean;
98+
// Writes performed outside Terminal.log/status while a status is visible.
99+
// We replay them in _update() so they are not erased by status redraws.
100+
_externalWrites: Array<ExternalWrite>;
97101
_updatePromise: Promise<void> | null;
98102
_isUpdating: boolean;
103+
// Guards our own cursor/status writes from being treated as "external".
104+
_isInternalWrite: boolean;
99105
_isPendingUpdate: boolean;
100106
_shouldFlush: boolean;
101107
_writeStatusThrottled: (status: string) => void;
@@ -109,14 +115,117 @@ class Terminal {
109115
this._statusStr = '';
110116
this._stream = stream;
111117
this._ttyStream = ttyPrint ? getTTYStream(stream) : null;
118+
this._rawStreamWrite = stream.write.bind(stream) as (
119+
...args: Array<any>
120+
) => boolean;
121+
this._externalWrites = [];
112122
this._updatePromise = null;
113123
this._isUpdating = false;
124+
this._isInternalWrite = false;
114125
this._isPendingUpdate = false;
115126
this._shouldFlush = false;
116-
this._writeStatusThrottled = throttle(
117-
(status) => this._stream.write(status),
118-
3500
119-
);
127+
this._writeStatusThrottled = throttle((status) => {
128+
this._writeRaw(status);
129+
}, 3500);
130+
131+
this._patchTTYStreamWrites();
132+
}
133+
134+
_patchTTYStreamWrites(): void {
135+
if (!this._ttyStream) {
136+
return;
137+
}
138+
139+
// In interactive TTY mode, status redraw uses cursor movement + clear.
140+
// Any direct stream.write from other sources (plugins, other loggers)
141+
// can be wiped by that redraw. Intercept those writes and route them
142+
// through _update() so they are persisted above the status line.
143+
this._stream.write = ((
144+
chunk: StreamChunk,
145+
encodingOrCallback?: BufferEncoding | WriteCallback,
146+
maybeCallback?: WriteCallback
147+
) => {
148+
const encoding =
149+
typeof encodingOrCallback === 'string' ? encodingOrCallback : undefined;
150+
const callback =
151+
typeof encodingOrCallback === 'function'
152+
? encodingOrCallback
153+
: maybeCallback;
154+
155+
const shouldCaptureExternalWrite =
156+
!this._isInternalWrite && this._hasVisibleStatus();
157+
158+
if (!shouldCaptureExternalWrite) {
159+
return this._writeRaw(chunk, encoding, callback);
160+
}
161+
162+
// Queue for replay in _update() before status is drawn again.
163+
this._externalWrites.push({ chunk, encoding, callback });
164+
this._scheduleUpdate();
165+
return true;
166+
}) as UnderlyingStream['write'];
167+
}
168+
169+
_writeRaw(
170+
chunk: StreamChunk,
171+
encoding?: BufferEncoding,
172+
callback?: WriteCallback
173+
): boolean {
174+
if (encoding !== undefined) {
175+
return this._rawStreamWrite(chunk, encoding, callback);
176+
}
177+
178+
if (callback) {
179+
return this._rawStreamWrite(chunk, callback);
180+
}
181+
182+
return this._rawStreamWrite(chunk);
183+
}
184+
185+
async _writeInternal(
186+
chunk: StreamChunk,
187+
encoding?: BufferEncoding
188+
): Promise<void> {
189+
// Wrap stream writes in a promise so _update() can preserve ordering:
190+
// clear old status -> logs/external writes -> new status.
191+
await new Promise<void>((resolve, reject) => {
192+
this._isInternalWrite = true;
193+
194+
const done: WriteCallback = (error) => {
195+
this._isInternalWrite = false;
196+
if (error) {
197+
reject(error);
198+
return;
199+
}
200+
resolve();
201+
};
202+
203+
try {
204+
this._writeRaw(chunk, encoding, done);
205+
} catch (error) {
206+
this._isInternalWrite = false;
207+
reject(error as Error);
208+
}
209+
});
210+
}
211+
212+
_hasVisibleStatus(): boolean {
213+
return this._statusStr.length > 0 || this._nextStatusStr.length > 0;
214+
}
215+
216+
async _clearCurrentStatus(
217+
ttyStream: tty.WriteStream,
218+
statusStr: string
219+
): Promise<void> {
220+
const statusLinesCount = statusStr.split('\n').length - 1;
221+
// extra -1 because we print the status with a trailing new line
222+
this._isInternalWrite = true;
223+
try {
224+
await moveCursor(ttyStream, -ttyStream.columns, -statusLinesCount - 1);
225+
await clearScreenDown(ttyStream);
226+
} finally {
227+
this._isInternalWrite = false;
228+
}
120229
}
121230

122231
/**
@@ -159,8 +268,8 @@ class Terminal {
159268
this._shouldFlush = true;
160269
}
161270
await this.waitForUpdates();
162-
// @ts-expect-error missing type
163-
this._writeStatusThrottled.flush();
271+
// @ts-expect-error missing type on throttle return
272+
this._writeStatusThrottled.flush?.();
164273
}
165274

166275
/**
@@ -175,29 +284,50 @@ class Terminal {
175284
const nextStatusStr = this._nextStatusStr;
176285
const statusStr = this._statusStr;
177286
const logLines = this._logLines;
287+
const externalWrites = this._externalWrites;
178288

179289
// reset these here to not have them changed while updating
180290
this._statusStr = nextStatusStr;
181291
this._logLines = [];
292+
this._externalWrites = [];
182293

183-
if (statusStr === nextStatusStr && logLines.length === 0) {
294+
if (
295+
statusStr === nextStatusStr &&
296+
logLines.length === 0 &&
297+
externalWrites.length === 0
298+
) {
184299
return;
185300
}
186301

187302
if (ttyStream && statusStr.length > 0) {
188-
const statusLinesCount = statusStr.split('\n').length - 1;
189-
// extra -1 because we print the status with a trailing new line
190-
await moveCursor(ttyStream, -ttyStream.columns, -statusLinesCount - 1);
191-
await clearScreenDown(ttyStream);
303+
await this._clearCurrentStatus(ttyStream, statusStr);
192304
}
193305

194306
if (logLines.length > 0) {
195-
await streamWrite(this._stream, logLines.join('\n') + '\n');
307+
await this._writeInternal(logLines.join('\n') + '\n');
308+
}
309+
310+
if (externalWrites.length > 0) {
311+
// Preserve third-party stdout lines by writing them after the clear and
312+
// before redrawing status. This keeps status live while avoiding "eaten"
313+
// plugin output lines.
314+
for (const externalWrite of externalWrites) {
315+
try {
316+
await this._writeInternal(
317+
externalWrite.chunk,
318+
externalWrite.encoding
319+
);
320+
externalWrite.callback?.(null);
321+
} catch (error) {
322+
externalWrite.callback?.(error as Error);
323+
throw error;
324+
}
325+
}
196326
}
197327

198328
if (ttyStream) {
199329
if (nextStatusStr.length > 0) {
200-
await streamWrite(this._stream, nextStatusStr + '\n');
330+
await this._writeInternal(nextStatusStr + '\n');
201331
}
202332
} else {
203333
this._writeStatusThrottled(

pnpm-lock.yaml

Lines changed: 7 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)