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
52 changes: 46 additions & 6 deletions docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1604,18 +1604,30 @@ Most of the time, an explicit try/catch is not needed, as Bun prints uncaught ex
Each item in `error.errors` is an instance of `BuildMessage` or `ResolveMessage` (subclasses of `Error`), containing detailed information for each error.

```ts title="build.ts" icon="/icons/typescript.svg"
class BuildMessage {
name: string;
position?: Position;
class BuildMessage extends Error {
name: "BuildMessage";
position: Position | null;
message: string;
level: "error" | "warning" | "info" | "debug" | "verbose";
notes: BuildMessage[]; // related locations, each with level "note"
line: number; // zero-based; position.line is one-based
column: number; // zero-based; position.column is one-based
toJSON(): Pick<BuildMessage, "name" | "position" | "message" | "level">;
}

class ResolveMessage extends BuildMessage {
class ResolveMessage extends Error {
name: "ResolveMessage";
position: Position | null;
message: string;
level: "error" | "warning" | "info" | "debug" | "verbose";
code: string;
requireStack: string[] | undefined; // only set when a require() failed
referrer: string;
specifier: string;
importKind: ImportKind;
line: number; // zero-based; position.line is one-based
column: number; // zero-based; position.column is one-based
toJSON(): Pick<ResolveMessage, "name" | "position" | "message" | "level" | "specifier" | "importKind" | "referrer">;
}
```

Expand Down Expand Up @@ -1806,11 +1818,23 @@ interface BuildOutput {
logs: Array<BuildMessage | ResolveMessage>;
}

