Skip to content
Draft
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
2 changes: 1 addition & 1 deletion docs/runtime/globals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Bun implements the following globals.
| [`__filename`](https://nodejs.org/api/globals.html#__filename) | Node.js | |
| [`atob()`](https://developer.mozilla.org/en-US/docs/Web/API/atob) | Web | |
| [`btoa()`](https://developer.mozilla.org/en-US/docs/Web/API/btoa) | Web | |
| `BuildMessage` | Bun | |
| `BuildMessage` | Bun | A diagnostic in the `logs` of [`Bun.build`](/bundler). A file that Bun's parser refuses fails `import` and `require()` with a `SyntaxError` that has `position`, `notes` and `level`. |
| [`clearImmediate()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearImmediate) | Web | |
| [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearInterval) | Web | |
| [`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout) | Web | |
Expand Down
12 changes: 12 additions & 0 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,18 @@ declare class ResolveMessage {
toString(): string;
}

/**
* A diagnostic of Bun's bundler and transpiler. {@link Bun.build} lists them
* in `logs`. {@link Bun.Transpiler} throws one, or an `AggregateError` of
* several.
*
* A file that Bun's parser refuses fails `import`, `import()` and `require()`
* with a `SyntaxError`, not with a `BuildMessage`. That `SyntaxError` has
* three more properties, with the values of its first error: `position`
* ({@link Position}), `notes` (an array of `BuildMessage`) and `level`
* (`"error"`). The `SyntaxError` of a file that JavaScriptCore's parser
* refuses, or of a `.json` file, does not have them.
*/
declare class BuildMessage {
readonly name: "BuildMessage";
readonly position: Position | null;
Expand Down
3 changes: 2 additions & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "4d45755fed4aeef19e95beb90d3d0c884a2812bd";
// Preview build of oven-sh/WebKit#451 (4d45755fed plus its two commits). Replace it with the merged sha before this lands.
export const WEBKIT_VERSION = "autobuild-preview-pr-451-5ba62c84";

/**
* WebKit (JavaScriptCore) — the JS engine, with WTF and bmalloc.
Expand Down
1 change: 1 addition & 0 deletions src/js/builtins/BunBuiltinNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ using namespace JSC;
macro(lazy) \
macro(lightStandardStreams) \
macro(lineText) \
macro(loadDiagnostics) \
macro(loadEsmIntoCjs) \
macro(main) \
macro(makeAbortError) \
Expand Down
30 changes: 29 additions & 1 deletion src/js/internal/repl/node-shims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ const {
RegExpPrototypeSymbolReplace,
RegExpPrototypeSymbolSplit,
StringPrototypeIncludes,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = require("internal/repl/node-primordials");

// ---- internal/util ----------------------------------------------------
Expand All @@ -35,6 +37,30 @@ function SideEffectFreeRegExpPrototypeSymbolSplit(regexp: RegExp, str: string, l
return RegExpPrototypeSymbolSplit(regexp, str, limit);
}

// Node's decorateErrorStack() puts the file, the source line and a caret above the message of a parse error.
function parseErrorLocation(err: object): string {
try {
if ((err as { name?: unknown }).name !== "SyntaxError") return "";
const position = (err as { position?: { file?: unknown; line?: unknown; column?: unknown; lineText?: unknown } })
.position;
if (typeof position?.file !== "string" || typeof position.line !== "number") return "";
const { lineText, column } = position;
// Like Node, no source line when it is excessively long.
if (
typeof lineText !== "string" ||
lineText.length > 1024 ||
typeof column !== "number" ||
!(column >= 1 && column <= lineText.length + 1)
) {
return `${position.file}:${position.line}\n`;
}
const indent = RegExpPrototypeSymbolReplace(/[^\t]/g, StringPrototypeSlice(lineText, 0, column - 1), " ");
return `${position.file}:${position.line}\n${lineText}\n${indent}^\n\n`;
} catch {
return "";
}
}

function decorateErrorStack<E extends { stack?: unknown } | null | undefined>(err: E) {
// JSC materializes stacks eagerly so Node's overrideStackTrace never runs;
// reproduce it by normalizing "<anonymous> (loc)" frames and cutting at the
Expand All @@ -47,7 +73,9 @@ function decorateErrorStack<E extends { stack?: unknown } | null | undefined>(er
if (RegExpPrototypeExec(/^\s+at REPL\d*:\d+:\d+$/, lines[i]) !== null) anonIdx = i;
}
if (anonIdx !== -1) lines = ArrayPrototypeSlice(lines, 0, anonIdx);
const newStack = ArrayPrototypeJoin(lines, "\n");
let newStack = ArrayPrototypeJoin(lines, "\n");
const location = parseErrorLocation(err);
if (location !== "" && !StringPrototypeStartsWith(newStack, location)) newStack = location + newStack;
if (newStack !== err.stack) {
// Errors with a non-writable .stack (Object.freeze, getter-only) must
// not turn into a TypeError that escapes the REPL's error handler.
Expand Down
6 changes: 6 additions & 0 deletions src/js_parser_jsc/Macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@ impl<'a> Run<'a> {
// SAFETY: `vm()` is the per-thread VM; uniquely accessed here.
let _ =
unsafe { (*self.macro_.vm()).uncaught_exception(self.global, value, false) };
// The error of a file that does not parse fails the macro, as a `BuildMessage` does below.
if BuildMessage::load_diagnostics(self.global, value).is_some() {
return Err(MacroError::MacroFailed);
}
return Ok(self.caller);
}
T::Undefined => {
Expand Down Expand Up @@ -895,6 +899,8 @@ impl<'a> Run<'a> {
}

let promise = value.as_any_promise().expect("Unexpected promise type");
// A rejection is reported below, not by the rejection tracker.
promise.set_handled(self.global.vm());

let _ = self.macro_.vm();
let vm = VirtualMachine::get();
Expand Down
131 changes: 131 additions & 0 deletions src/jsc/BuildMessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,138 @@ pub struct BuildMessage {
pub(crate) logged: Cell<bool>,
}

unsafe extern "C" {
fn Bun__createLoaderSyntaxError(
global: &JSGlobalObject,
message: *const u8,
message_length: usize,
source_url: *const u8,
source_url_length: usize,
line: u32,
column: u32,
stack: *const u8,
stack_length: usize,
first: JSValue,
all: JSValue,
) -> JSValue;
safe fn Bun__loaderSyntaxErrorDiagnostic(
global: &JSGlobalObject,
value: JSValue,
index: u32,
) -> JSValue;
}

/// The diagnostics of the module loader's `SyntaxError`, borrowed from this holder, which keeps the error alive.
pub struct LoadDiagnostics<'a> {
global: &'a JSGlobalObject,
error: JSValue,
}

impl LoadDiagnostics<'_> {
fn get(&self, index: u32) -> Option<&bun_ast::Msg> {
let cell = Bun__loaderSyntaxErrorDiagnostic(self.global, self.error, index);
if cell.is_empty() {
return None;
}
// The error holds the wrappers under a private name, so they live as long as the error does.
match cell.as_class_ref::<BuildMessage>() {
Some(build) => Some(&build.msg),
None => cell
.as_class_ref::<crate::ResolveMessage>()
.map(|resolve| &resolve.msg),
}
}

/// In log order.
pub fn iter(&self) -> impl Iterator<Item = &bun_ast::Msg> + '_ {
(0u32..).map_while(|index| self.get(index))
}
}

impl Drop for LoadDiagnostics<'_> {
fn drop(&mut self) {
self.error.ensure_still_alive();
}
}

impl BuildMessage {
/// The diagnostics of `value`, when it is the `SyntaxError` of [`Self::to_syntax_error`].
pub fn load_diagnostics(
global: &JSGlobalObject,
value: JSValue,
) -> Option<LoadDiagnostics<'_>> {
if !value.is_cell() || value.js_type() != crate::JSType::ErrorInstance {
return None;
}
let diagnostics = LoadDiagnostics {
global,
error: value,
};
diagnostics.get(0)?;
Some(diagnostics)
}

/// Whether `msg`, the first error of a log, is a syntax error: it has a line, and it is not a stack overflow.
pub(crate) fn is_syntax_error(msg: &bun_ast::Msg) -> bool {
!matches!(msg.metadata, bun_ast::Metadata::Resolve(_))
&& msg
.data
.location
.as_ref()
.is_some_and(|location| location.line > 0)
&& &*msg.data.text != b"Maximum call stack size exceeded"
}

/// The loader's `SyntaxError` for this diagnostic. `all` is `ZERO`, or an array of the wrappers that it keeps.
pub(crate) fn to_syntax_error(
&self,
global: &JSGlobalObject,
this_value: JSValue,
all: JSValue,
) -> JsResult<JSValue> {
let text: &[u8] = &self.msg.data.text;
let location = self
.msg
.data
.location
.as_ref()
.expect("a diagnostic that is_syntax_error accepts has a location");
let file: &[u8] = &location.file;
let line = location.line.max(0) as u32;
let column = location.column.max(0) as u32;

// The frame has the form of a JavaScriptCore parse error from `require()`, which the error printer reads back.
let mut stack = Vec::with_capacity(text.len() + file.len() + 56);
stack.extend_from_slice(b"SyntaxError: ");
stack.extend_from_slice(text);
stack.extend_from_slice(b"\n at <parse> (");
stack.extend_from_slice(file);
write!(&mut stack, ":{line}").expect("infallible: in-memory write");
if column > 0 {
write!(&mut stack, ":{column}").expect("infallible: in-memory write");
}
stack.push(b')');

crate::host_fn::from_js_host_call(global, || {
// SAFETY: C++ copies the three byte ranges before it returns.
unsafe {
Bun__createLoaderSyntaxError(
global,
text.as_ptr(),
text.len(),
file.as_ptr(),
file.len(),
line,
column,
stack.as_ptr(),
stack.len(),
this_value,
all,
)
}
})
}

pub fn get_notes(&self, cx: &mut JsScope<'_>) -> JsResult<JSValue> {
let global = cx.global();
JSValue::create_array_from_iter(global, self.msg.notes.iter(), |note| {
Expand Down
13 changes: 13 additions & 0 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3864,6 +3864,19 @@ pub mod formatter {
return Ok(());
}

// The module loader's `SyntaxError` prints as a `BuildMessage` does: the text of its diagnostics.
if let Some(diagnostics) =
crate::BuildMessage::load_diagnostics(self.global_this, value)
{
for (i, msg) in diagnostics.iter().enumerate() {
if i > 0 {
let _ = writer_.write_all(b"\n\n");
}
let _ = msg.write_format::<C>(&mut bun_io::AsFmt::new(&mut *writer_));
}
return Ok(());
}

let mut adapter = DynWriteAdapter::new(&mut *writer_);
// SAFETY: per-thread VM.
let vm = VirtualMachine::get().as_mut();
Expand Down
Loading
Loading