Skip to content

Commit bc97062

Browse files
mythmonmbostockFil
authored
better messaging when deploying large files (#1267)
Co-authored-by: Mike Bostock <[email protected]> Co-authored-by: Philippe Rivière <[email protected]>
1 parent 724009e commit bc97062

File tree

5 files changed

+67
-20
lines changed

5 files changed

+67
-20
lines changed

src/bin/observable.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type {ParseArgsConfig} from "node:util";
33
import {parseArgs} from "node:util";
44
import * as clack from "@clack/prompts";
5+
import wrapAnsi from "wrap-ansi";
56
import {readConfig} from "../config.js";
67
import {CliError} from "../error.js";
78
import {faint, link, red} from "../tty.js";
@@ -199,29 +200,37 @@ try {
199200
}
200201
}
201202
} catch (error: any) {
203+
const wrapWidth = process.stdout.columns ?? 80;
204+
const bugMessage = "If you think this is a bug, please file an issue at";
205+
const bugUrl = "https://github.com/observablehq/framework/issues";
206+
const clackBugMessage = () => {
207+
// clack.outro doesn't handle multiple lines well, so do it manually
208+
console.log(`${faint("│\n│")} ${bugMessage}\n${faint("└")} ${link(bugUrl)}\n`);
209+
};
210+
const consoleBugMessage = () => {
211+
console.error(`${bugMessage}\n↳ ${link(bugUrl)}\n`);
212+
};
213+
202214
if (error instanceof CliError) {
203215
if (error.print) {
204216
if (command && CLACKIFIED_COMMANDS.includes(command)) {
205-
clack.outro(red(`Error: ${error.message}`));
217+
clack.log.error(wrapAnsi(red(`Error: ${error.message}`), wrapWidth));
218+
clackBugMessage();
206219
} else {
207220
console.error(red(error.message));
221+
consoleBugMessage();
208222
}
209223
}
210224
process.exit(error.exitCode);
211225
} else {
212226
if (command && CLACKIFIED_COMMANDS.includes(command)) {
213-
clack.log.error(`${red("Error:")} ${error.message}`);
227+
clack.log.error(wrapAnsi(`${red("Error:")} ${error.message}`, wrapWidth));
214228
if (values.debug) {
215229
clack.outro("The full error follows");
216230
throw error;
217231
} else {
218232
clack.log.info("To see the full stack trace, run with the --debug flag.");
219-
// clack.outro doesn't handle multiple lines well, so do it manually
220-
console.log(
221-
`${faint("│\n│")} If you think this is a bug, please file an issue at\n${faint("└")} ${link(
222-
"https://github.com/observablehq/framework/issues"
223-
)}\n`
224-
);
233+
clackBugMessage();
225234
}
226235
} else {
227236
console.error(`\n${red("Unexpected error:")} ${error.message}`);
@@ -230,11 +239,7 @@ try {
230239
throw error;
231240
} else {
232241
console.error("\nTip: To see the full stack trace, run with the --debug flag.\n");
233-
console.error(
234-
`If you think this is a bug, please file an issue at\n↳ ${link(
235-
"https://github.com/observablehq/framework/issues"
236-
)}\n`
237-
);
242+
consoleBugMessage();
238243
}
239244
}
240245
}

src/format.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,15 @@ export function formatLocaleDate(date: Date, locale: Intl.LocalesArgument = "en-
1313
function pad(number: number, length: number): string {
1414
return String(number).padStart(length, "0");
1515
}
16+
17+
export function formatByteSize(n: number): string {
18+
const suffixes = ["bytes", "KB", "MB", "GB"];
19+
for (const suffix of suffixes) {
20+
if (n < 1300) {
21+
if (suffix === "bytes") return `${n} ${suffix}`;
22+
return `${n > 100 ? n.toFixed(0) : n.toFixed(1)} ${suffix}`;
23+
}
24+
n /= 1000;
25+
}
26+
return `${n.toFixed(1)} TB`;
27+
}

src/observableApiClient.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs/promises";
22
import type {ClackEffects} from "./clack.js";
33
import {CliError, HttpError, isApiError} from "./error.js";
4+
import {formatByteSize} from "./format.js";
45
import type {ApiKey} from "./observableApiConfig.js";
56
import {faint, red} from "./tty.js";
67

@@ -87,9 +88,11 @@ export class ObservableApiClient {
8788
} catch (error) {
8889
// that's ok
8990
}
90-
const error = new HttpError(`Unexpected response status ${JSON.stringify(response.status)}`, response.status, {
91-
details
92-
});
91+
const error = new HttpError(
92+
`Unexpected response status ${JSON.stringify(response.status)} for ${options.method ?? "GET"} ${url.href}`,
93+
response.status,
94+
{details}
95+
);
9396

9497
// check for version mismatch
9598
if (
@@ -183,7 +186,14 @@ export class ObservableApiClient {
183186
const blob = new Blob([contents]);
184187
body.append("file", blob);
185188
body.append("client_name", relativePath);
186-
await this._fetch(url, {method: "POST", body});
189+
try {
190+
await this._fetch(url, {method: "POST", body});
191+
} catch (error) {
192+
const message = error instanceof Error ? error.message : `${error}`;
193+
throw new CliError(`While uploading ${relativePath} (${formatByteSize(contents.length)}): ${message}`, {
194+
cause: error
195+
});
196+
}
187197
}
188198

189199
async postDeployUploaded(deployId: string): Promise<DeployInfo> {

test/deploy-test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,8 +481,9 @@ describe("deploy", () => {
481481
await deploy({...TEST_OPTIONS, maxConcurrency: 1}, effects);
482482
fail("Should have thrown an error");
483483
} catch (error) {
484-
assert.ok(isHttpError(error));
485-
assert.equal(error.statusCode, 500);
484+
CliError.assert(error, {message: /While uploading index.html.*500/});
485+
assert.ok(isHttpError(error.cause));
486+
assert.equal(error.cause.statusCode, 500);
486487
}
487488

488489
effects.close();

test/format-test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "node:assert";
2-
import {formatIsoDate, formatLocaleDate} from "../src/format.js";
2+
import {formatByteSize, formatIsoDate, formatLocaleDate} from "../src/format.js";
33

44
describe("formatIsoDate", () => {
55
it("returns an ISO 8601 string in local time", () => {
@@ -21,3 +21,22 @@ describe("formatLocaleDate", () => {
2121
assert.strictEqual(formatLocaleDate(new Date("2013-01-02T08:00:00Z"), "fr-FR"), "2 janv. 2013");
2222
});
2323
});
24+
25+
describe("formatByteSize", () => {
26+
it("returns a human-readable byte size", () => {
27+
assert.strictEqual(formatByteSize(0), "0 bytes");
28+
assert.strictEqual(formatByteSize(500), "500 bytes");
29+
assert.strictEqual(formatByteSize(1200), "1200 bytes");
30+
assert.strictEqual(formatByteSize(1_500), "1.5 KB");
31+
assert.strictEqual(formatByteSize(3_000), "3.0 KB");
32+
assert.strictEqual(formatByteSize(400_000), "400 KB");
33+
assert.strictEqual(formatByteSize(50_000_000), "50.0 MB");
34+
assert.strictEqual(formatByteSize(1_250_000_000), "1250 MB");
35+
assert.strictEqual(formatByteSize(1_500_000_000), "1.5 GB");
36+
assert.strictEqual(formatByteSize(3_000_000_000), "3.0 GB");
37+
assert.strictEqual(formatByteSize(600_000_000_000), "600 GB");
38+
assert.strictEqual(formatByteSize(60_598_160), "60.6 MB");
39+
assert.strictEqual(formatByteSize(60_598_160_000), "60.6 GB");
40+
assert.strictEqual(formatByteSize(60_598_160_000_000), "60.6 TB");
41+
});
42+
});

0 commit comments

Comments
 (0)