Compare commits

...

10 Commits

Author SHA1 Message Date
Zack Radisic
95866b9428 WIP 2025-05-01 18:44:06 -07:00
Zack Radisic
ac666c28a8 fix that 2025-05-01 12:06:44 -07:00
Jarred Sumner
a1e5fb8c47 Merge branch 'main' into zack/devserver-log-inspector 2025-05-01 02:47:58 -07:00
Jarred Sumner
ed4f93e6a6 docs 2025-05-01 02:39:05 -07:00
Jarred Sumner
e2a6545e29 Update hmr-runtime-client.ts 2025-05-01 02:07:08 -07:00
Jarred Sumner
0f23dfdef2 Send console.logs before the websocket opens 2025-05-01 01:22:44 -07:00
Jarred Sumner
1a703edaf5 Support streaming console.log 2025-05-01 00:22:49 -07:00
Jarred Sumner
1ef077628a Update SetupWebKit.cmake 2025-04-30 21:07:26 -07:00
Zack Radisic
81b2255066 fixes 2025-04-30 20:46:50 -07:00
Zack Radisic
d92659ca03 add console logs to devserver inspector 2025-04-30 15:06:29 -07:00
17 changed files with 1127 additions and 55 deletions

View File

@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION e26b186170c48a40d872c05a5ba61821a3f31196)
set(WEBKIT_VERSION c244f567ab804c2558067d00733013c01725d824)
endif()
string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX)

View File

@@ -203,6 +203,34 @@ When `development` is `true`, Bun will:
- Include the `SourceMap` header in the response so that devtools can show the original source code
- Disable minification
- Re-bundle assets on each request to a .html file
- Enable hot module reloading (unless `hmr: false` is set)
#### Echo console logs from browser to terminal
Bun.serve() supports echoing console logs from the browser to the terminal.
To enable this, pass `console: true` in the `development` object in `Bun.serve()`.
```ts
import homepage from "./index.html";
Bun.serve({
// development can also be an object.
development: {
// Enable Hot Module Reloading
hmr: true,
// Echo console logs from the browser to the terminal
console: true,
},
routes: {
"/": homepage,
},
});
```
When `console: true` is set, Bun will stream console logs from the browser to the terminal. This reuses the existing WebSocket connection from HMR to send the logs.
#### Production mode

View File

@@ -194,6 +194,18 @@ import "tailwindcss";
Only one of those are necessary, not all three.
## `--console` streams console logs from the browser to the terminal
Bun's dev server supports streaming console logs from the browser to the terminal.
To enable, pass the `--console` CLI flag.
{% bunDevServerTerminal alt="bun --console ./index.html" path="./index.html" routes="" /%}
Each call to `console.log` or `console.error` will be broadcast to the terminal that started the server.
Internally, this reuses the existing WebSocket connection from hot module reloading to send the logs.
## Keyboard Shortcuts
While the server is running:
@@ -288,7 +300,6 @@ All paths are resolved relative to your HTML file, making it easy to organize yo
## This is a work in progress
- No HMR support yet
- Need more plugins
- Need more configuration options for things like asset handling
- Need a way to configure CORS, headers, etc.

View File

@@ -3258,6 +3258,12 @@ declare module "bun" {
*
*/
hmr?: boolean;
/**
* Enable console log streaming from browser to server
* @default false
*/
console?: boolean;
};
error?: (this: Server, error: ErrorLike) => Response | Promise<Response> | void | Promise<void>;

View File

