Skip to content

Commit e53d32c

Browse files
committed
fix(@clack/prompts): handle carriage return output in taskLog
1 parent ec432f9 commit e53d32c

File tree

4 files changed

+207
-4
lines changed

4 files changed

+207
-4
lines changed

.changeset/lovely-radios-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/prompts": patch
3+
---
4+
5+
Fix `taskLog` raw message handling so carriage-return spinner updates do not accumulate repeated frames.

packages/prompts/src/task-log.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ const stripDestructiveANSI = (input: string): string => {
4141
return input.replace(/\x1b\[(?:\d+;)*\d*[ABCDEFGHfJKSTsu]|\x1b\[(s|u)/g, '');
4242
};
4343

44+
const replaceLastLine = (input: string, replacement: string): string => {
45+
const lastNewline = input.lastIndexOf('\n');
46+
if (lastNewline === -1) {
47+
return replacement;
48+
}
49+
return `${input.slice(0, lastNewline + 1)}${replacement}`;
50+
};
51+
52+
const appendRawMessage = (input: string, msg: string, prependNewline: boolean): string => {
53+
let next = prependNewline ? `${input}\n` : input;
54+
const [first, ...overwrites] = msg.split('\r');
55+
next += first;
56+
for (const overwrite of overwrites) {
57+
next = replaceLastLine(next, overwrite);
58+
}
59+
return next;
60+
};
61+
4462
/**
4563
* Renders a log which clears on success and remains on failure
4664
*/
@@ -139,11 +157,22 @@ export const taskLog = (opts: TaskLogOptions) => {
139157
};
140158
const message = (buffer: BufferEntry, msg: string, mopts?: TaskLogMessageOptions) => {
141159
clear(false);
142-
if ((mopts?.raw !== true || !lastMessageWasRaw) && buffer.value !== '') {
143-
buffer.value += '\n';
160+
const sanitized = stripDestructiveANSI(msg);
161+
if (mopts?.raw === true) {
162+
const rawMessage = sanitized.replace(/\n+$/g, '');
163+
buffer.value = appendRawMessage(
164+
buffer.value,
165+
rawMessage,
166+
buffer.value !== '' && !lastMessageWasRaw && !rawMessage.startsWith('\n') && !rawMessage.startsWith('\r')
167+
);
168+
lastMessageWasRaw = !sanitized.endsWith('\n');
169+
} else {
170+
if (buffer.value !== '') {
171+
buffer.value += '\n';
172+
}
173+
buffer.value += sanitized;
174+
lastMessageWasRaw = false;
144175
}
145-
buffer.value += stripDestructiveANSI(msg);
146-
lastMessageWasRaw = mopts?.raw === true;
147176
if (opts.limit !== undefined) {
148177
const lines = buffer.value.split('\n');
149178
const linesToRemove = lines.length - opts.limit;

packages/prompts/test/__snapshots__/task-log.test.ts.snap

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`taskLog (isCI = false) > error > clears carriage return spinner line when showLog = false 1`] = `
4+
[
5+
"│
6+
",
7+
"◇ foo
8+
",
9+
"│
10+
",
11+
"│ ◒ Cloning repository
12+
",
13+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
14+
"│ ◐ Cloning repository
15+
",
16+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
17+
"│ ◓ Cloning repository
18+
",
19+
"<erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
20+
"│
21+
■ some error!
22+
",
23+
]
24+
`;
25+
326
exports[`taskLog (isCI = false) > error > clears output if showLog = false 1`] = `
427
[
528
"│
@@ -21,6 +44,32 @@ exports[`taskLog (isCI = false) > error > clears output if showLog = false 1`] =
2144
]
2245
`;
2346

47+
exports[`taskLog (isCI = false) > error > renders latest carriage return spinner line with error 1`] = `
48+
[
49+
"│
50+
",
51+
"◇ foo
52+
",
53+
"│
54+
",
55+
"│ ◒ Cloning repository
56+
",
57+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
58+
"│ ◐ Cloning repository
59+
",
60+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
61+
"│ ◓ Cloning repository
62+
",
63+
"<erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
64+
"│
65+
■ some error!
66+
",
67+
"│
68+
│ ◓ Cloning repository
69+
",
70+
]
71+
`;
72+
2473
exports[`taskLog (isCI = false) > error > renders output with message 1`] = `
2574
[
2675
"│
@@ -712,6 +761,28 @@ exports[`taskLog (isCI = false) > message > raw = true appends message text unti
712761
]
713762
`;
714763

764+
exports[`taskLog (isCI = false) > message > raw = true replaces carriage return spinner updates 1`] = `
765+
[
766+
"│
767+
",
768+
"◇ foo
769+
",
770+
"│
771+
",
772+
"│ ◒ Cloning repository
773+
",
774+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
775+
"│ ◐ Cloning repository
776+
",
777+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
778+
"│ ◓ Cloning repository
779+
",
780+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
781+
"│ ◇ Repository cloned
782+
",
783+
]
784+
`;
785+
715786
exports[`taskLog (isCI = false) > message > raw = true works when mixed with non-raw messages 1`] = `
716787
[
717788
"│
@@ -1164,6 +1235,23 @@ exports[`taskLog (isCI = false) > writes message header 1`] = `
11641235
]
11651236
`;
11661237

1238+
exports[`taskLog (isCI = true) > error > clears carriage return spinner line when showLog = false 1`] = `
1239+
[
1240+
"│
1241+
",
1242+
"◇ foo
1243+
",
1244+
"│
1245+
",
1246+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1247+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1248+
"<erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1249+
"│
1250+
■ some error!
1251+
",
1252+
]
1253+
`;
1254+
11671255
exports[`taskLog (isCI = true) > error > clears output if showLog = false 1`] = `
11681256
[
11691257
"│
@@ -1180,6 +1268,26 @@ exports[`taskLog (isCI = true) > error > clears output if showLog = false 1`] =
11801268
]
11811269
`;
11821270

1271+
exports[`taskLog (isCI = true) > error > renders latest carriage return spinner line with error 1`] = `
1272+
[
1273+
"│
1274+
",
1275+
"◇ foo
1276+
",
1277+
"│
1278+
",
1279+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1280+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1281+
"<erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1282+
"│
1283+
■ some error!
1284+
",
1285+
"│
1286+
│ ◓ Cloning repository
1287+
",
1288+
]
1289+
`;
1290+
11831291
exports[`taskLog (isCI = true) > error > renders output with message 1`] = `
11841292
[
11851293
"│
@@ -1432,6 +1540,20 @@ exports[`taskLog (isCI = true) > message > raw = true appends message text until
14321540
]
14331541
`;
14341542

1543+
exports[`taskLog (isCI = true) > message > raw = true replaces carriage return spinner updates 1`] = `
1544+
[
1545+
"│
1546+
",
1547+
"◇ foo
1548+
",
1549+
"│
1550+
",
1551+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1552+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1553+
"<erase.line><cursor.up count=1><erase.line><cursor.left count=1>",
1554+
]
1555+
`;
1556+
14351557
exports[`taskLog (isCI = true) > message > raw = true works when mixed with non-raw messages 1`] = `
14361558
[
14371559
"│

packages/prompts/test/task-log.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ describe.each(['true', 'false'])('taskLog (isCI = %s)', (isCI) => {
119119
expect(output.buffer).toMatchSnapshot();
120120
});
121121

122+
test('raw = true replaces carriage return spinner updates', async () => {
123+
const log = prompts.taskLog({
124+
input,
125+
output,
126+
title: 'foo',
127+
});
128+
129+
log.message('◒ Cloning repository', { raw: true });
130+
log.message('\r◐ Cloning repository', { raw: true });
131+
log.message('\r◓ Cloning repository', { raw: true });
132+
log.message('\r◇ Repository cloned\n', { raw: true });
133+
134+
expect(output.buffer).toMatchSnapshot();
135+
});
136+
122137
test('prints empty lines', async () => {
123138
const log = prompts.taskLog({
124139
input,
@@ -178,6 +193,38 @@ describe.each(['true', 'false'])('taskLog (isCI = %s)', (isCI) => {
178193

179194
expect(output.buffer).toMatchSnapshot();
180195
});
196+
197+
test('renders latest carriage return spinner line with error', () => {
198+
const log = prompts.taskLog({
199+
input,
200+
output,
201+
title: 'foo',
202+
});
203+
204+
log.message('◒ Cloning repository', { raw: true });
205+
log.message('\r◐ Cloning repository', { raw: true });
206+
log.message('\r◓ Cloning repository', { raw: true });
207+
208+
log.error('some error!');
209+
210+
expect(output.buffer).toMatchSnapshot();
211+
});
212+
213+
test('clears carriage return spinner line when showLog = false', () => {
214+
const log = prompts.taskLog({
215+
input,
216+
output,
217+
title: 'foo',
218+
});
219+
220+
log.message('◒ Cloning repository', { raw: true });
221+
log.message('\r◐ Cloning repository', { raw: true });
222+
log.message('\r◓ Cloning repository', { raw: true });
223+
224+
log.error('some error!', { showLog: false });
225+
226+
expect(output.buffer).toMatchSnapshot();
227+
});
181228
});
182229

183230
describe('success', () => {

0 commit comments

Comments
 (0)