Skip to content

Commit 7c51cc1

Browse files
committed
fix(cli): keep download progress on one row
1 parent eddbf82 commit 7c51cc1

12 files changed

Lines changed: 276 additions & 36 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import assert from 'node:assert/strict';
2+
import { spawn } from 'node:child_process';
3+
import { createHash } from 'node:crypto';
4+
import { once } from 'node:events';
5+
import { mkdtempSync, rmSync } from 'node:fs';
6+
import { createServer } from 'node:http';
7+
import { tmpdir } from 'node:os';
8+
import { join } from 'node:path';
9+
import { gzipSync } from 'node:zlib';
10+
11+
// Small, valid archives keep this test offline. The installed files are never run.
12+
function archive(files) {
13+
const blocks = [];
14+
for (const [name, contents] of Object.entries(files)) {
15+
const body = Buffer.from(contents);
16+
const header = Buffer.alloc(512);
17+
header.write(name);
18+
header.write('0000755\0', 100);
19+
header.write(body.length.toString(8).padStart(11, '0') + '\0', 124);
20+
header.fill(' ', 148, 156);
21+
header.write('0', 156);
22+
const checksum = header.reduce((sum, byte) => sum + byte, 0);
23+
header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148);
24+
blocks.push(header, body, Buffer.alloc((512 - (body.length % 512)) % 512));
25+
}
26+
return gzipSync(Buffer.concat([...blocks, Buffer.alloc(1024)]));
27+
}
28+
29+
const version = '99.0.0';
30+
const musl = process.platform === 'linux' && !process.report.getReport().header.glibcVersionRuntime;
31+
const platform = `${process.platform}-${process.arch}${musl ? '-musl' : ''}`;
32+
const nodeRoot = `node-v${version}-${platform}`;
33+
const nodeArchive = archive({ [`${nodeRoot}/bin/node`]: '#!/bin/sh\nexit 0\n' });
34+
const npmArchive = archive({
35+
'package/package.json': JSON.stringify({
36+
name: 'npm',
37+
version,
38+
bin: { npm: 'bin/npm-cli.js', npx: 'bin/npx-cli.js' },
39+
}),
40+
'package/bin/npm-cli.js': '// fixture\n',
41+
'package/bin/npx-cli.js': '// fixture\n',
42+
});
43+
let downloads = 0;
44+
const server = createServer((request, response) => {
45+
if (request.url.endsWith('/SHASUMS256.txt.asc')) {
46+
response.writeHead(404).end();
47+
return;
48+
}
49+
if (request.url.endsWith('/SHASUMS256.txt')) {
50+
response.end(`${createHash('sha256').update(nodeArchive).digest('hex')} ${nodeRoot}.tar.gz\n`);
51+
return;
52+
}
53+
const body = request.url.endsWith(`/${nodeRoot}.tar.gz`)
54+
? nodeArchive
55+
: request.url === `/npm/-/npm-${version}.tgz`
56+
? npmArchive
57+
: undefined;
58+
if (!body) {
59+
response.writeHead(404).end();
60+
return;
61+
}
62+
downloads++;
63+
if (process.argv[2] === 'known') response.setHeader('Content-Length', body.length);
64+
response.flushHeaders();
65+
// Exercise multiple progress redraws; only the completed screen is snapshotted.
66+
let offset = 0;
67+
const timer = setInterval(() => {
68+
const end = Math.min(offset + Math.ceil(body.length / 8), body.length);
69+
response.write(body.subarray(offset, end));
70+
offset = end;
71+
if (offset === body.length) response.end();
72+
}, 50);
73+
response.on('close', () => clearInterval(timer));
74+
});
75+
server.listen(0, '127.0.0.1');
76+
await once(server, 'listening');
77+
const mirror = `http://127.0.0.1:${server.address().port}`;
78+
// A separate home prevents mock installs from changing the runner's shared runtime seed.
79+
const home = mkdtempSync(join(tmpdir(), 'vp-download-progress-'));
80+
try {
81+
for (const tool of ['node', 'npm']) {
82+
console.log(`Before ${tool} download: preserve this output.`);
83+
const child = spawn('vp', ['env', 'install', `${tool}@${version}`], {
84+
stdio: 'inherit',
85+
env: { ...process.env, VP_HOME: home, VP_NODE_DIST_MIRROR: mirror, npm_config_registry: mirror },
86+
});
87+
const [code, signal] = await once(child, 'exit');
88+
assert.equal(signal, null);
89+
assert.equal(code, 0);
90+
console.log(`After ${tool} download.`);
91+
}
92+
assert.equal(downloads, 2);
93+
} finally {
94+
server.close();
95+
server.closeAllConnections();
96+
rmSync(home, { recursive: true, force: true });
97+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[[case]]
2+
name = "download_progress_known_length"
3+
vp = "global"
4+
# The mock Node distribution uses Unix tar archives.
5+
skip-platforms = ["windows"]
6+
steps = [["node", "download.mjs", "known"]]
7+
8+
[[case]]
9+
name = "download_progress_unknown_length"
10+
vp = "global"
11+
skip-platforms = ["windows"]
12+
steps = [["node", "download.mjs", "unknown"]]
13+
14+
[[case]]
15+
name = "download_progress_non_tty"
16+
vp = "global"
17+
skip-platforms = ["windows"]
18+
steps = [{ argv = ["node", "download.mjs", "known"], tty = false }]
19+
20+
[[case]]
21+
name = "download_progress_ci"
22+
vp = "global"
23+
skip-platforms = ["windows"]
24+
steps = [{ argv = ["node", "download.mjs", "known"], envs = [["CI", "1"]] }]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# download_progress_ci
2+
3+
## `CI=1 node download.mjs known`
4+
5+
```
6+
Before node download: preserve this output.
7+
VITE+ - The Unified Toolchain for the Web
8+
9+
Installing Node.js <version>...
10+
Installed Node.js <version>
11+
After node download.
12+
Before npm download: preserve this output.
13+
VITE+ - The Unified Toolchain for the Web
14+
15+
Installing npm <version>...
16+
Installed npm <version>
17+
After npm download.
18+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# download_progress_known_length
2+
3+
## `node download.mjs known`
4+
5+
```
6+
Before node download: preserve this output.
7+
VITE+ - The Unified Toolchain for the Web
8+
9+
Installing Node.js <version>...
10+
Installed Node.js <version>
11+
After node download.
12+
Before npm download: preserve this output.
13+
VITE+ - The Unified Toolchain for the Web
14+
15+
Installing npm <version>...
16+
Installed npm <version>
17+
After npm download.
18+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# download_progress_non_tty
2+
3+
## `node download.mjs known`
4+
5+
```
6+
Before node download: preserve this output.
7+
Installing Node.js <version>...
8+
Installed Node.js <version>
9+
After node download.
10+
Before npm download: preserve this output.
11+
Installing npm <version>...
12+
Installed npm <version>
13+
After npm download.
14+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# download_progress_unknown_length
2+
3+
## `node download.mjs unknown`
4+
5+
```
6+
Before node download: preserve this output.
7+
VITE+ - The Unified Toolchain for the Web
8+
9+
Installing Node.js <version>...
10+
Installed Node.js <version>
11+
After node download.
12+
Before npm download: preserve this output.
13+
VITE+ - The Unified Toolchain for the Web
14+
15+
Installing npm <version>...
16+
Installed npm <version>
17+
After npm download.
18+
```

‎crates/vp_js_runtime/src/download.rs‎

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{fs::File, time::Duration};
77

88
use backon::{ExponentialBuilder, Retryable};
99
use futures_util::StreamExt;
10-
use indicatif::{ProgressBar, ProgressStyle};
10+
use indicatif::ProgressBar;
1111
use serde::de::DeserializeOwned;
1212
use sha2::{Digest, Sha256};
1313
use tokio::{
@@ -50,13 +50,9 @@ pub async fn download_file(
5050
let is_ci = vp_shared::EnvConfig::get().is_ci;
5151
let progress = if vp_shared::is_stderr_terminal() && !is_ci {
5252
let pb = ProgressBar::new_spinner();
53-
pb.set_style(
54-
ProgressStyle::default_spinner()
55-
.template("{msg}\n{spinner:.green} [{elapsed_precise}] {bytes} ({bytes_per_sec})")
56-
.expect("valid spinner template"),
57-
);
58-
pb.enable_steady_tick(Duration::from_millis(100));
53+
pb.set_style(vp_shared::download_progress::spinner_style());
5954
pb.set_message(message.to_string());
55+
pb.enable_steady_tick(Duration::from_millis(100));
6056
Some(pb)
6157
} else {
6258
None
@@ -130,15 +126,7 @@ pub async fn download_file(
130126
pb.set_position(if is_resumed { resume_from } else { 0 });
131127
if let Some(size) = total_size {
132128
pb.set_length(size);
133-
pb.set_style(
134-
ProgressStyle::default_bar()
135-
.template(
136-
"{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.blue/white}] \
137-
{bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
138-
)
139-
.expect("valid progress bar template")
140-
.progress_chars("#>-"),
141-
);
129+
pb.set_style(vp_shared::download_progress::bar_style());
142130
}
143131
}
144132

‎crates/vp_pm_cli/src/request.rs‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
use backon::{ExponentialBuilder, Retryable};
77
use flate2::read::GzDecoder;
88
use futures_util::stream::StreamExt;
9-
use indicatif::{ProgressBar, ProgressStyle};
9+
use indicatif::ProgressBar;
1010
use reqwest::{Response, StatusCode};
1111
use serde::de::DeserializeOwned;
1212
use sha1::Sha1;
@@ -140,7 +140,7 @@ impl HttpClient {
140140

141141
/// Download a file to a specified path
142142
///
143-
/// The optional `message` is displayed above a progress bar (e.g. "Downloading
143+
/// The optional `message` is displayed alongside a progress bar (e.g. "Downloading
144144
/// pnpm v10.0.0..."), shown only on a TTY and outside CI so piped/non-interactive
145145
/// output stays clean. Pass `None` for downloads that shouldn't surface progress
146146
/// (e.g. small metadata probes).
@@ -149,7 +149,7 @@ impl HttpClient {
149149
///
150150
/// * `url` - The URL of the file to download
151151
/// * `target_path` - The path where the file will be saved
152-
/// * `message` - Optional message shown above the progress bar
152+
/// * `message` - Optional message shown alongside the progress bar
153153
///
154154
/// # Returns
155155
///
@@ -175,15 +175,9 @@ impl HttpClient {
175175
&& !is_ci
176176
{
177177
let pb = ProgressBar::new_spinner();
178-
pb.set_style(
179-
ProgressStyle::default_spinner()
180-
.template(
181-
"{msg}\n{spinner:.green} [{elapsed_precise}] {bytes} ({bytes_per_sec})",
182-
)
183-
.expect("valid spinner template"),
184-
);
185-
pb.enable_steady_tick(Duration::from_millis(100));
178+
pb.set_style(vp_shared::download_progress::spinner_style());
186179
pb.set_message(message.to_string());
180+
pb.enable_steady_tick(Duration::from_millis(100));
187181
Some(pb)
188182
} else {
189183
None
@@ -204,15 +198,7 @@ impl HttpClient {
204198
pb.set_position(0);
205199
if let Some(size) = response.content_length() {
206200
pb.set_length(size);
207-
pb.set_style(
208-
ProgressStyle::default_bar()
209-
.template(
210-
"{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.blue/white}] \
211-
{bytes}/{total_bytes} ({bytes_per_sec}, {eta})",
212-
)
213-
.expect("valid progress bar template")
214-
.progress_chars("#>-"),
215-
);
201+
pb.set_style(vp_shared::download_progress::bar_style());
216202
}
217203
}
218204
Self::write_response_to_file(response, target_path, progress.as_ref()).await

‎crates/vp_shared/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ test-utils = ["dep:temp-env", "dep:tempfile"]
1818
directories = { workspace = true }
1919
nix = { workspace = true, features = ["fs", "poll", "term"] }
2020
console = { workspace = true }
21+
indicatif = { workspace = true }
2122
cow-utils = { workspace = true }
2223
serde = { workspace = true }
2324
# use `preserve_order` feature to preserve the order of the fields in `package.json`
@@ -41,6 +42,7 @@ rustls = { workspace = true }
4142
webpki-root-certs = { workspace = true }
4243

4344
[dev-dependencies]
45+
indicatif = { workspace = true, features = ["in_memory"] }
4446
serial_test = { workspace = true }
4547
tempfile = { workspace = true }
4648
# Enables the `test-utils` feature for this crate's own tests and doctests

0 commit comments

Comments
 (0)