@@ -19,6 +19,7 @@ pub const Options = struct {
vm: *VirtualMachine,
framework: bake.Framework,
bundler_options: bake.SplitBundlerOptions,
broadcast_console_log_from_browser_to_server: bool,
// Debugging features
dump_sources: ?[]const u8 = if (Environment.isDebug) ".bake-debug" else null,
@@ -193,8 +194,9 @@ next_bundle: struct {
requests: DeferredRequest.List,
},
deferred_request_pool: bun.HiveArray(DeferredRequest.Node, DeferredRequest.max_preallocated).Fallback,
hmr_socket_id_counter: i32 = 0,
/// UWS can handle closing the websocket connections themselves
active_websocket_connections: std.AutoHashMapUnmanaged(*HmrSocket, void),
active_websocket_connections: std.AutoHashMapUnmanaged(HmrSocket.Id, *HmrSocket),
relative_path_buf_lock: bun.DebugThreadLock,
relative_path_buf: bun.PathBuffer,
@@ -217,6 +219,15 @@ has_pre_crash_handler: bool,
/// Can be enabled with env var `BUN_ASSUME_PERFECT_INCREMENTAL=1`
assume_perfect_incremental_bundling: bool = false,
/// If true, console logs from the browser will be echoed to the server console.
/// This works by overriding console.log & console.error in hmr-runtime-client.ts
/// with a function that sends the message from the client to the server.
///
/// There are two usecases:
/// - Echoing browser console logs to the server for debugging
/// - WebKit Inspector remote debugging integration
broadcast_console_log_from_browser_to_server: bool = false,
pub const internal_prefix = "/_bun";
/// Assets which are routed to the `Assets` storage.
pub const asset_prefix = internal_prefix ++ "/asset";
@@ -430,7 +441,7 @@ pub fn init(options: Options) bun.JSOOM!*DevServer {
.reload_event = null,
.requests = .{},
},
.debugger_id = .init(0), // TODO paper clover:
.debugger_id = BunFrontendDevServerAgent.newDevServerID(),
.assets = .{
.path_map = .empty,
.files = .empty,
@@ -448,7 +459,7 @@ pub fn init(options: Options) bun.JSOOM!*DevServer {
bun.getRuntimeFeatureFlag("BUN_ASSUME_PERFECT_INCREMENTAL"),
.relative_path_buf_lock = .unlocked,
.testing_batch_events = .disabled,
.broadcast_console_log_from_browser_to_server = options.broadcast_console_log_from_browser_to_server,
.server_transpiler = undefined,
.client_transpiler = undefined,
.ssr_transpiler = undefined,
@@ -658,6 +669,11 @@ pub fn init(options: Options) bun.JSOOM!*DevServer {
if (bun.FeatureFlags.bake_debugging_features and dev.has_pre_crash_handler)
try bun.crash_handler.appendPreCrashHandler(DevServer, dev, dumpStateDueToCrash);
// If the web inspector is enabled, track it.
if (dev.inspector()) |agent| {
agent.dev_servers.put(bun.default_allocator, dev.debugger_id, dev) catch bun.outOfMemory();
}
return dev;
}
@@ -674,7 +690,6 @@ pub fn deinit(dev: *DevServer) void {
.bundles_since_last_error = {},
.client_transpiler = {},
.configuration_hash_key = {},
.debugger_id = {},
.deferred_request_pool = {},
.emit_visualizer_events = {},
.framework = {},
@@ -688,7 +703,14 @@ pub fn deinit(dev: *DevServer) void {
.server_transpiler = {},
.ssr_transpiler = {},
.vm = {},
.hmr_socket_id_counter = {},
.debugger_id = {
if (dev.inspector()) |agent| {
const existed = agent.dev_servers.swapRemove(dev.debugger_id);
bun.debugAssert(existed);
}
},
.graph_safety_lock = dev.graph_safety_lock.lock(),
.bun_watcher = dev.bun_watcher.deinit(true),
.dump_dir = if (bun.FeatureFlags.bake_debugging_features) if (dev.dump_dir) |*dir| dir.close(),
@@ -769,7 +791,7 @@ pub fn deinit(dev: *DevServer) void {
dev.source_maps.entries.deinit(allocator);
},
.active_websocket_connections = {
var it = dev.active_websocket_connections.keyIterator();
var it = dev.active_websocket_connections.valueIterator();
while (it.next()) |item| {
const s: *HmrSocket = item.*;
if (s.underlying) |websocket|
@@ -789,6 +811,7 @@ pub fn deinit(dev: *DevServer) void {
},
.enable_after_bundle => {},
},
.broadcast_console_log_from_browser_to_server = {},
};
dev.allocation_scope.deinit();
bun.destroy(dev);
@@ -828,6 +851,7 @@ pub fn memoryCost(dev: *DevServer) usize {
.server_fetch_function_callback = {},
.server_register_update_callback = {},
.watcher_atomics = {},
.hmr_socket_id_counter = {},
// pointers that are not considered a part of DevServer
.vm = {},
@@ -839,7 +863,7 @@ pub fn memoryCost(dev: *DevServer) usize {
.framework = {},
.bundler_options = {},
.allocation_scope = {},
.broadcast_console_log_from_browser_to_server = {},
// to be counted.
.root = {
cost += dev.root.len;
@@ -1483,6 +1507,7 @@ fn onHtmlRequestWithBundle(dev: *DevServer, route_bundle_index: RouteBundle.Inde
const blob = html.cached_response orelse generate: {
const payload = generateHTMLPayload(dev, route_bundle_index, route_bundle, html) catch bun.outOfMemory();
errdefer dev.allocator.free(payload);
html.cached_response = StaticRoute.initFromAnyBlob(
&.fromOwnedSlice(dev.allocator, payload),
.{
@@ -1542,11 +1567,19 @@ fn generateHTMLPayload(dev: *DevServer, route_bundle_index: RouteBundle.Index, r
"<script type=\"module\" crossorigin src=\"\"></script>".len +
client_prefix.len + "/".len +
display_name.len +
"-0000000000000000.js".len;
"-0000000000000000.js".len +
if (dev.shouldReceiveConsoleLogFromBrowser()) "<meta name=\"bun:echo-console-log\" content=\"1\">".len else 0;
var array: std.ArrayListUnmanaged(u8) = try std.ArrayListUnmanaged(u8).initCapacity(dev.allocator, payload_size);
errdefer array.deinit(dev.allocator);
array.appendSliceAssumeCapacity(before_head_end);
// Mark the meta tag if console log streaming is enabled
// This will get removed from the HTML before the client-side code executes.
if (dev.shouldReceiveConsoleLogFromBrowser()) {
array.appendSliceAssumeCapacity("<meta name=\"bun:echo-console-log\" content=\"1\">");
}
// Insert all link tags before "</head>"
for (css_ids) |name| {
array.appendSliceAssumeCapacity("<link rel=\"stylesheet\" href=\"" ++ asset_prefix ++ "/");
@@ -2310,7 +2343,6 @@ pub fn finalizeBundle(
}
dev.allocation_scope.assertOwned(compile_result.code);
html.bundled_html_text = compile_result.code;
html.script_injection_offset = .init(compile_result.script_injection_offset);
chunk.entry_point.entry_point_id = @intCast(route_bundle_index.get());
@@ -5869,7 +5901,10 @@ pub fn onWebSocketUpgrade(
) void {
assert(id == 0);
const hmr_socket_id = HmrSocket.Id.init(dev.hmr_socket_id_counter);
dev.hmr_socket_id_counter += 1;
const dw = bun.create(dev.allocator, HmrSocket, .{
.id = hmr_socket_id,
.dev = dev,
.is_from_localhost = if (res.getRemoteSocketInfo()) |addr|
if (addr.is_ipv6)
@@ -5881,7 +5916,7 @@ pub fn onWebSocketUpgrade(
.subscriptions = .{},
.active_route = .none,
});
dev.active_websocket_connections.put(dev.allocator, dw, {}) catch bun.outOfMemory();
dev.active_websocket_connections.put(dev.allocator, hmr_socket_id, dw) catch bun.outOfMemory();
_ = res.upgrade(
*HmrSocket,
dw,
@@ -5990,6 +6025,9 @@ pub const MessageId = enum(u8) {
/// acknowledged by the watcher but intentionally took no action.
/// - `u8`: See bake-harness.ts WatchSynchronization enum.
testing_watch_synchronization = 'r',
/// Tell the client to take a screenshot of the current page and send it over.
/// - `u32`: Unique ID for the inspector request to associate it with a response.
screenshot = 's',
pub inline fn char(id: MessageId) u8 {
return @intFromEnum(id);
@@ -6006,11 +6044,23 @@ pub const IncomingMessageId = enum(u8) {
set_url = 'n',
/// Tells the DevServer to batch events together.
testing_batch_events = 'H',
/// Console log from the client
console_log = 'l',
/// Response of a screenshot request.
/// - `u32`: Unique ID to associate it with the request.
/// - `[]u8`: (Rest of the data) The screenshot encoded in base64
screenshot = 'S',
/// Invalid data
_,
};
pub const ConsoleLogKind = enum(u8) {
log = 'l',
err = 'e',
};
const HmrTopic = enum(u8) {
hot_update = 'h',
errors = 'e',
@@ -6047,7 +6097,8 @@ const HmrTopic = enum(u8) {
} });
};
const HmrSocket = struct {
pub const HmrSocket = struct {
pub const Id = bun.GenericIndex(i32, HmrSocket);
dev: *DevServer,
underlying: ?AnyWebSocket = null,
subscriptions: HmrTopic.Bits,
@@ -6056,7 +6107,7 @@ const HmrSocket = struct {
/// By telling DevServer the active route, this enables receiving detailed
/// `hot_update` events for when the route is updated.
active_route: RouteBundle.Index.Optional,
inspector_connection_id: i32 = -1,
id: Id,
pub fn onOpen(s: *HmrSocket, ws: AnyWebSocket) void {
const send_status = ws.send(&(.{MessageId.version.char()} ++ s.dev.configuration_hash_key), .binary, false, true);
@@ -6065,12 +6116,22 @@ const HmrSocket = struct {
if (send_status != .dropped) {
// Notify inspector about client connection
if (s.dev.inspector()) |agent| {
s.inspector_connection_id = agent.nextConnectionID();
agent.notifyClientConnected(s.dev.debugger_id, s.inspector_connection_id);
agent.notifyClientConnected(s.dev.debugger_id, s.id.get());
}
}
}
pub fn requestScreenshot(s: *HmrSocket, unique_id: u32) bool {
if (s.underlying) |ws| {
var payload: [1 + 4]u8 = undefined;
payload[0] = MessageId.screenshot.char();
std.mem.writeInt(u32, payload[1..], unique_id, std.builtin.Endian.big);
_ = ws.send(payload[0..], .binary, false, true);
return true;
}
return false;
}
pub fn onMessage(s: *HmrSocket, ws: AnyWebSocket, msg: []const u8, opcode: uws.Opcode) void {
_ = opcode;
@@ -6121,16 +6182,14 @@ const HmrSocket = struct {
const pattern = msg[1..];
const maybe_rbi = s.dev.routeToBundleIndexSlow(pattern);
if (s.dev.inspector()) |agent| {
if (s.inspector_connection_id > -1) {
var pattern_str = bun.String.init(pattern);
defer pattern_str.deref();
agent.notifyClientNavigated(
s.dev.debugger_id,
s.inspector_connection_id,
&pattern_str,
maybe_rbi,
);
}
var pattern_str = bun.String.init(pattern);
defer pattern_str.deref();
agent.notifyClientNavigated(
s.dev.debugger_id,
s.id.get(),
&pattern_str,
maybe_rbi,
);
}
const rbi = maybe_rbi orelse return;
if (s.active_route.unwrap()) |old| {
@@ -6181,6 +6240,56 @@ const HmrSocket = struct {
event.entry_points.deinit(s.dev.allocator);
},
},
.console_log => {
if (msg.len < 2) {
ws.close();
return;
}
const kind: ConsoleLogKind = switch (msg[1]) {
'l' => .log,
'e' => .err,
else => {
ws.close();
return;
},
};
const data = msg[2..];
if (s.dev.inspector()) |agent| {
var log_str = bun.String.init(data);
defer log_str.deref();
agent.notifyConsoleLog(s.dev.debugger_id, kind, &log_str);
}
if (s.dev.broadcast_console_log_from_browser_to_server) {
switch (kind) {
.log => {
bun.Output.print("{s}\n", .{data});
},
.err => {
bun.Output.printError("{s}\n", .{data});
},
}
bun.Output.flush();
}
},
.screenshot => {
if (s.dev.inspector()) |agent| {
var payload = msg[1..];
// We need at least 4 bytes for the unique ID and then the bytes for the base64 image
if (payload.len <= 4) {
ws.close();
return;
}
const unique_id = std.mem.readInt(u32, payload[0..4], std.builtin.Endian.big);
payload = payload[4..];
var screenshot_str = bun.String.init(payload);
defer screenshot_str.deref();
agent.notifyScreenshot(unique_id, &screenshot_str);
}
},
_ => ws.close(),
}
}
@@ -6190,11 +6299,9 @@ const HmrSocket = struct {
_ = exit_code;
_ = message;
if (s.inspector_connection_id > -1) {
// Notify inspector about client disconnection
if (s.dev.inspector()) |agent| {
agent.notifyClientDisconnected(s.dev.debugger_id, s.inspector_connection_id);
}
// Notify inspector about client disconnection
if (s.dev.inspector()) |agent| {
agent.notifyClientDisconnected(s.dev.debugger_id, s.id.get());
}
if (s.subscriptions.visualizer) {
@@ -6205,7 +6312,8 @@ const HmrSocket = struct {
s.dev.routeBundlePtr(old).active_viewers -= 1;
}
bun.debugAssert(s.dev.active_websocket_connections.remove(s));
const was_present_in_map = s.dev.active_websocket_connections.remove(s.id);
bun.debugAssert(was_present_in_map);
s.dev.allocator.destroy(s);
}
};
@@ -6859,6 +6967,13 @@ fn releaseRelativePathBuf(dev: *DevServer) void {
}
}
/// Either of two conditions make this true:
/// - The inspector is enabled
/// - The user passed "console": true in serve({development: {console: true}}) options
fn shouldReceiveConsoleLogFromBrowser(dev: *const DevServer) bool {
return dev.inspector() != null or dev.broadcast_console_log_from_browser_to_server;
}
fn dumpStateDueToCrash(dev: *DevServer) !void {
comptime assert(bun.FeatureFlags.bake_debugging_features);

629
src/bake/client/inspect.ts Normal file
View File

@@ -0,0 +1,629 @@
/* This file provides utilities for inspecting and formatting JavaScript values in client-side code.
* It implements a Node.js-like `inspect` function that converts any JavaScript value
* into a string representation, handling circular references, various object types,
* and respecting customization options.
*
* The implementation supports:
* - Primitive values (strings, numbers, booleans, etc.)
* - Complex objects and arrays with customizable depth
* - Special handling for typed arrays, Sets, Maps, and ArrayBuffers
* - Custom inspection via Symbol.for("nodejs.util.inspect.custom")
* - Configurable output formatting (indentation, truncation, etc.)
*
* This is mostly intended for pretty printing console.log from browser to CLI.
*/
const inspectSymbol = Symbol.for("nodejs.util.inspect.custom");
export interface InspectOptions {
showHidden?: boolean;
depth?: number;
maxArrayLength?: number;
maxStringLength?: number;
breakLength?: number;
compact?: number;
sorted?: boolean;
getters?: boolean;
numericSeparator?: boolean;
customInspect?: boolean;
}
export interface InspectContext extends InspectOptions {
seen: any[];
currentDepth: number;
}
// Default options
const defaultOptions: InspectOptions = {
showHidden: false,
depth: 2,
maxArrayLength: 100,
maxStringLength: 10000,
breakLength: 80,
compact: 3,
sorted: false,
getters: false,
numericSeparator: false,
customInspect: true,
} as const;
/**
* Main inspection function
* @param {any} obj - Object to inspect
* @param {Object} options - Configuration options
* @returns {string} String representation
*/
export function inspect(obj: any, options: InspectOptions = {}) {
// Set up context with merged options
const ctx: InspectContext = {
seen: [],
currentDepth: 0,
...defaultOptions,
...options,
} as InspectContext;
return formatValue(ctx, obj, 0);
}
/**
* Format a value based on its type
* @param {Object} ctx - Context object with settings
* @param {any} value - Value to format
* @param {number} recurseTimes - Current recursion depth
* @returns {string} Formatted value
*/
function formatValue(ctx: InspectContext, value: any, recurseTimes: number) {
// Handle primitive types
if (value === null) return "null";
if (value === undefined) return "undefined";
// Check for custom inspect implementation
if (
ctx.customInspect !== false &&
value !== null &&
typeof value === "object" &&
typeof value[inspectSymbol] === "function" &&
value[inspectSymbol] !== inspect
) {
return String(value[inspectSymbol](recurseTimes, { ...ctx }));
}
// Check for circular references
if (ctx.seen.includes(value)) {
return "[Circular]";
}
// Format based on type
switch (typeof value) {
case "string":
return formatString(ctx, value);
case "number":
return formatNumber(value, ctx.numericSeparator!);
case "bigint":
return `${value}n`;
case "boolean":
return `${value}`;
case "symbol":
return formatSymbol(value);
case "function":
return formatFunction(value);
case "object":
return formatObject(ctx, value, recurseTimes);
default:
return String(value);
}
}
/**
* Format a string with proper escaping
* @param {Object} ctx - Context object
* @param {string} value - String to format
* @returns {string} Formatted string
*/
function formatString(ctx, value) {
// Truncate long strings
if (value.length > ctx.maxStringLength) {
const remaining = value.length - ctx.maxStringLength;
const truncated = value.slice(0, ctx.maxStringLength);
return `'${escape(truncated)}'... ${remaining} more character${remaining > 1 ? "s" : ""}`;
}
return `'${escape(value)}'`;
}
/**
* Escape special characters in a string
* @param {string} str - String to escape
* @returns {string} Escaped string
*/
function escape(str) {
return str
.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t")
.replace(/[\x00-\x1F\x7F-\x9F]/g, ch => {
const code = ch.charCodeAt(0);
return `\\x${code.toString(16).padStart(2, "0")}`;
});
}
/**
* Format a number with optional numeric separators
* @param {number} value - Number to format
* @param {boolean} useNumericSeparator - Whether to use numeric separators
* @returns {string} Formatted number
*/
function formatNumber(value: number, useNumericSeparator: boolean) {
if (Object.is(value, -0)) return "-0";
if (!useNumericSeparator) return String(value);
const str = String(value);
if (!/^\d+$/.test(str)) return str;
// Add numeric separators for readability
return str.replace(/\B(?=(\d{3})+(?!\d))/g, "_");
}
/**
* Format a symbol
* @param {Symbol} value - Symbol to format
* @returns {string} Formatted symbol
*/
function formatSymbol(value: symbol) {
return value.toString();
}
/**
* Format a function
* @param {Function} value - Function to format
* @returns {string} Formatted function
*/
function formatFunction(value: Function) {
const name = value.name || "<anonymous>";
const constructorName = Object.getPrototypeOf(value)?.constructor?.name;
if (constructorName === "AsyncFunction") {
return `[AsyncFunction: ${name}]`;
}
if (constructorName === "GeneratorFunction") {
return `[GeneratorFunction: ${name}]`;
}
if (constructorName === "AsyncGeneratorFunction") {
return `[AsyncGeneratorFunction: ${name}]`;
}
return `[Function: ${name}]`;
}
/**
* Format an object based on its type
* @param {Object} ctx - Context object
* @param {any} value - Object to format
* @param {number} recurseTimes - Current recursion depth
* @returns {string} Formatted object
*/
function formatObject(ctx: InspectContext, value: any, recurseTimes: number) {
// Check recursion depth
if (recurseTimes >= ctx.depth!) {
if (Array.isArray(value)) return "[Array]";
return `[${getConstructorName(value)}]`;
}
// Mark as seen to detect circular references
ctx.seen.push(value);
recurseTimes += 1;
let output;
// Handle special object types
if (Array.isArray(value)) {
output = formatArray(ctx, value, recurseTimes);
} else if (value instanceof Date) {
output = formatDate(value);
} else if (value instanceof RegExp) {
output = formatRegExp(value);
} else if (value instanceof Error) {
output = formatError(value);
} else if (value instanceof Map) {
output = formatMap(ctx, value, recurseTimes);
} else if (value instanceof Set) {
output = formatSet(ctx, value, recurseTimes);
} else if (value instanceof WeakMap) {
output = "WeakMap { ... }";
} else if (value instanceof WeakSet) {
output = "WeakSet { ... }";
} else if (value instanceof Promise) {
output = "Promise { ... }";
} else if (ArrayBuffer.isView(value)) {
output = formatTypedArray(ctx, value as ArrayBufferView & { length: number });
} else if (value instanceof ArrayBuffer) {
output = formatArrayBuffer(ctx, value);
} else {
// Regular object
output = formatPlainObject(ctx, value, recurseTimes);
}
// Remove from seen
ctx.seen.pop();
return output;
}
/**
* Format an array
* @param {Object} ctx - Context object
* @param {Array} value - Array to format
* @param {number} recurseTimes - Current recursion depth
* @returns {string} Formatted array
*/
function formatArray(ctx: InspectContext, value: any[], recurseTimes: number) {
// Special case for empty arrays
if (value.length === 0) return "[]";
const maxLength = Math.min(ctx.maxArrayLength!, value.length);
const output: string[] = [];
for (let i = 0; i < maxLength; i++) {
if (Object.prototype.hasOwnProperty.call(value, i)) {
output.push(formatValue(ctx, value[i], recurseTimes));
} else {
output.push("empty");
}
}
if (value.length > maxLength) {
const remaining = value.length - maxLength;
output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`);
}
// Add array properties that aren't indices
const keys = Object.keys(value).filter(key => {
return !(Number(key) >= 0 && Number(key) < value.length && Number(key) === +key);
});
if (keys.length > 0) {
for (const key of keys) {
output.push(`${key}: ${formatValue(ctx, value[key], recurseTimes)}`);
}
}
return `[ ${output.join(", ")} ]`;
}
/**
* Format a plain object with property enumeration
* @param {Object} ctx - Context object
* @param {Object} value - Object to format
* @param {number} recurseTimes - Current recursion depth
* @returns {string} Formatted object
*/
function formatPlainObject(ctx: InspectContext, value: any, recurseTimes: number) {
// Get constructor name for the prefix
const constructorName = getConstructorName(value);
const prefix = constructorName !== "Object" ? `${constructorName} ` : "";
// Get own and inherited properties
const keys = getObjectKeys(value, ctx.showHidden);
if (keys.length === 0) {
// Handle empty objects
if (constructorName !== "Object" && getPrototypeKeys(value).length > 0) {
// If the object has no own properties but has inherited ones
return formatWithPrototype(ctx, value, recurseTimes, constructorName);
}
return `${prefix}{}`;
}
// Format properties
const output: string[] = [];
for (const key of keys) {
try {
const desc = Object.getOwnPropertyDescriptor(value, key);
if (desc) {
if (desc.get || desc.set) {
if (desc.get && desc.set) {
output.push(`${formatPropertyKey(key)}: [Getter/Setter]`);
} else if (desc.get) {
output.push(`${formatPropertyKey(key)}: [Getter]`);
} else {
output.push(`${formatPropertyKey(key)}: [Setter]`);
}
} else {
output.push(`${formatPropertyKey(key)}: ${formatValue(ctx, value[key], recurseTimes)}`);
}
}
} catch (err) {
output.push(`${formatPropertyKey(key)}: undefined`);
}
}
// Sort keys if requested
if (ctx.sorted) {
output.sort();
}
// Create the final string
if (output.length === 0) {
return `${prefix}{}`;
}
// Check if it fits on one line
if (output.join(", ").length < ctx.breakLength!) {
return `${prefix}{ ${output.join(", ")} }`;
}
// Otherwise format with line breaks
return `${prefix}{\n ${output.join(",\n ")}\n}`;
}
/**
* Format an object by showing its prototype chain properties
* @param {Object} ctx - Context object
* @param {Object} value - Object to format
* @param {number} recurseTimes - Current recursion depth
* @param {string} constructorName - Constructor name
* @returns {string} Formatted object with prototype info
*/
function formatWithPrototype(ctx: InspectContext, value: any, recurseTimes: number, constructorName: string) {
const protoKeys = getPrototypeKeys(value);
if (protoKeys.length === 0) {
return `${constructorName} {}`;
}
const output: string[] = [];
for (const key of protoKeys) {
try {
// Add prototype prefix to distinguish from own properties
output.push(`${formatPropertyKey(key)}: ${formatValue(ctx, value[key], recurseTimes)}`);
} catch (err) {
output.push(`${formatPropertyKey(key)}: undefined`);
}
}
if (ctx.sorted) {
output.sort();
}
if (output.length === 0) {
return `${constructorName} {}`;
}
if (output.join(", ").length < ctx.breakLength!) {
return `${constructorName} { ${output.join(", ")} }`;
}
return `${constructorName} {\n ${output.join(",\n ")}\n}`;
}
/**
* Get keys from an object's prototype
* @param {Object} obj - Object to inspect
* @returns {Array} Array of prototype keys
*/
function getPrototypeKeys(obj) {
const proto = Object.getPrototypeOf(obj);
if (!proto || proto === Object.prototype) {
return [];
}
const protoKeys = Object.getOwnPropertyNames(proto).filter(key => {
if (key === "constructor") return false;
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
return typeof descriptor?.value !== "function" && key !== "__proto__";
});
return protoKeys;
}
/**
* Format a property key with proper quoting if needed
* @param {string|Symbol} key - Property key
* @returns {string} Formatted key
*/
function formatPropertyKey(key: string | symbol) {
if (typeof key === "symbol") {
return `[${key.toString()}]`;
}
if (key === "__proto__") {
return "['__proto__']";
}
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
return key;
}
return `'${escape(String(key))}'`;
}
/**
* Get all relevant keys from an object
* @param {Object} obj - Object to inspect
* @param {boolean} showHidden - Whether to include non-enumerable properties
* @returns {Array} Array of property keys
*/
function getObjectKeys(obj, showHidden) {
if (showHidden) {
return Object.getOwnPropertyNames(obj);
}
return Object.keys(obj);
}
/**
* Get constructor name of an object
* @param {Object} obj - Object to inspect
* @returns {string} Constructor name
*/
function getConstructorName(obj: any) {
if (!obj || typeof obj !== "object") {
return "";
}
let constructorName = obj.constructor?.name;
if (!constructorName) {
const prototype = Object.getPrototypeOf(obj);
const protoName = prototype?.constructor?.name;
if (protoName) {
constructorName = protoName;
}
}
return constructorName || "Object";
}
/**
* Format a Date object
* @param {Date} value - Date to format
* @returns {string} Formatted date
*/
function formatDate(value) {
// Check if date is valid
if (isNaN(value.getTime())) {
return "Invalid Date";
}
return `${value.toISOString()} [Date]`;
}
/**
* Format a RegExp object
* @param {RegExp} value - RegExp to format
* @returns {string} Formatted regexp
*/
function formatRegExp(value) {
return String(value);
}
/**
* Format an Error object
* @param {Error} value - Error to format
* @returns {string} Formatted error
*/
function formatError(value: Error) {
return value?.stack || value + "";
}
/**
* Format a Map object
* @param {Object} ctx - Context object
* @param {Map} value - Map to format
* @param {number} recurseTimes - Current recursion depth
* @returns {string} Formatted map
*/
function formatMap(ctx: InspectContext, value: Map<any, any>, recurseTimes: number) {
const output: string[] = [];
const size = value.size;
let i = 0;
for (const [k, v] of value) {
if (i >= ctx.maxArrayLength!) {
const remaining = size - ctx.maxArrayLength!;
output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`);
break;
}
output.push(`${formatValue(ctx, k, recurseTimes)} => ${formatValue(ctx, v, recurseTimes)}`);
i++;
}
if (output.length === 0) {
return "Map {}";
}
const joined = output.join(", ");
if (joined.length < ctx.breakLength!) {
return `Map { ${joined} }`;
}
return `Map {\n ${output.join(",\n ")}\n}`;
}
/**
* Format a Set object
* @param {Object} ctx - Context object
* @param {Set} value - Set to format
* @param {number} recurseTimes - Current recursion depth
* @returns {string} Formatted set
*/
function formatSet(ctx: InspectContext, value: Set<any>, recurseTimes: number) {
const output: string[] = [];
const size = value.size;
let i = 0;
const max = ctx.maxArrayLength!;
for (const v of value) {
if (i >= max) {
const remaining = size - max;
output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`);
break;
}
output.push(formatValue(ctx, v, recurseTimes));
i++;
}
if (output.length === 0) {
return "Set {}";
}
if (output.join(", ").length < ctx.breakLength!) {
return `Set { ${output.join(", ")} }`;
}
return `Set {\n ${output.join(",\n ")}\n}`;
}
/**
* Format a typed array
* @param {Object} ctx - Context object
* @param {TypedArray} value - Typed array to format
* @returns {string} Formatted typed array
*/
function formatTypedArray(ctx: InspectContext, value: ArrayBufferView & { length: number }) {
const name = value.constructor.name;
const length = value.length;
const maxLength = Math.min(ctx.maxArrayLength!, length);
const output: string[] = [];
for (let i = 0; i < maxLength; i++) {
output.push(String(value[i]));
}
if (value.length > maxLength) {
const remaining = value.length - maxLength;
output.push(`... ${remaining} more item${remaining > 1 ? "s" : ""}`);
}
return `${name} [ ${output.join(", ")} ]`;
}
/**
* Format an ArrayBuffer
* @param {Object} ctx - Context object
* @param {ArrayBuffer} value - ArrayBuffer to format
* @returns {string} Formatted array buffer
*/
function formatArrayBuffer(ctx: InspectContext, value: ArrayBuffer) {
const constructorName = getConstructorName(value);
let bytes;
try {
bytes = new Uint8Array(value);
} catch {
return `${constructorName} { [Detached] }`;
}
const byteLength = bytes.byteLength;
const maxLength = Math.min(ctx.maxArrayLength!, byteLength);
const output: string[] = [];
for (let i = 0; i < maxLength; i++) {
output.push(bytes[i].toString(16).padStart(2, "0"));
}
if (byteLength > maxLength) {
const remaining = byteLength - maxLength;
output.push(`... ${remaining} more byte${remaining > 1 ? "s" : ""}`);
}
return `${constructorName} { ${output.join(" ")} }`;
}

View File

@@ -42,6 +42,13 @@ interface WebSocketWrapper {
/** When re-connected, this is re-assigned */
wrapped: WebSocket | null;
send(data: string | ArrayBuffer): void;
/**
* Send data once the connection is established.
* Buffer if the connection is not established yet.
*
* @param data String or ArrayBuffer
*/
sendBuffered(data: string | ArrayBuffer): void;
close(): void;
[Symbol.dispose](): void;
}
@@ -57,6 +64,13 @@ export function initWebSocket(
let firstConnection = true;
let closed = false;
// Allow some messages to be queued if sent before the connection is established.
let sendQueue: Array<string | ArrayBuffer> = [];
let sendQueueLength = 0;
// Don't queue infinite data incase the user has a bug somewhere in their code.
const MAX_SEND_QUEUE_LENGTH = 1024 * 256;
const wsProxy: WebSocketWrapper = {
wrapped: null,
send(data) {
@@ -65,6 +79,15 @@ export function initWebSocket(
wrapped.send(data);
}
},
sendBuffered(data) {
const wrapped = this.wrapped;
if (wrapped && wrapped.readyState === 1) {
wrapped.send(data);
} else if (wrapped && wrapped.readyState === 0 && sendQueueLength < MAX_SEND_QUEUE_LENGTH) {
sendQueue.push(data);
sendQueueLength += typeof data === "string" ? data.length : (data as ArrayBuffer).byteLength;
}
},
close() {
closed = true;
this.wrapped?.close();
@@ -84,6 +107,14 @@ export function initWebSocket(
function onFirstOpen() {
console.info("[Bun] Hot-module-reloading socket connected, waiting for changes...");
onStatusChange?.(true);
// Drain the send queue.
const oldSendQueue = sendQueue;
sendQueue = [];
sendQueueLength = 0;
for (const data of oldSendQueue) {
wsProxy.send(data);
}
}
function onMessage(ev: MessageEvent<string | ArrayBuffer>) {
@@ -110,6 +141,9 @@ export function initWebSocket(
await new Promise(done => setTimeout(done, 1000));
// Clear the send queue.
sendQueue.length = sendQueueLength = 0;
while (true) {
if (closed) return;

View File

@@ -1,6 +1,6 @@
// This file is the entrypoint to the hot-module-reloading runtime
// In the browser, this uses a WebSocket to communicate with the bundler.
import './debug';
import "./debug";
import {
loadModuleAsync,
replaceModules,
@@ -9,13 +9,16 @@ import {
emitEvent,
fullReload,
} from "./hmr-module";
import { inspect } from "./client/inspect";
import { hasFatalError, onServerErrorPayload, onRuntimeError } from "./client/overlay";
import { DataViewReader } from "./client/data-view";
import { initWebSocket } from "./client/websocket";
import { MessageId } from "./generated";
import { editCssContent, editCssArray } from "./client/css-reloader";
import { td } from "./shared";
import { addMapping, SourceMapURL } from './client/stack-trace';
import { addMapping, SourceMapURL } from "./client/stack-trace";
const consoleErrorWithoutInspector = console.error;
if (typeof IS_BUN_DEVELOPMENT !== "boolean") {
throw new Error("DCE is configured incorrectly");
@@ -75,7 +78,7 @@ globalThis[Symbol.for("bun:hmr")] = (modules: any, id: string) => {
console.error(e);
fullReload();
});
}
};
let isFirstRun = true;
const handlers = {
@@ -163,13 +166,13 @@ const handlers = {
// JavaScript modules
if (reader.hasMoreData()) {
const rest = reader.rest();
const sourceMapId = td.decode(new Uint8Array(rest, rest.byteLength - 24, 16))
const sourceMapId = td.decode(new Uint8Array(rest, rest.byteLength - 24, 16));
DEBUG.ASSERT(sourceMapId.match(/[a-f0-9]{16}/));
const blob = new Blob([rest], { type: 'application/javascript' });
const blob = new Blob([rest], { type: "application/javascript" });
const url = URL.createObjectURL(blob);
const script = document.createElement('script');
const script = document.createElement("script");
scriptTags.set(sourceMapId, [script, rest.byteLength]);
script.className = 'bun-hmr-script';
script.className = "bun-hmr-script";
script.src = url;
script.onerror = onHmrLoadError;
document.head.appendChild(script);
@@ -183,6 +186,47 @@ const handlers = {
currentRouteIndex = reader.u32();
},
[MessageId.errors]: onServerErrorPayload,
[MessageId.screenshot]() {
// TODO: feature detect this, not all browsers support it?
// Use the MediaStream Image Capture API as described in the API documentation
navigator.mediaDevices
.getUserMedia({ video: true })
.then(mediaStream => {
// Get the video track from the stream
const track = mediaStream.getVideoTracks()[0];
// Create an ImageCapture object
const imageCapture = new ImageCapture(track);
// Take a photo
return imageCapture.takePhoto();
})
.then(blob => {
// Convert blob to base64 data URL
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
})
.then(dataUrl => {
// Send the screenshot data to the server
// Prefix with "S" for screenshot message type
ws.send("S" + dataUrl);
// Clean up - stop all video tracks
navigator.mediaDevices
.getUserMedia({ video: true })
.then(stream => {
stream.getTracks().forEach(track => track.stop());
})
.catch(err => console.error("Error stopping tracks:", err));
})
.catch(error => {
console.error("Failed to capture screenshot:", error);
});
},
};
const ws = initWebSocket(handlers, {
onStatusChange(connected) {
@@ -196,7 +240,7 @@ function onHmrLoadError(event: Event | string, source?: string, lineno?: number,
} else if (error) {
console.error(error);
} else {
console.error('Failed to load HMR script', event);
console.error("Failed to load HMR script", event);
}
fullReload();
}
@@ -240,6 +284,57 @@ window.addEventListener("unhandledrejection", event => {
}
}
// This implements streaming console.log and console.error from the browser to the server.
//
// Bun.serve({
// development: {
// console: true,
// ^^^^^^^^^^^^^^^^
// },
// })
//
let isStreamingConsoleLogFromBrowserToServer = document.querySelector("meta[name='bun:echo-console-log']");
if (isStreamingConsoleLogFromBrowserToServer) {
// Ensure it only runs once, and avoid the extra noise in the HTML.
isStreamingConsoleLogFromBrowserToServer.remove();
const originalLog = console.log;
function websocketInspect(logLevel: "l" | "e", values: any[]) {
let str = "l" + logLevel;
let first = true;
for (const value of values) {
if (first) {
first = false;
} else {
str += " ";
}
if (typeof value === "string") {
str += value;
} else {
str += inspect(value);
}
}
ws.sendBuffered(str);
}
if (typeof originalLog === "function") {
console.log = function log(...args: any[]) {
originalLog(...args);
websocketInspect("l", args);
};
}
if (typeof consoleErrorWithoutInspector === "function") {
console.error = function error(...args: any[]) {
consoleErrorWithoutInspector(...args);
websocketInspect("e", args);
};
}
}
try {
const { refresh } = config;
if (refresh) {
@@ -251,6 +346,8 @@ try {
emitEvent("bun:ready", null);
} catch (e) {
console.error(e);
// Use consoleErrorWithoutInspector to avoid double-reporting errors.
consoleErrorWithoutInspector(e);
onRuntimeError(e, true, false);
}

View File

@@ -1551,7 +1551,11 @@ fn NewSocket(comptime ssl: bool) type {
pub fn onWritable(this: *This, _: Socket) void {
JSC.markBinding(@src());
log("onWritable", .{});
log("onWritable detached={s}, native_callback_writable={s} sanity={s}", .{
if (this.socket.isDetached()) "true" else "false",
if (this.native_callback.onWritable()) "true" else "false",
if (this.handlers.onWritable == .zero) "true" else "false",
});
if (this.socket.isDetached()) return;
if (this.native_callback.onWritable()) return;
const handlers = this.handlers;
@@ -1565,6 +1569,7 @@ fn NewSocket(comptime ssl: bool) type {
this.ref();
defer this.deref();
this.internalFlush();
log("onWritable buffered_data_for_node_net {d}", .{this.buffered_data_for_node_net.len});
// is not writable if we have buffered data or if we are already detached
if (this.buffered_data_for_node_net.len > 0 or this.socket.isDetached()) return;

View File

@@ -385,6 +385,7 @@ pub const ServerConfig = struct {
sni: ?bun.BabyList(SSLConfig) = null,
max_request_body_size: usize = 1024 * 1024 * 128,
development: DevelopmentOption = .development,
broadcast_console_log_from_browser_to_server_for_bake: bool = false,
onError: JSC.JSValue = JSC.JSValue.zero,
onRequest: JSC.JSValue = JSC.JSValue.zero,
@@ -1307,6 +1308,10 @@ pub const ServerConfig = struct {
} else {
args.development = .development;
}
if (try dev.getBooleanStrict(global, "console")) |console| {
args.broadcast_console_log_from_browser_to_server_for_bake = console;
}
} else {
args.development = if (dev.toBoolean()) .development else .production;
}
@@ -6228,6 +6233,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
.framework = bake_options.framework,
.bundler_options = bake_options.bundler_options,
.vm = global.bunVM(),
.broadcast_console_log_from_browser_to_server = config.broadcast_console_log_from_browser_to_server_for_bake,
})
else
null;

View File

@@ -13,6 +13,8 @@ const InspectorBunFrontendDevServerAgentHandle = opaque {
extern "c" fn InspectorBunFrontendDevServerAgent__notifyClientNavigated(agent: *InspectorBunFrontendDevServerAgentHandle, devServerId: i32, connectionId: i32, url: *bun.String, routeBundleId: i32) void;
extern "c" fn InspectorBunFrontendDevServerAgent__notifyClientErrorReported(agent: *InspectorBunFrontendDevServerAgentHandle, devServerId: i32, clientErrorPayloadBase64: *bun.String) void;
extern "c" fn InspectorBunFrontendDevServerAgent__notifyGraphUpdate(agent: *InspectorBunFrontendDevServerAgentHandle, devServerId: i32, visualizerPayloadBase64: *bun.String) void;
extern "c" fn InspectorBunFrontendDevServerAgent__notifyConsoleLog(agent: *InspectorBunFrontendDevServerAgentHandle, devServerId: i32, kind: u8, data: *bun.String) void;
extern "c" fn InspectorBunFrontendDevServerAgent__notifyScreenshot(agent: *InspectorBunFrontendDevServerAgentHandle, uniqueId: u32, payload: *bun.String) void;
};
const notifyClientConnected = c.InspectorBunFrontendDevServerAgent__notifyClientConnected;
const notifyClientDisconnected = c.InspectorBunFrontendDevServerAgent__notifyClientDisconnected;
@@ -22,16 +24,22 @@ const InspectorBunFrontendDevServerAgentHandle = opaque {
const notifyClientNavigated = c.InspectorBunFrontendDevServerAgent__notifyClientNavigated;
const notifyClientErrorReported = c.InspectorBunFrontendDevServerAgent__notifyClientErrorReported;
const notifyGraphUpdate = c.InspectorBunFrontendDevServerAgent__notifyGraphUpdate;
const notifyConsoleLog = c.InspectorBunFrontendDevServerAgent__notifyConsoleLog;
const notifyScreenshot = c.InspectorBunFrontendDevServerAgent__notifyScreenshot;
};
pub const BunFrontendDevServerAgent = struct {
next_inspector_connection_id: i32 = 0,
handle: ?*InspectorBunFrontendDevServerAgentHandle = null,
var dev_server_id_counter: std.atomic.Value(u32) = std.atomic.Value(u32).init(0);
pub fn nextConnectionID(this: *BunFrontendDevServerAgent) i32 {
const id = this.next_inspector_connection_id;
this.next_inspector_connection_id +%= 1;
return id;
pub const BunFrontendDevServerAgent = struct {
handle: ?*InspectorBunFrontendDevServerAgentHandle = null,
dev_servers: std.AutoArrayHashMapUnmanaged(DevServer.DebuggerId, *DevServer) = .{},
pub fn newDevServerID() DevServer.DebuggerId {
return DevServer.DebuggerId.init(dev_server_id_counter.fetchAdd(1, .monotonic));
}
pub fn __insertDevServer(this: *BunFrontendDevServerAgent, dev_server: *DevServer) void {
this.dev_servers.put(bun.default_allocator, dev_server.debugger_id, dev_server) catch bun.outOfMemory();
}
pub fn isEnabled(this: BunFrontendDevServerAgent) bool {
@@ -101,9 +109,38 @@ pub const BunFrontendDevServerAgent = struct {
}
}
pub fn notifyConsoleLog(this: BunFrontendDevServerAgent, devServerId: DevServer.DebuggerId, kind: bun.bake.DevServer.ConsoleLogKind, data: *bun.String) void {
if (this.handle) |handle| {
handle.notifyConsoleLog(devServerId.get(), @intFromEnum(kind), data);
}
}
pub fn notifyScreenshot(this: BunFrontendDevServerAgent, unique_id: u32, payload: *bun.String) void {
if (this.handle) |handle| {
handle.notifyScreenshot(unique_id, payload);
}
}
export fn Bun__InspectorBunFrontendDevServerAgent__setEnabled(agent: ?*InspectorBunFrontendDevServerAgentHandle) void {
if (JSC.VirtualMachine.get().debugger) |*debugger| {
debugger.frontend_dev_server_agent.handle = agent;
}
}
export fn Bun__InspectorBunFrontendDevServerAgent__screenshot(_: *InspectorBunFrontendDevServerAgentHandle, dev_server_id_raw: i32, connectionId: i32, uniqueId: i32) c_int {
if (JSC.VirtualMachine.get().debugger) |*debugger| {
const dev_server_id = DevServer.DebuggerId.init(@intCast(dev_server_id_raw));
const dev_server = debugger.frontend_dev_server_agent.dev_servers.get(dev_server_id) orelse {
return -1;
};
const connection = dev_server.active_websocket_connections.get(bun.bake.DevServer.HmrSocket.Id.init(connectionId)) orelse {
return -2;
};
if (connection.requestScreenshot(@intCast(uniqueId))) {
return 0;
}
return -4;
}
return -3;
}
};

View File

@@ -16,6 +16,7 @@
namespace Inspector {
extern "C" void Bun__InspectorBunFrontendDevServerAgent__setEnabled(Inspector::InspectorBunFrontendDevServerAgent*);
extern "C" int Bun__InspectorBunFrontendDevServerAgent__screenshot(Inspector::InspectorBunFrontendDevServerAgent*, int serverId, int connectionId, int uniqueId);
WTF_MAKE_TZONE_ALLOCATED_IMPL(InspectorBunFrontendDevServerAgent);
@@ -61,6 +62,27 @@ Protocol::ErrorStringOr<void> InspectorBunFrontendDevServerAgent::disable()
return {};
}
void InspectorBunFrontendDevServerAgent::notifyScreenshot(int uniqueId, const String& payload)
{
if (!m_enabled || !m_frontendDispatcher)
return;
m_frontendDispatcher->screenshotted(uniqueId, payload);
}
Protocol::ErrorStringOr<void> InspectorBunFrontendDevServerAgent::screenshot(int serverId, int connectionId, int uniqueId)
{
int result = Bun__InspectorBunFrontendDevServerAgent__screenshot(this, serverId, connectionId, uniqueId);
if (result < 0) {
if (result == -1) return makeUnexpected("Failed to find dev server"_s);
if (result == -2) return makeUnexpected("Failed to find connection"_s);
if (result == -3) return makeUnexpected("Debugger not active"_s);
if (result == -4) return makeUnexpected("No websocket connection"_s);
return makeUnexpected("Failed for unknown reason"_s);
}
return {};
}
void InspectorBunFrontendDevServerAgent::clientConnected(int devServerId, int connectionId)
{
if (!m_enabled || !m_frontendDispatcher)
@@ -125,6 +147,14 @@ void InspectorBunFrontendDevServerAgent::graphUpdate(int devServerId, const Stri
// m_frontendDispatcher->graphUpdate(devServerId, visualizerPayloadBase64);
}
void InspectorBunFrontendDevServerAgent::consoleLog(int devServerId, char kind, const String& data)
{
if (!m_enabled || !m_frontendDispatcher)
return;
m_frontendDispatcher->consoleLog(devServerId, kind, data);
}
// C API implementations for Zig
extern "C" {
@@ -178,6 +208,16 @@ void InspectorBunFrontendDevServerAgent__notifyGraphUpdate(InspectorBunFrontendD
{
agent->graphUpdate(devServerId, visualizerPayloadBase64->toWTFString());
}
void InspectorBunFrontendDevServerAgent__notifyConsoleLog(InspectorBunFrontendDevServerAgent* agent, int devServerId, char kind, BunString* data)
{
agent->consoleLog(devServerId, kind, data->toWTFString());
}
void InspectorBunFrontendDevServerAgent__notifyScreenshot(InspectorBunFrontendDevServerAgent* agent, int uniqueId, BunString* payload)
{
agent->notifyScreenshot(uniqueId, payload->toWTFString());
}
}
} // namespace Inspector

View File

@@ -31,6 +31,7 @@ public:
// BunFrontendDevServerBackendDispatcherHandler
virtual Protocol::ErrorStringOr<void> enable() final;
virtual Protocol::ErrorStringOr<void> disable() final;
virtual Protocol::ErrorStringOr<void> screenshot(int serverId, int connectionId, int uniqueId) final;
// Public API for events
void clientConnected(int devServerId, int connectionId);
@@ -41,6 +42,8 @@ public:
void clientNavigated(int devServerId, int connectionId, const String& url, std::optional<int> routeBundleId);
void clientErrorReported(int devServerId, const String& clientErrorPayloadBase64);
void graphUpdate(int devServerId, const String& visualizerPayloadBase64);
void consoleLog(int devServerId, char kind, const String& data);
void notifyScreenshot(int uniqueId, const String& payload);
private:
// JSC::JSGlobalObject& m_globalobject;
@@ -59,6 +62,7 @@ void BunFrontendDevServerAgent__notifyBundleFailed(InspectorBunFrontendDevServer
void BunFrontendDevServerAgent__notifyClientNavigated(InspectorBunFrontendDevServerAgent* agent, int connectionId, const BunString* url, int routeBundleId);
void BunFrontendDevServerAgent__notifyClientErrorReported(InspectorBunFrontendDevServerAgent* agent, const BunString* clientErrorPayloadBase64);
void BunFrontendDevServerAgent__notifyGraphUpdate(InspectorBunFrontendDevServerAgent* agent, const BunString* visualizerPayloadBase64);
void BunFrontendDevServerAgent__notifyConsoleLog(InspectorBunFrontendDevServerAgent* agent, int devServerId, char kind, const BunString* data);
}
} // namespace Inspector

View File

@@ -3299,7 +3299,7 @@ pub fn GenericIndex(backing_int: type, uid: anytype) type {
_,
const Index = @This();
comptime {
_ = uid;
_ = uid; // Capture `uid` to ensure a unique type.
}
/// Prefer this over @enumFromInt to assert the int is in range

View File

@@ -31,9 +31,12 @@ class SocketFramer {
$debug("local:", data);
}
socketFramerMessageLengthBuffer.writeUInt32BE(data.length, 0);
// Use this instead of data.length because multi-byte characters
const length = Buffer.byteLength(data, "utf-8");
socketFramerMessageLengthBuffer.writeUInt32BE(length, 0);
socket.$write(socketFramerMessageLengthBuffer);
socket.$write(data);
socket.$write(data, "utf-8");
}
onData(socket: Socket<{ framer: SocketFramer; backend: Writer }>, data: Buffer): void {
@@ -483,6 +486,8 @@ async function connectToUnixServer(
socket.data.framer.onData(socket, bytes);
},
// THIS NEEDS TO BE HERE OTHERWISE IT WILL NEVER WRITE BUFFERED DATA!!
drain: socket => {},
close: socket => {
if (socket.data) {
const { backend, framer } = socket.data;

View File

@@ -15,7 +15,7 @@ async function start() {
const cwd = process.cwd();
let hostname = "localhost";
let port: number | undefined = undefined;
let enableConsoleLog = false;
// Step 1. Resolve all HTML entry points
for (let i = 1, argvLength = argv.length; i < argvLength; i++) {
const arg = argv[i];
@@ -37,6 +37,10 @@ async function start() {
hostname = host;
port = parseInt(portString, 10);
}
} else if (arg === "--console") {
enableConsoleLog = true;
} else if (arg === "--no-console") {
enableConsoleLog = false;
}
if (arg === "--help") {
@@ -50,7 +54,8 @@ Options:
--port=<NUM>
--host=<STR>, --hostname=<STR>
--console # print console logs from browser
--no-console # don't print console logs from browser
Examples:
bun index.html
@@ -58,6 +63,7 @@ Examples:
bun index.html --host=localhost:3000
bun index.html --hostname=localhost:3000
bun ./*.html
bun index.html --console
This is a small wrapper around Bun.serve() that automatically serves the HTML files you pass in without
having to manually call Bun.serve() or write the boilerplate yourself. This runs Bun's bundler on
@@ -215,7 +221,13 @@ yourself with Bun.serve().
try {
server = Bun.serve({
static: staticRoutes,
development: env.NODE_ENV !== "production",
development:
env.NODE_ENV !== "production"
? {
console: enableConsoleLog,
hmr: undefined,
}
: false,
hostname,
port,
@@ -235,7 +247,13 @@ yourself with Bun.serve().
try {
server = Bun.serve({
static: staticRoutes,
development: env.NODE_ENV !== "production",
development:
env.NODE_ENV !== "production"
? {
console: enableConsoleLog,
hmr: undefined,
}
: false,
hostname,

View File

@@ -475,6 +475,43 @@ describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => {
ws.close();
});
test("should notify on consoleLog events", async () => {
await fetch(serverUrl.href).then(r => r.blob());
// Connect a client to trigger connection events
const ws = await createHMRClient();
// Wait for clientConnected event to get the connectionId
const connectedEvent = await session.waitForEvent("BunFrontendDevServer.clientConnected");
// Listen for consoleLog event
const consoleLogPromise = session.waitForEvent("BunFrontendDevServer.consoleLog");
// Send a console log message from the client
// 'l' is the message type for console.log (see ConsoleLogKind enum in DevServer.zig)
ws.send("ll" + "Hello from client test");
// Verify we received the consoleLog event
const consoleLogEvent = await consoleLogPromise;
expect(consoleLogEvent).toHaveProperty("kind");
expect(consoleLogEvent.kind).toBe("l".charCodeAt(0));
expect(consoleLogEvent).toHaveProperty("message");
expect(consoleLogEvent.message).toBe("Hello from client test");
// Test error log
const consoleErrorPromise = session.waitForEvent("BunFrontendDevServer.consoleLog");
ws.send("le" + "Error from client test");
const consoleErrorEvent = await consoleErrorPromise;
expect(consoleErrorEvent).toHaveProperty("kind");
expect(consoleErrorEvent.kind).toBe("e".charCodeAt(0));
expect(consoleErrorEvent).toHaveProperty("message");
expect(consoleErrorEvent.message).toBe("Error from client test");
// Clean up
ws.close();
});
test.todo("should notify on clientErrorReported events", async () => {
// fs.writeFileSync(join(tempdir, "main.ts"), errorReportingScript);