declare class ResolveMessage {
interface Position {
lineText: string;
file: string;
namespace: string;
line: number; // one-based
column: number; // one-based
length: number;
offset: number;
}

declare class ResolveMessage extends Error {
readonly name: "ResolveMessage";
readonly position: Position | null;
readonly code: string;
readonly message: string;
stack: string; // "ResolveMessage: <message>", no stack frames
readonly requireStack: string[] | undefined; // only set when a require() failed
readonly referrer: string;
readonly specifier: string;
readonly importKind:
Expand All @@ -1825,8 +1849,24 @@ declare class ResolveMessage {
| "url"
| "internal";
readonly level: "error" | "warning" | "info" | "debug" | "verbose";
readonly line: number; // zero-based, 0 without a position
readonly column: number; // zero-based, 0 without a position

toString(): string; // "ResolveMessage: <message>"
toJSON(): Pick<ResolveMessage, "name" | "position" | "message" | "level" | "specifier" | "importKind" | "referrer">;
}

declare class BuildMessage extends Error {
readonly name: "BuildMessage";
readonly position: Position | null;
readonly message: string;
readonly level: "error" | "warning" | "info" | "debug" | "verbose";
readonly notes: BuildMessage[]; // related locations, each with level "note"
readonly line: number; // zero-based, 0 without a position
readonly column: number; // zero-based, 0 without a position

toString(): string;
toString(): string; // "BuildMessage: <message>"
toJSON(): Pick<BuildMessage, "name" | "position" | "message" | "level">;
}
```

Expand Down
75 changes: 73 additions & 2 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -989,11 +989,28 @@ interface Position {
offset: number;
}

declare class ResolveMessage {
/**
* A module specifier that could not be resolved. Thrown by `import`,
* `require()` and {@link Bun.resolveSync}, and reported in
* {@link Bun.BuildOutput.logs} by {@link Bun.build}.
*/
declare class ResolveMessage extends Error {
readonly name: "ResolveMessage";
readonly position: Position | null;
readonly code: string;
readonly message: string;
/**
* `"ResolveMessage: <message>"`, with no `at ...` frames: no JavaScript
* stack is captured when resolution fails.
*/
stack: string;
/**
* Like the `requireStack` of a Node.js `MODULE_NOT_FOUND` error: the module
* whose `require()` or `require.resolve()` call did not find {@link specifier}
* (only the direct caller is recorded). `undefined` for every other failure,
* including everything reached through `import`.
*/
readonly requireStack: string[] | undefined;
readonly referrer: string;
readonly specifier: string;
readonly importKind:
Expand All @@ -1008,15 +1025,69 @@ declare class ResolveMessage {
| "url"
| "internal";
readonly level: "error" | "warning" | "info" | "debug" | "verbose";
/**
* Zero-based line of {@link position}, or `0` when there is no position.
* `position.line` is the same line, one-based.
*/
readonly line: number;
/**
* Zero-based column of {@link position}, or `0` when there is no position.
* `position.column` is the same column, one-based.
*/
readonly column: number;

/**
* `"ResolveMessage: <message>"`, which is also what converting the object to
* a string (`String(...)`, a template literal) produces.
*/
toString(): string;
/**
* The fields `JSON.stringify()` serializes this message as.
*/
toJSON(): Pick<ResolveMessage, "name" | "position" | "message" | "level" | "specifier" | "importKind" | "referrer">;
[Symbol.toPrimitive](hint: "default" | "string"): string;
[Symbol.toPrimitive](hint: string): string | null;
}

declare class BuildMessage {
/**
* An error, warning or note produced while parsing or bundling a module.
* Thrown by `import` and `require()` of a file with a syntax error, and
* reported in {@link Bun.BuildOutput.logs} by {@link Bun.build}.
*/
declare class BuildMessage extends Error {
readonly name: "BuildMessage";
readonly position: Position | null;
readonly message: string;
readonly level: "error" | "warning" | "info" | "debug" | "verbose";
/**
* Secondary locations attached to this message, each a `BuildMessage` whose
* `level` is `"note"`, such as the original declaration behind a
* "has already been declared" error. Empty when there are none.
*/
readonly notes: BuildMessage[];
/**
* Zero-based line of {@link position}, or `0` when there is no position.
* `position.line` is the same line, one-based.
*/
readonly line: number;
/**
* Zero-based column of {@link position}, or `0` when there is no position.
* `position.column` is the same column, one-based.
*/
readonly column: number;

/**
* `"BuildMessage: <message>"`, which is also what converting the object to a
* string (`String(...)`, a template literal) produces.
*/
toString(): string;
/**
* The fields `JSON.stringify()` serializes this message as. {@link notes}
* are not included.
*/
toJSON(): Pick<BuildMessage, "name" | "position" | "message" | "level">;
[Symbol.toPrimitive](hint: "default" | "string"): string;
[Symbol.toPrimitive](hint: string): string | null;
}

interface ErrorOptions {
Expand Down
155 changes: 132 additions & 23 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $ as Shell, fileURLToPath } from "bun";
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { bunEnv, bunExe, isDebug, makeTree } from "harness";
import { bunEnv, bunExe, isDebug, makeTree, tempDir } from "harness";
import { existsSync, readFileSync } from "node:fs";
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -369,34 +369,143 @@ describe("@types/bun integration test", () => {

// Runs on debug builds too: spawning tsc over a single file is cheap,
// unlike the in-process LanguageService runs above.
async function expectToTypecheck(dirName: string, fileName: string, source: string) {
const checkDir = join(TEMP_DIR, dirName);
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.include = [fileName];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
[fileName]: source,
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
}

describe("Bun.mmap", () => {
test("MMapOptions accepts offset and size", async () => {
const checkDir = join(TEMP_DIR, "mmap-options-check");
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.include = ["mmap-options.ts"];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
"mmap-options.ts": `const view = Bun.mmap("./data.bin", { shared: true, sync: false, offset: 4096, size: 1024 });
view satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { offset: 4096 }) satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { size: 1024 }) satisfies Uint8Array<ArrayBuffer>;`,
});
await expectToTypecheck(
"mmap-options-check",
"mmap-options.ts",
`const view = Bun.mmap("./data.bin", { shared: true, sync: false, offset: 4096, size: 1024 });
view satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { offset: 4096 }) satisfies Uint8Array<ArrayBuffer>;
Bun.mmap("./data.bin", { size: 1024 }) satisfies Uint8Array<ArrayBuffer>;`,
);
});
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
describe("BuildMessage / ResolveMessage", () => {
test("declare the Error base and the members the runtime objects have", async () => {
using dir = tempDir("bun-types-message-members", {
"redeclared.ts": "let a = 1;\nlet a = 2;\n",
"missing-import.ts": 'import "./missing";\n',
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const { logs } = await Bun.build({
entrypoints: [join(String(dir), "redeclared.ts"), join(String(dir), "missing-import.ts")],
throw: false,
});
const buildMessage = logs.find(log => log instanceof BuildMessage)!;
const resolveMessage = logs.find(log => log instanceof ResolveMessage)!;
let requireFailure: unknown;
try {
require(join(String(dir), "missing-require"));
} catch (e) {
requireFailure = e;
}
if (!(requireFailure instanceof ResolveMessage)) throw new Error("require() did not throw a ResolveMessage");

const buildJsonKeys = Object.keys(buildMessage.toJSON());
const resolveJsonKeys = Object.keys(resolveMessage.toJSON());

// The runtime shape the declarations below have to mirror.
expect({
errors: [buildMessage instanceof Error, resolveMessage instanceof Error],
stacks: [buildMessage.stack, resolveMessage.stack],
notes: buildMessage.notes.map((note): [boolean, string] => [note instanceof BuildMessage, note.level]),
// zero-based [line, column], unlike the one-based position.line / position.column
locations: [
[buildMessage.line, buildMessage.column, buildMessage.position?.line, buildMessage.position?.column],
[resolveMessage.line, resolveMessage.column, resolveMessage.position?.line, resolveMessage.position?.column],
],
requireStacks: [resolveMessage.requireStack, requireFailure.requireStack],
strings: [`${buildMessage}`, buildMessage[Symbol.toPrimitive]("number")],
json: { build: buildJsonKeys, resolve: resolveJsonKeys },
}).toEqual({
errors: [true, true],
stacks: [undefined, `ResolveMessage: ${resolveMessage.message}`],
notes: [[true, "note"]],
locations: [
[1, 4, 2, 5],
[0, 7, 1, 8],
],
requireStacks: [undefined, [expect.any(String)]],
strings: [`BuildMessage: ${buildMessage.message}`, null],
json: {
build: ["name", "position", "message", "level"],
resolve: ["name", "position", "message", "level", "specifier", "importKind", "referrer"],
},
});

expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
await expectToTypecheck(
"build-message-members-check",
"build-logs.ts",
[
`declare const output: Bun.BuildOutput;`,
`for (const log of output.logs) {`,
` log.stack satisfies string | undefined;`,
` log.cause satisfies unknown;`,
` log.line satisfies number;`,
` log.column satisfies number;`,
` log[Symbol.toPrimitive]("default") satisfies string;`,
` log[Symbol.toPrimitive]("number") satisfies string | null;`,
` if (log instanceof ResolveMessage) {`,
` log.stack satisfies string;`,
` log.requireStack satisfies string[] | undefined;`,
` const json = log.toJSON();`,
...resolveJsonKeys.map(key => ` json.${key};`),
` // @ts-expect-error - not serialized`,
` json.code;`,
` // @ts-expect-error - not serialized`,
` json.requireStack;`,
` } else {`,
` log.notes satisfies BuildMessage[];`,
` log.notes[0]?.notes satisfies BuildMessage[] | undefined;`,
` const json = log.toJSON();`,
...buildJsonKeys.map(key => ` json.${key};`),
` // @ts-expect-error - not serialized`,
` json.notes;`,
` // @ts-expect-error - only ResolveMessage has requireStack`,
` log.requireStack;`,
` }`,
`}`,
`try {`,
` require("./missing");`,
`} catch (e) {`,
` if (e instanceof ResolveMessage) {`,
` const requireStack: string[] = e.requireStack ?? [];`,
` console.error(e.stack, requireStack, e.line, e.column);`,
` } else if (e instanceof BuildMessage) {`,
` const error: Error = e;`,
` console.error(error.stack, e.notes, e.line, e.column);`,
` }`,
`}`,
].join("\n"),
);
});
});

Expand Down
Loading