Skip to content
Open
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
11 changes: 10 additions & 1 deletion src/runtime/bake/client/error-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ export interface BundlerMessage {
export interface BundlerMessageLocation {
/** One-based */
line: number;
/** One-based */
/** One-based, relative to the whole source line (not to `lineText`) */
column: number;
/** Byte length */
length: number;
/**
* Number of columns of the source line that precede `lineText`. Non-zero
* when `lineText` is only a window of a long line, so the highlight starts
* `column - 1 - lineTextColumnOffset` characters into `lineText`.
*/
lineTextColumnOffset: number;
/** The source line, or a window of it around the highlighted range. */
lineText: string;
}

Expand Down Expand Up @@ -71,12 +78,14 @@ function readBundlerMessageLocationOrNull(r: DataViewReader): BundlerMessageLoca

const column = r.u32();
const length = r.u32();
const lineTextColumnOffset = r.u32();
const lineText = r.string32();

return {
line,
column,
length,
lineTextColumnOffset,
lineText,
};
}
6 changes: 5 additions & 1 deletion src/runtime/bake/client/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,11 @@ function renderCodeLine(location: BundlerMessageLocation, level: BundlerMessageL
elem("div", { class: "view" }, [
mapCodePreviewLine(syntaxHighlight(location.lineText)),
elem("div", { class: "highlight-wrap log-" + bundleLogLevelToName[level] }, [
elemText("span", { class: "space" }, "_".repeat(location.column - 1)),
elemText(
"span",
{ class: "space" },
"_".repeat(Math.max(0, location.column - 1 - location.lineTextColumnOffset)),
),
elemText("span", { class: "line" }, "_".repeat(location.length)),
]),
]),
Expand Down
6 changes: 5 additions & 1 deletion src/runtime/bake/dev_server/serialized_failure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ pub enum ErrorKind {
JsAggregate,
}

// All "write" functions get a corresponding "read" function in ./client/error.ts
// All "write" functions get a corresponding "read" function in ../client/error-serialization.ts
type Writer = Vec<u8>;

fn write_log_msg(msg: &bun_ast::Msg, w: &mut Writer) {
Expand Down Expand Up @@ -231,6 +231,10 @@ fn write_log_data(data: &bun_ast::Data, w: &mut Writer) {
_ = w.write_int_le::<u32>(u32::try_from(loc.column).expect("int cast"));
_ = w.write_int_le::<u32>(u32::try_from(loc.length).expect("int cast"));

// `line_text` may be a window of a long line (`Location::init_or_null`)
// while `column` is relative to the whole line; the client needs the
// width of the dropped prefix to put the underline under the window.
_ = w.write_int_le::<u32>(loc.line_text_column_offset);
// TODO: syntax highlighted line text + give more context lines
write_string32(loc.line_text.as_deref().unwrap_or(b""), w);

Expand Down
27 changes: 27 additions & 0 deletions test/bake/dev/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,33 @@ devTest("importing bun on the client", {
});
},
});
devTest("error overlay underlines an error deep inside a long line", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `let ok = 1;\n${"a".repeat(100)}]${"b".repeat(100)}`,
},
async test(dev) {
// The `]` is at column 101 of a 201 character line. The overlay only shows
// a window of the line that starts 40 characters before the error, so the
// underline (what the `:41` below is measured from) has to start 40
// characters in rather than 100.
await using c = await dev.client("/", {
errors: ['index.ts:2:41: error: Expected ";" but found "]"'],
});
const { excerpt, underlineStart } = await c.js<{ excerpt: string; underlineStart: number }>`{
const msg = document.querySelector("bun-hmr").shadowRoot.querySelector(".b-msg");
return {
excerpt: msg.querySelector(".view > pre").textContent,
underlineStart: msg.querySelector(".highlight-wrap > .space").textContent.length,
};
}`;
expect(excerpt).toBe(`${"a".repeat(40)}]${"b".repeat(79)}`);
expect(excerpt[underlineStart]).toBe("]");
},
});
devTest("import.meta.main", {
files: {
"index.html": emptyHtmlFile({
Expand Down
109 changes: 109 additions & 0 deletions test/cli/inspect/BunFrontendDevServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => {
"length": 1,
"line": 3,
"lineText": " export function brokenFunction(name: string {",
"lineTextColumnOffset": 0,
},
"message": "Expected ")" but found "{"",
"notes": [],
Expand All @@ -455,6 +456,7 @@ describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => {
"length": 1,
"line": 5,
"lineText": " }",
"lineTextColumnOffset": 0,
},
"message": "Unexpected }",
"notes": [],
Expand All @@ -477,6 +479,113 @@ describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => {
);
});

test("bundleFailed locations on a long line carry the column offset of the lineText window", async () => {
// For an error deep inside a long line, `lineText` is only a window of the
// line (40 bytes in front of the error, 80 after it) while `column` still
// counts from the start of the line. `lineTextColumnOffset` is the width of
// the part the window dropped, so the overlay underlines `lineText` at
// `column - 1 - lineTextColumnOffset`. Like `column`, it counts UTF-16 code
// units (JS string indices), not bytes: 170 bytes of the second line are
// dropped but they are only 91 characters.
const asciiLine = `let a = 1;/*${"-".repeat(96)}*/let a = 2;/*${"-".repeat(96)}*/`;
const unicodeLine = `let b = 1;/*${"é".repeat(96)}*/let b = 2;/*${"é".repeat(96)}*/`;
const secondDeclaration = asciiLine.lastIndexOf("let ") + "let ".length; // 114, same on both lines

const bundleFailedPromise = session.waitForEvent("BunFrontendDevServer.bundleFailed");
// The file is already part of the graph, so the watcher rebundles it on its own.
fs.writeFileSync(join(tempdir, "utils.ts"), `${asciiLine}\n${unicodeLine}\n`);

const { buildErrorsPayloadBase64 } = await bundleFailedPromise;
const buffer = Uint8Array.from(atob(buildErrorsPayloadBase64), c => c.charCodeAt(0));
const reader = new DataViewReader(new DataView(buffer.buffer), 0);
const failures: Array<ReturnType<typeof decodeAndAppendServerError>> = [];
while (reader.hasMoreData()) {
failures.push(decodeAndAppendServerError(reader));
}

expect(failures.map(({ file, messages }) => ({ file, messages }))).toEqual([
{
file: "utils.ts",
messages: [
{
kind: "bundler",
level: 0,
message: '"a" has already been declared',
location: {
line: 1,
column: secondDeclaration + 1,
length: 1,
lineTextColumnOffset: secondDeclaration - 40,
lineText: asciiLine.slice(secondDeclaration - 40, secondDeclaration + 80),
},
notes: [
{
message: '"a" was originally declared here',
location: {
line: 1,
column: 5,
length: 1,
lineTextColumnOffset: 0,
lineText: asciiLine.slice(0, 4 + 80),
},
},
],
},
{
kind: "bundler",
level: 0,
message: '"b" has already been declared',
location: {
line: 2,
column: secondDeclaration + 1,
length: 1,
// The 40 bytes kept in front of `b` are "*/let " plus 17 two-byte
// "é", so the window drops "let b = 1;/*" and the other 79 "é".
lineTextColumnOffset: "let b = 1;/*".length + 79,
lineText: `${"é".repeat(17)}*/let b = 2;/*${"é".repeat(36)}`,
},
notes: [
{
message: '"b" was originally declared here',
location: {
line: 2,
column: 5,
length: 1,
lineTextColumnOffset: 0,
lineText: `let b = 1;/*${"é".repeat(36)}`,
},
},
],
},
],
},
]);

// What the overlay relies on: the offset is where `lineText` starts in the
// line, in string indices, so the underline lands on the declared name.
const [aMessage, bMessage] = failures[0].messages;
for (const [name, line, message] of [
["a", asciiLine, aMessage],
["b", unicodeLine, bMessage],
] as const) {
for (const { location } of [message, ...message.notes]) {
expect(line.indexOf(location.lineText)).toBe(location.lineTextColumnOffset);
expect(location.lineText[location.column - 1 - location.lineTextColumnOffset]).toBe(name);
}
}

// Fix the file so subsequent tests don't fail
fs.writeFileSync(
join(tempdir, "utils.ts"),
`
// Fixed utility module
export function greet(name: string) {
return \`Hello, \${name}!\`;
}
`,
);
});

test("should notify on clientNavigated events", async () => {
await fetch(serverUrl.href).then(r => r.blob());

Expand Down