mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 07:58:54 +00:00
Compare commits
10 Commits
ciro/fix-a
...
zack/devse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95866b9428 | ||
|
|
ac666c28a8 | ||
|
|
a1e5fb8c47 | ||
|
|
ed4f93e6a6 | ||
|
|
e2a6545e29 | ||
|
|
0f23dfdef2 | ||
|
|
1a703edaf5 | ||
|
|
1ef077628a | ||
|
|
81b2255066 | ||
|
|
d92659ca03 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
6
packages/bun-types/bun.d.ts
vendored
6
packages/bun-types/bun.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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
629
src/bake/client/inspect.ts
Normal 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(" ")} }`;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user