Compare commits

...

11 Commits

Author SHA1 Message Date
Jarred Sumner
9f48fca461 more 2025-03-23 02:37:10 -07:00
Jarred Sumner
7ca8d301f6 a 2025-03-23 02:06:46 -07:00
Jarred Sumner
557f506549 Update wumbo.zig 2025-03-23 01:16:21 -07:00
Jarred-Sumner
e0653134e1 bun run clang-format 2025-03-23 08:13:43 +00:00
Jarred Sumner
c4f59f96f8 Merge branch 'main' into chloe/hmr13 2025-03-23 01:11:50 -07:00
Jarred-Sumner
6d786ef426 bun run zig-format 2025-03-18 04:56:06 +00:00
chloe caruso
23ab206b59 fix merge 2025-03-17 15:52:13 -07:00
chloe caruso
1d5bdf3a4e fix assertion 2025-03-17 14:53:38 -07:00
chloe caruso
c9e72cd3cd Merge remote-tracking branch 'origin/main' into chloe/hmr13 2025-03-17 14:53:31 -07:00
chloe caruso
812300637c add jsx debugging information 2025-03-17 14:48:39 -07:00
chloe caruso
601efe9ede an experiment 2025-03-11 14:14:07 -07:00
23 changed files with 1003 additions and 111 deletions

View File

@@ -46,7 +46,7 @@
},
// lldb
"lldb.launch.initCommands": ["command source ${workspaceFolder}/.lldbinit"],
// "lldb.launch.initCommands": ["command source ${workspaceFolder}/.lldbinit"],
"lldb.verboseLogging": false,
// C++

View File

@@ -0,0 +1,14 @@
// Experiment
document.getElementById("bun-wumbo")?.remove();
console.log("hello");
globalThis.$send = () => {
import.meta.hot.send("wumbo:data", {
message: "hello",
});
};
import.meta.hot.on("wumbo:meow", data => {
console.log("meow", data);
});

View File

@@ -0,0 +1,36 @@
// Experiment
export default {
name: "wumboplugin",
setup({ unstable_devServer: devServer }) {
devServer.addRoutes({
"/_bun/plugin/wumbo/endpoint1": (req: Request) => {
return new Response("Hello, world!");
},
"/_bun/plugin/wumbo/endpoint2": (req: Request) => {
return new Response("Hello, world!");
},
});
devServer.onEvent("wumbo:data", data => {
// import.meta.hot.send("wumbo:data", data)
devServer.send("wumbo:meow", data); // import.meta.hot.on("wumbo:meow", () => {})
console.log(data);
});
devServer.onEvent("bun:buildError", errors => {
console.error("errs", { errors }); // array of BuildMessage
});
devServer.onEvent("bun:browserError", errors => {
console.error("errs", { errors }); // object of { name: string, message: string, stack: string }
});
devServer.onEvent("bun:successfulBuild", () => {
console.log("successfulBuild");
});
devServer.onEvent("bun:buildStart", () => {
console.log("buildStart"); // buildEnd is either buildError or successfulBuild
});
process.on("uncaughtException", error => {
console.error("uncaughtException", { error });
});
},
};

View File

@@ -86,10 +86,6 @@ bundling_failures: std.ArrayHashMapUnmanaged(
/// When set, nothing is ever bundled for the server-side,
/// and DevSever acts purely as a frontend bundler.
frontend_only: bool,
/// The Plugin API is missing a way to attach filesystem watchers (addWatchFile)
/// This special case makes `bun-plugin-tailwind` work, which is a requirement
/// to ship initial incremental bundling support for HTML files.
has_tailwind_plugin_hack: ?bun.StringArrayHashMapUnmanaged(void) = null,
// These values are handles to the functions in `hmr-runtime-server.ts`.
// For type definitions, see `./bake.private.d.ts`
@@ -190,7 +186,20 @@ has_pre_crash_handler: bool,
///
/// DISABLED in releases, ENABLED in debug.
/// Can be enabled with env var `BUN_ASSUME_PERFECT_INCREMENTAL=1`
assume_perfect_incremental_bundling: bool = false,
assume_perfect_incremental_bundling: bool,
// Hacks
/// The Plugin API is missing `invalidate` callback, which is needed for defer
/// plugins to be able to rebuild the deferred file when non-trivial
/// dependencies change. This special case makes `bun-plugin-tailwind` work,
/// which is a requirement to ship initial incremental HTML bundling support.
has_tailwind_plugin_hack: ?bun.StringArrayHashMapUnmanaged(void),
/// The Plugin API is missing a way to listen for user events
on_event_callback_hack: JSC.Strong,
/// The Plugin API is missing a way to listen for bundling errors.
on_error_callback_hack: JSC.Strong,
pub const internal_prefix = "/_bun";
/// Assets which are routed to the `Assets` storage.
@@ -421,6 +430,10 @@ pub fn init(options: Options) bun.JSOOM!*DevServer {
.relative_path_buf_lock = .unlocked,
.testing_batch_events = .disabled,
.has_tailwind_plugin_hack = null,
.on_error_callback_hack = .empty,
.on_event_callback_hack = .empty,
.server_transpiler = undefined,
.client_transpiler = undefined,
.ssr_transpiler = undefined,
@@ -666,6 +679,8 @@ pub fn deinit(dev: *DevServer) void {
.log = dev.log.deinit(),
.server_fetch_function_callback = dev.server_fetch_function_callback.deinit(),
.server_register_update_callback = dev.server_register_update_callback.deinit(),
.on_error_callback_hack = dev.on_error_callback_hack.deinit(),
.on_event_callback_hack = dev.on_event_callback_hack.deinit(),
.has_pre_crash_handler = if (dev.has_pre_crash_handler)
bun.crash_handler.removePreCrashHandler(dev),
.router = {
@@ -796,6 +811,8 @@ pub fn memoryCost(dev: *DevServer) usize {
.server_register_update_callback = {},
.deferred_request_pool = {},
.assume_perfect_incremental_bundling = {},
.on_error_callback_hack = {},
.on_event_callback_hack = {},
.relative_path_buf = {},
.relative_path_buf_lock = {},
@@ -978,6 +995,7 @@ pub fn setRoutes(dev: *DevServer, server: anytype) !bool {
app.get(asset_prefix ++ "/:asset", *DevServer, dev, wrapGenericRequestHandler(onAssetRequest, is_ssl));
app.get(internal_prefix ++ "/src/*", *DevServer, dev, wrapGenericRequestHandler(onSrcRequest, is_ssl));
app.post(internal_prefix ++ "/report_error", *DevServer, dev, wrapGenericRequestHandler(ErrorReportRequest.run, is_ssl));
app.post(internal_prefix ++ "/init_wumbo", *DevServer, dev, wrapGenericRequestHandler(wumbo.onInitRequest, is_ssl));
app.any(internal_prefix, *DevServer, dev, wrapGenericRequestHandler(onNotFound, is_ssl));
@@ -1765,6 +1783,12 @@ fn startAsyncBundle(
.resolution_failure_entries = .{},
};
dev.next_bundle.requests = .{};
if (dev.on_error_callback_hack.get()) |cb| {
const global = dev.vm.global;
_ = cb.call(global, global.toJSValue(), &.{.true}) catch |err|
global.reportActiveExceptionAsUnhandled(err);
}
}
fn prepareAndLogResolutionFailures(dev: *DevServer) !void {
@@ -1988,6 +2012,10 @@ fn traceAllRouteImports(dev: *DevServer, route_bundle: *RouteBundle, gts: *Graph
try dev.client_graph.traceImports(html.bundled_file, gts, goal);
},
}
for (dev.client_graph.preloads.items) |preload| {
try dev.client_graph.traceImports(preload, gts, goal);
}
}
fn makeArrayForServerComponentsPatch(dev: *DevServer, global: *JSC.JSGlobalObject, items: []const IncrementalGraph(.server).FileIndex) JSValue {
@@ -2061,6 +2089,12 @@ pub fn finalizeBundle(
// not fatal: the assets may be reindexed some time later.
};
if (dev.on_error_callback_hack.get()) |cb| {
const global = dev.vm.global;
_ = cb.call(global, global.toJSValue(), &.{.undefined}) catch |err|
global.reportActiveExceptionAsUnhandled(err);
}
// Signal for testing framework where it is in synchronization
if (dev.testing_batch_events == .enable_after_bundle) {
dev.testing_batch_events = .{ .enabled = .empty };
@@ -2815,6 +2849,57 @@ fn appendOpaqueEntryPoint(
}
}
pub fn addPreload(dev: *DevServer, comptime side: bake.Side, abs_path: []const u8) !IncrementalGraph(side).EmptyInsertion {
const entry = brk: {
dev.graph_safety_lock.lock();
defer dev.graph_safety_lock.unlock();
const g = &switch (side) {
.client => dev.client_graph,
.server => dev.server_graph,
};
const entry = try g.insertEmpty(abs_path, .unknown);
for (g.preloads.items) |index| {
if (index == entry.index) return entry;
}
try g.preloads.append(dev.allocator, entry.index);
break :brk entry;
};
// Invalidate all routes
for (dev.route_bundles.items) |*item| {
item.server_state = .unqueued;
item.invalidateClientBundle();
}
if (dev.current_bundle) |cb| {
// This function is called on the same thread as the
// bundler, meaning this cannot cause a race.
try cb.bv2.enqueueFileFromDevServerIncrementalGraphInvalidation(abs_path, switch (side) {
.client => .browser,
.server => .bun,
});
} else {
var sfb = std.heap.stackFallback(4096, dev.allocator);
const temp_alloc = sfb.get();
var entry_points: EntryPointList = EntryPointList.empty;
defer entry_points.deinit(temp_alloc);
try entry_points.append(temp_alloc, abs_path, switch (side) {
.client => .{ .client = true },
.server => .{ .client = true },
});
dev.startAsyncBundle(
entry_points,
true,
std.time.Timer.start() catch @panic("timers unsupported"),
) catch bun.outOfMemory();
}
return entry;
}
pub fn routeBundlePtr(dev: *DevServer, idx: RouteBundle.Index) *RouteBundle {
return &dev.route_bundles.items[idx.get()];
}
@@ -3066,6 +3151,10 @@ pub fn IncrementalGraph(side: bake.Side) type {
/// so garbage collection can run less often.
edges_free_list: ArrayListUnmanaged(EdgeIndex),
/// Plugins may request their code to be unconditionally bundled and evaluated
/// before user-code. This is a less special-cased version of react refresh.
preloads: ArrayListUnmanaged(FileIndex),
/// Byte length of every file queued for concatenation
current_chunk_len: usize = 0,
/// All part contents
@@ -3092,6 +3181,8 @@ pub fn IncrementalGraph(side: bake.Side) type {
.edges = .empty,
.edges_free_list = .empty,
.preloads = .empty,
.current_chunk_len = 0,
.current_chunk_parts = .empty,
@@ -3375,6 +3466,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
.current_chunk_len = {},
.current_chunk_parts = g.current_chunk_parts.deinit(allocator),
.current_css_files = if (side == .client) g.current_css_files.deinit(allocator),
.preloads = g.preloads.deinit(allocator),
};
}
@@ -4223,11 +4315,13 @@ pub fn IncrementalGraph(side: bake.Side) type {
return file_index;
}
/// Returns the key that was inserted.
pub fn insertEmpty(g: *@This(), abs_path: []const u8, kind: FileKind) bun.OOM!struct {
pub const EmptyInsertion = struct {
index: FileIndex,
key: []const u8,
} {
};
/// Returns the key that was inserted.
pub fn insertEmpty(g: *@This(), abs_path: []const u8, kind: FileKind) bun.OOM!EmptyInsertion {
g.owner().graph_safety_lock.assertLocked();
const dev_allocator = g.owner().allocator;
const gop = try g.bundled_files.getOrPut(dev_allocator, abs_path);
@@ -4403,6 +4497,13 @@ pub fn IncrementalGraph(side: bake.Side) type {
try dev.incremental_result.failures_removed.append(dev.allocator, fail_gop.key_ptr.*);
fail_gop.key_ptr.* = failure;
}
if (dev.on_error_callback_hack.get()) |cb| {
const global = dev.vm.global;
const array = log.toJSArray(global, dev.allocator);
_ = cb.call(global, global.toJSValue(), &.{array}) catch |err|
global.reportActiveExceptionAsUnhandled(err);
}
}
pub fn onFileDeleted(g: *@This(), abs_path: []const u8, bv2: *bun.BundleV2) void {
@@ -4606,32 +4707,52 @@ pub fn IncrementalGraph(side: bake.Side) type {
switch (kind) {
.initial_response => {
if (side == .server) @panic("unreachable");
try w.writeAll("}, {\n main: ");
const initial_response_entry_point = options.initial_response_entry_point;
if (initial_response_entry_point.len > 0) {
try w.writeAll("}, {\n entry: [");
var is_first_entry = true;
const keys = g.bundled_files.keys();
for (g.preloads.items) |preload| {
if (!is_first_entry) {
try w.writeAll(", ");
}
is_first_entry = false;
const file_name = keys[preload.get()];
const rel = g.owner().relativePath(file_name);
defer g.owner().releaseRelativePathBuf();
try bun.js_printer.writeJSONString(
g.owner().relativePath(initial_response_entry_point),
rel,
@TypeOf(w),
w,
.utf8,
);
}
const initial_response_entry_point = options.initial_response_entry_point;
if (initial_response_entry_point.len > 0) {
if (!is_first_entry) try w.writeAll(", ");
const rel = g.owner().relativePath(initial_response_entry_point);
defer g.owner().releaseRelativePathBuf();
try bun.js_printer.writeJSONString(
rel,
@TypeOf(w),
w,
.utf8,
);
g.owner().releaseRelativePathBuf();
} else {
try w.writeAll("null");
}
try w.writeAll(",\n bun: \"" ++ bun.Global.package_json_version_with_canary ++ "\"");
try w.writeAll("],\n bun: \"" ++ bun.Global.package_json_version_with_canary ++ "\"");
try w.writeAll(",\n version: \"");
try w.writeAll(&g.owner().configuration_hash_key);
try w.writeAll("\"");
if (options.react_refresh_entry_point.len > 0) {
try w.writeAll(",\n refresh: ");
const rel = g.owner().relativePath(options.react_refresh_entry_point);
defer g.owner().releaseRelativePathBuf();
try bun.js_printer.writeJSONString(
g.owner().relativePath(options.react_refresh_entry_point),
rel,
@TypeOf(w),
w,
.utf8,
);
g.owner().releaseRelativePathBuf();
}
try w.writeAll("\n");
},
@@ -5829,14 +5950,6 @@ pub const MessageId = enum(u8) {
/// - Remainder are added errors. For Each:
/// - `SerializedFailure`: Error Data
errors = 'e',
/// A message from the browser. This is used to communicate.
/// - `u32`: Unique ID for the browser tab. Each tab gets a different ID
/// - `[n]u8`: Opaque bytes, untouched from `IncomingMessageId.browser_error`
browser_message = 'b',
/// Sent to clear the messages from `browser_error`
/// - For each removed ID:
/// - `u32`: Unique ID for the browser tab.
browser_message_clear = 'B',
/// Sent when a request handler error is emitted. Each route will own at
/// most 1 error, where sending a new request clears the original one.
///
@@ -5873,6 +5986,11 @@ 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',
/// A user provided event was emitted.
/// - `u32`: Length of event name
/// - `[n]u8: Event name in UTF-8 encoded text
/// - Rest of payload: JSON UTF-8 opaque data.
user_event = 'j',
pub inline fn char(id: MessageId) u8 {
return @intFromEnum(id);
@@ -5887,6 +6005,14 @@ pub const IncomingMessageId = enum(u8) {
/// Emitted on client-side navigations.
/// Rest of payload is a UTF-8 string.
set_url = 'n',
/// Subscribe to an event. Payload is event name in UTF-8 text.
user_event_sub = 'a',
/// Unsubscribe from an event. Payload is event name in UTF-8 text.
user_event_unsub = 'r',
/// Emit an event for plugins.
user_event_emit = 'E',
// /// Request an HMR chunk that can satisfy a pending dynamic import.
// dynamic_import = 'd',
/// Tells the DevServer to batch events together.
testing_batch_events = 'H',
@@ -5940,9 +6066,21 @@ const HmrSocket = struct {
/// `hot_update` events for when the route is updated.
active_route: RouteBundle.Index.Optional,
user_id: u32 = 0,
var hmr_socket_id_counter = std.atomic.Value(u32).init(0);
pub fn onOpen(s: *HmrSocket, ws: AnyWebSocket) void {
_ = ws.send(&(.{MessageId.version.char()} ++ s.dev.configuration_hash_key), .binary, false, true);
s.underlying = ws;
if (s.dev.on_event_callback_hack.get()) |cb| {
s.user_id = hmr_socket_id_counter.fetchAdd(1, .monotonic);
s.dev.vm.eventLoop().runCallback(cb, s.dev.vm.global, s.dev.vm.global.toJSValue(), &.{
bun.String.createUTF8ForJS(s.dev.vm.global, "bun:hmr:connect"),
.undefined,
JSValue.jsNumber(s.user_id),
});
}
}
pub fn onMessage(s: *HmrSocket, ws: AnyWebSocket, msg: []const u8, opcode: uws.Opcode) void {
@@ -5953,6 +6091,9 @@ const HmrSocket = struct {
return;
}
var sfa = std.heap.stackFallback(8192, s.dev.allocator);
const temp_alloc = sfa.get();
switch (@as(IncomingMessageId, @enumFromInt(msg[0]))) {
.subscribe => {
var new_bits: HmrTopic.Bits = .{};
@@ -5968,7 +6109,7 @@ const HmrSocket = struct {
}
inline for (comptime std.enums.values(HmrTopic)) |field| {
if (@field(new_bits, @tagName(field)) and !@field(s.subscriptions, @tagName(field))) {
_ = ws.subscribe(&.{@intFromEnum(field)});
_ = ws.subscribe("bun:dev:" ++ [_]u8{@intFromEnum(field)});
// on-subscribe hooks
switch (field) {
@@ -5979,7 +6120,7 @@ const HmrSocket = struct {
else => {},
}
} else if (@field(new_bits, @tagName(field)) and !@field(s.subscriptions, @tagName(field))) {
_ = ws.unsubscribe(&.{@intFromEnum(field)});
_ = ws.unsubscribe("bun:dev:" ++ [_]u8{@intFromEnum(field)});
// on-unsubscribe hooks
switch (field) {
@@ -6004,6 +6145,62 @@ const HmrSocket = struct {
var response: [5]u8 = .{MessageId.set_url_response.char()} ++ std.mem.toBytes(rbi.get());
_ = ws.send(&response, .binary, false, true);
},
.user_event_sub => {
const name = msg[1..];
const name_prefixed = std.fmt.allocPrint(temp_alloc, "bun:dev:user:{s}", .{name}) catch bun.outOfMemory();
defer temp_alloc.free(name_prefixed);
_ = ws.subscribe(name_prefixed);
},
.user_event_unsub => {
const name = msg[1..];
const name_prefixed = std.fmt.allocPrint(temp_alloc, "bun:dev:user:{s}", .{name}) catch bun.outOfMemory();
defer temp_alloc.free(name_prefixed);
_ = ws.unsubscribe(name_prefixed);
},
.user_event_emit => {
const payload = msg[1..];
const name = bun.sliceTo(payload, 0);
if (name.len == payload.len) {
ws.close();
return;
}
const user_data = payload[name.len + 1 ..];
if (user_data.len == 0) {
ws.close();
return;
}
const vm = s.dev.vm;
const globalObject = vm.global;
if (user_data.len > 1 and user_data[0] == 0) {
// Binary message
const binary_data = user_data[1..];
if (s.dev.on_event_callback_hack.get()) |cb| {
vm.eventLoop().runCallback(cb, globalObject, globalObject.toJSValue(), &.{
bun.String.createUTF8ForJS(globalObject, name),
JSC.ArrayBuffer.createBuffer(globalObject, binary_data),
JSValue.jsNumber(s.user_id),
});
}
} else if (s.dev.on_event_callback_hack.get()) |cb| {
// json message
const global = s.dev.vm.global;
const parsed = brk: {
var str = bun.String.createUTF8(user_data);
defer str.deref();
break :brk str.toJSByParseJSON(global) catch {
ws.close();
global.clearException();
return;
};
};
vm.eventLoop().runCallback(cb, globalObject, globalObject.toJSValue(), &.{
bun.String.createUTF8ForJS(globalObject, name),
parsed,
JSValue.jsNumber(s.user_id),
});
}
},
.testing_batch_events => switch (s.dev.testing_batch_events) {
.disabled => {
if (s.dev.current_bundle != null) {
@@ -6046,6 +6243,45 @@ const HmrSocket = struct {
}
}
const DispatchCloseEvent = struct {
callback: JSC.Strong,
globalObject: *JSC.JSGlobalObject,
task: JSC.AnyTask,
id: u32,
const ThisTask = JSC.AnyTask.New(@This(), runFromJS);
fn runFromJS(this: *@This()) void {
const cb = this.callback.swap();
const vm = JSC.VirtualMachine.get();
const global = this.globalObject;
const eventLoop = vm.eventLoop();
defer this.deinit();
eventLoop.runCallback(cb, global, global.toJSValue(), &.{
bun.String.createUTF8ForJS(global, "bun:hmr:disconnect"),
.undefined,
JSValue.jsNumber(this.id),
});
}
pub fn deinit(this: *@This()) void {
this.callback.deinit();
bun.destroy(this);
}
pub fn dispatch(callback: JSC.JSValue, event_loop: *JSC.EventLoop, globalObject: *JSC.JSGlobalObject, id: u32) void {
const this = bun.default_allocator.create(@This()) catch bun.outOfMemory();
this.* = .{
.callback = JSC.Strong.create(callback, globalObject),
.globalObject = globalObject,
.task = ThisTask.init(this),
.id = id,
};
this.task.enqueue(event_loop);
}
};
pub fn onClose(s: *HmrSocket, ws: AnyWebSocket, exit_code: i32, message: []const u8) void {
_ = ws;
_ = exit_code;
@@ -6059,6 +6295,10 @@ const HmrSocket = struct {
s.dev.routeBundlePtr(old).active_viewers -= 1;
}
if (s.dev.on_event_callback_hack.get()) |cb| {
DispatchCloseEvent.dispatch(cb, s.dev.vm.eventLoop(), s.dev.vm.global, s.user_id);
}
bun.debugAssert(s.dev.active_websocket_connections.remove(s));
s.dev.allocator.destroy(s);
}
@@ -6605,11 +6845,11 @@ pub fn onWatchError(_: *DevServer, err: bun.sys.Error) void {
}
pub fn publish(dev: *DevServer, topic: HmrTopic, message: []const u8, opcode: uws.Opcode) void {
if (dev.server) |s| _ = s.publish(&.{@intFromEnum(topic)}, message, opcode, false);
if (dev.server) |s| _ = s.publish("bun:dev:" ++ [_]u8{@intFromEnum(topic)}, message, opcode, false);
}
pub fn numSubscribers(dev: *DevServer, topic: HmrTopic) u32 {
return if (dev.server) |s| s.numSubscribers(&.{@intFromEnum(topic)}) else 0;
return if (dev.server) |s| s.numSubscribers("bun:dev:" ++ [_]u8{@intFromEnum(topic)}) else 0;
}
const SafeFileId = packed struct(u32) {
@@ -6673,7 +6913,7 @@ fn fromOpaqueFileId(comptime side: bake.Side, id: OpaqueFileId) IncrementalGraph
/// Returns posix style path, suitible for URLs and reproducible hashes.
/// To avoid overwriting memory, this has a lock for the buffer.
fn relativePath(dev: *DevServer, path: []const u8) []const u8 {
pub fn relativePath(dev: *DevServer, path: []const u8) []const u8 {
dev.relative_path_buf_lock.lock();
bun.assert(dev.root[dev.root.len - 1] != '/');
@@ -6694,7 +6934,7 @@ fn relativePath(dev: *DevServer, path: []const u8) []const u8 {
return rel;
}
fn releaseRelativePathBuf(dev: *DevServer) void {
pub fn releaseRelativePathBuf(dev: *DevServer) void {
dev.relative_path_buf_lock.unlock();
if (bun.Environment.isDebug) {
dev.relative_path_buf = undefined;
@@ -7309,7 +7549,7 @@ const ErrorReportRequest = struct {
// When before the first generated line, remap to the HMR runtime
const generated_mappings = result.mappings.items(.generated);
if (frame.position.line.oneBased() < generated_mappings[1].lines) {
if (generated_mappings.len <= 1 or frame.position.line.oneBased() < generated_mappings[1].lines) {
frame.source_url = .init(runtime_name); // matches value in source map
frame.position = .invalid;
continue;
@@ -7456,6 +7696,39 @@ const ErrorReportRequest = struct {
try w.writeInt(u8, 0, .little);
}
const dev = ctx.dev;
if (dev.on_error_callback_hack.get()) |cb| {
const vm = dev.vm;
const global = vm.global;
const eventLoop = vm.eventLoop();
eventLoop.runCallback(
cb,
global,
global.toJSValue(),
&.{JSC.JSObject.create(.{
.name = bun.String.createUTF8ForJS(global, name),
.message = bun.String.createUTF8ForJS(global, message),
.stack = stack: {
var stack = JSC.JSValue.createEmptyArray(global, exception.stack.frames_len);
for (exception.stack.frames(), 0..) |frame, i| {
stack.putIndex(
global,
@intCast(i),
JSC.JSObject.create(.{
.function_name = bun.String.createUTF8ForJS(global, frame.function_name.value.ZigString.slice()),
.source_url = bun.String.createUTF8ForJS(global, frame.source_url.value.ZigString.slice()),
.line = JSValue.jsNumber(frame.position.line.oneBased()),
.column = JSValue.jsNumber(frame.position.column.oneBased()),
}, global).toJS(),
);
}
break :stack stack;
},
}, global).toJS()},
);
}
StaticRoute.sendBlobThenDeinit(r, &.fromArrayList(out), .{
.mime_type = &.other,
.server = ctx.dev.server.?,
@@ -7616,6 +7889,7 @@ const bake = bun.bake;
const FrameworkRouter = bake.FrameworkRouter;
const Route = FrameworkRouter.Route;
const OpaqueFileId = FrameworkRouter.OpaqueFileId;
const wumbo = bake.wumbo;
const Log = bun.logger.Log;
const Output = bun.Output;

View File

@@ -7,7 +7,8 @@ type FileIndex = number;
interface Config {
// Server + Client
main: Id;
// Contains all preloads, then the main entry point.
entry: Id[];
// Server
separateSSRGraph?: true;

View File

@@ -2,9 +2,10 @@
//! combines `Bun.build` and `Bun.serve`, providing a hot-reloading development
//! server, server components, and other integrations. Instead of taking the
//! role as a framework, Bake is tool for frameworks to build on top of.
pub const production = @import("./production.zig");
pub const DevServer = @import("./DevServer.zig");
pub const FrameworkRouter = @import("./FrameworkRouter.zig");
pub const DevServer = @import("DevServer.zig");
pub const production = @import("production.zig");
pub const FrameworkRouter = @import("FrameworkRouter.zig");
pub const wumbo = @import("wumbo.zig");
/// export default { app: ... };
pub const api_name = "app";

View File

@@ -667,7 +667,6 @@ declare global {
}
import { BundlerMessageLevel } from "../enums";
import { css } from "../macros" with { type: "macro" };
import {
BundlerMessage,
BundlerMessageLocation,

View File

@@ -36,7 +36,7 @@ let wait =
})
: () => new Promise<void>(done => setTimeout(done, 2_500));
let mainWebSocket: WebSocketWrapper | null = null;
export let mainWebSocket: WebSocketWrapper = null!;
interface WebSocketWrapper {
/** When re-connected, this is re-assigned */
@@ -46,30 +46,30 @@ interface WebSocketWrapper {
[Symbol.dispose](): void;
}
export function getMainWebSocket(): WebSocketWrapper | null {
return mainWebSocket;
}
export function initWebSocket(
handlers: Record<number, (dv: DataView<ArrayBuffer>, ws: WebSocket) => void>,
handlers: Record<number, (dv: any, ws: WebSocket) => void>,
{ url = "/_bun/hmr", onStatusChange }: { url?: string; onStatusChange?: (connected: boolean) => void } = {},
): WebSocketWrapper {
let firstConnection = true;
let closed = false;
let bufferedMessages: any[] = [];
const wsProxy: WebSocketWrapper = {
wrapped: null,
send(data) {
const wrapped = this.wrapped;
if (wrapped && wrapped.readyState === 1) {
wrapped.send(data);
} else {
bufferedMessages.push(data);
}
},
close() {
closed = true;
this.wrapped?.close();
if (mainWebSocket === this) {
mainWebSocket = null;
mainWebSocket = null!;
}
},
[Symbol.dispose]() {
@@ -84,6 +84,10 @@ export function initWebSocket(
function onFirstOpen() {
console.info("[Bun] Hot-module-reloading socket connected, waiting for changes...");
onStatusChange?.(true);
for (const message of bufferedMessages) {
wsProxy.send(message);
}
bufferedMessages = [];
}
function onMessage(ev: MessageEvent<string | ArrayBuffer>) {
@@ -94,6 +98,11 @@ export function initWebSocket(
console.info("[WS] receive message '" + String.fromCharCode(view.getUint8(0)) + "',", new Uint8Array(data));
}
handlers[view.getUint8(0)]?.(view, ws);
} else {
if (IS_BUN_DEVELOPMENT) {
console.info("[WS] receive string message '" + data[0] + "'");
}
handlers[data.charCodeAt(0)]?.(data, ws);
}
}
@@ -124,6 +133,10 @@ export function initWebSocket(
done(true);
onStatusChange?.(true);
ws.onerror = onError;
for (const message of bufferedMessages) {
ws.send(message);
}
bufferedMessages = [];
};
ws.onmessage = onMessage;
ws.onerror = ev => {

View File

@@ -14,6 +14,7 @@ import {
__name,
__using,
} from "../runtime.bun";
import { mainWebSocket } from "./client/websocket";
/** List of loaded modules. Every `Id` gets one HMRModule, mutated across updates. */
let registry = new Map<Id, HMRModule>();
@@ -29,6 +30,7 @@ let refreshRuntime: any;
* in Mozilla Firefox in 2025. Bun lazily evaluates it, so a SyntaxError gets
* thrown upon first usage. */
let lazyDynamicImportWithOptions;
let beforeUnload: null | Promise<void> = null;
const enum State {
Pending,
@@ -221,22 +223,21 @@ export class HMRModule {
if (event.startsWith("vite:")) {
event = "bun:" + event.slice(4);
}
(eventHandlers[event] ??= []).push(cb);
onHotEvent(event, cb);
this.dispose(() => this.off(event, cb));
}
off(event: string, cb: HotEventHandler) {
const handlers = eventHandlers[event];
if (!handlers) return;
const index = handlers.indexOf(cb);
if (index !== -1) {
handlers.splice(index, 1);
}
offHotEvent(event, cb);
}
send(event: string, cb: HotEventHandler) {
throw new Error("TODO: implement ImportMetaHot.send");
send(event: string, data: any) {
if (data && data instanceof ArrayBuffer) {
mainWebSocket.send(encodeEvent(event, data));
} else {
const encodedData = JSON.stringify(data);
mainWebSocket.send("E" + event + "\0" + encodedData);
}
}
declare indirectHot: any;
@@ -258,6 +259,30 @@ HMRModule.prototype.indirectHot = new Proxy({}, {
},
});
export function onHotEvent(event: string, cb: HotEventHandler) {
const handlers = (eventHandlers[event] ??= []);
handlers.push(cb);
if (!event.startsWith("bun:")) {
if (handlers.length === 1) {
mainWebSocket.send("a" + event);
}
}
}
export function offHotEvent(event: string, cb: HotEventHandler) {
const handlers = eventHandlers[event];
if (!handlers) return;
const index = handlers.indexOf(cb);
if (index !== -1) {
handlers.splice(index, 1);
if (!event.startsWith("bun:")) {
if (handlers.length === 0) {
mainWebSocket.send("r" + event);
}
}
}
}
// TODO: This function is currently recursive.
export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModule | null): HMRModule {
// First, try and re-use an existing module.
@@ -436,7 +461,7 @@ export function loadModuleAsync<IsUserDynamic extends boolean>(
DEBUG.ASSERT(
isAsync //
? list.some(x => x instanceof Promise)
: list.every(x => x instanceof HMRModule)
: list.every(x => x instanceof HMRModule),
);
// Running finishLoadModuleAsync synchronously when there are no promises is
@@ -517,7 +542,7 @@ function parseEsmDependencies<T extends GenericModuleLoader<any>>(
DEBUG.ASSERT(typeof key === "string");
// TODO: there is a bug in the way exports are verified. Additionally a
// possible performance issue. For the meantime, this is disabled since
// it was not shipped in the initial 1.2.3 HMR, and real issues will
// it was not shipped in the initial 1.2.3 HMR, and real issues will
// just throw 'undefined is not a function' or so on.
// if (!availableExportKeys.includes(key)) {
@@ -533,7 +558,7 @@ function parseEsmDependencies<T extends GenericModuleLoader<any>>(
i = expectedExportKeyEnd;
if (IS_BUN_DEVELOPMENT) {
DEBUG.ASSERT(list[list.length - 1] as any instanceof HMRModule);
DEBUG.ASSERT((list[list.length - 1] as any) instanceof HMRModule);
}
}
}
@@ -579,7 +604,7 @@ type HotEventHandler = (data: any) => void;
// If updating this, make sure the `devserver.d.ts` types are
// kept in sync.
type HMREvent =
type HMREvent =
| "bun:ready"
| "bun:beforeUpdate"
| "bun:afterUpdate"
@@ -663,6 +688,12 @@ export async function replaceModules(modules: Record<Id, UnloadedModule>) {
// If roots were hit, print a nice message before reloading.
if (failures) {
// A reload is about to happen, but if that reload doesn't ever happen
// (cancel), Bun should propagate this HMR error
if (beforeUnload) {
await beforeUnload;
}
let message =
"[Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:\n\n";
@@ -670,9 +701,8 @@ export async function replaceModules(modules: Record<Id, UnloadedModule>) {
for (const boundary of failures) {
const path: Id[] = [];
let current = registry.get(boundary)!;
DEBUG.ASSERT(!boundary.endsWith(".html")); // caller should have already reloaded
DEBUG.ASSERT(current);
DEBUG.ASSERT(current.selfAccept === null);
DEBUG.ASSERT(current);
DEBUG.ASSERT(current.selfAccept === null);
if (current.importers.size === 0) {
message += `Module "${boundary}" is a root module that does not self-accept.\n`;
continue;
@@ -796,7 +826,7 @@ function createAcceptArray(modules: string[], key: Id) {
return arr;
}
export function emitEvent(event: HMREvent, data: any) {
export function emitEvent(event: HMREvent | string, data: any) {
const handlers = eventHandlers[event];
if (!handlers) return;
for (const handler of handlers) {
@@ -940,14 +970,43 @@ if (side === "client") {
registerSynthetic("bun:bake/client", {
onServerSideReload: cb => (onServerSideReload = cb),
});
window.addEventListener("navigate", ev => {
beforeUnload = new Promise(resolve => {
function done() {
window.removeEventListener("navigatesuccess", done);
window.removeEventListener("navigateerror", done);
resolve();
}
window.addEventListener("navigatesuccess", done);
window.addEventListener("navigateerror", done);
});
});
}
// The following API may be altered at any point.
// Thankfully, you can just call `import.meta.hot.on`
let testingHook = globalThis['bun do not use this outside of internal testing or else i\'ll cry'];
let testingHook = globalThis["bun do not use this outside of internal testing or else i'll cry"];
testingHook?.({
onEvent(event: HMREvent, cb) {
eventHandlers[event] ??= [];
eventHandlers[event]!.push(cb);
},
});
});
function encodeEvent(event: string, data: ArrayBuffer) {
const bytes = new Uint8Array(data.byteLength + 3 + event.length);
let j = 0;
bytes[j++] = "E".charCodeAt(0);
for (let i = 0, eventNameLength = event.length; i < eventNameLength; i++) {
bytes[j++] = event.charCodeAt(i);
}
// End of event name
bytes[j++] = 0;
// Binary message
bytes[j++] = 0;
bytes.set(new Uint8Array(data), j);
return bytes.buffer;
}

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,
@@ -8,14 +8,77 @@ import {
setRefreshRuntime,
emitEvent,
fullReload,
onHotEvent,
offHotEvent,
} from "./hmr-module";
import { hasFatalError, onServerErrorPayload, onRuntimeError } from "./client/overlay";
import { DataViewReader } from "./client/data-view";
import { initWebSocket } from "./client/websocket";
import { initWebSocket, mainWebSocket } from "./client/websocket";
import { MessageId } from "./generated";
import { editCssContent, editCssArray } from "./client/css-reloader";
import { td } from "./shared";
let wumbo: HTMLButtonElement | null = null;
switch (globalThis?.localStorage?.getItem?.("bun:wumbo")) {
case "true": {
const url = await (await fetch("/_bun/init_wumbo", { method: "POST" })).text();
if (!unloadedModuleRegistry[url]) {
const load = Promise.withResolvers();
onHotEvent("bun:afterUpdate", function handler() {
if (unloadedModuleRegistry[url]) {
offHotEvent("bun:afterUpdate", handler);
load.resolve();
}
});
await load.promise;
}
loadModuleAsync(url, false, null);
break;
}
default: {
// TODO: move this into overlay.ts
wumbo = document.createElement("button");
wumbo.id = "bun-wumbo";
wumbo.setAttribute(
"style",
`
position: fixed !important;
bottom: 16px !important;
left: 16px !important;
width: 32px !important;
height: 32px !important;
border-radius: 50% !important;
background: #ff4444 !important;
border: none !important;
cursor: pointer !important;
z-index: 2147483646 !important;
display: none !important;
`,
);
wumbo.addEventListener("click", async () => {
if (globalThis.localStorage) {
globalThis.localStorage?.setItem?.("bun:wumbo", "true");
}
wumbo!.remove();
const url = await (await fetch("/_bun/init_wumbo", { method: "POST" })).text();
if (!unloadedModuleRegistry[url]) {
const load = Promise.withResolvers();
onHotEvent("bun:afterUpdate", function handler() {
if (unloadedModuleRegistry[url]) {
offHotEvent("bun:afterUpdate", handler);
load.resolve();
}
});
await load.promise;
}
loadModuleAsync(url, false, null);
});
document.body.appendChild(wumbo);
break;
}
}
if (typeof IS_BUN_DEVELOPMENT !== "boolean") {
throw new Error("DCE is configured incorrectly");
}
@@ -72,8 +135,8 @@ const handlers = {
return;
}
ws.send("she"); // IncomingMessageId.subscribe with hot_update and errors
ws.send("n" + location.pathname); // IncomingMessageId.set_url
mainWebSocket.send("she"); // IncomingMessageId.subscribe with hot_update and errors
mainWebSocket.send("n" + location.pathname); // IncomingMessageId.set_url
},
[MessageId.hot_update](view) {
const reader = new DataViewReader(view, 1);
@@ -162,13 +225,25 @@ const handlers = {
emitEvent("bun:afterUpdate", null);
}
},
[MessageId.user_event](view: string) {
const nullTerminator = view.indexOf("\0");
if (nullTerminator === -1) {
if (IS_BUN_DEVELOPMENT) {
console.error("Invalid user event", view);
}
return;
}
const event = view.slice(1, nullTerminator);
const data = view.slice(nullTerminator + 1);
emitEvent(event, JSON.parse(data));
},
[MessageId.set_url_response](view) {
const reader = new DataViewReader(view, 1);
currentRouteIndex = reader.u32();
},
[MessageId.errors]: onServerErrorPayload,
};
const ws = initWebSocket(handlers, {
initWebSocket(handlers, {
onStatusChange(connected) {
emitEvent(connected ? "bun:ws:connect" : "bun:ws:disconnect", null);
},
@@ -179,7 +254,7 @@ const ws = initWebSocket(handlers, {
const truePushState = History.prototype.pushState;
History.prototype.pushState = function pushState(this: History, state: any, title: string, url?: string | null) {
truePushState.call(this, state, title, url);
ws.send("n" + location.pathname);
mainWebSocket.send("n" + location.pathname);
};
const trueReplaceState = History.prototype.replaceState;
History.prototype.replaceState = function replaceState(
@@ -189,7 +264,7 @@ const ws = initWebSocket(handlers, {
url?: string | null,
) {
trueReplaceState.call(this, state, title, url);
ws.send("n" + location.pathname);
mainWebSocket.send("n" + location.pathname);
};
}
@@ -220,7 +295,13 @@ try {
setRefreshRuntime(refreshRuntime);
}
await loadModuleAsync(config.main, false, null);
for (const entry of config.entry) {
await loadModuleAsync(entry, false, null);
}
if (wumbo) {
wumbo.style.display = "block";
}
emitEvent("bun:ready", null);
} catch (e) {

225
src/bake/wumbo.zig Normal file
View File

@@ -0,0 +1,225 @@
const Init = struct {
dev: *DevServer,
aborted: bool,
resp: AnyResponse,
fn onAbort(this: *@This(), resp: AnyResponse) void {
this.aborted = true;
_ = resp;
}
};
pub fn onInitRequest(dev: *DevServer, req: *Request, resp: AnyResponse) void {
onInit(dev, req, resp) catch |err| switch (err) {
error.OutOfMemory => bun.outOfMemory(),
error.JSError => |e| {
bun.handleErrorReturnTrace(err, @errorReturnTrace());
const err_value = dev.vm.global.takeException(e);
dev.vm.printErrorLikeObjectToConsole(err_value.toError() orelse err_value);
resp.corked(onHttp500, .{resp});
},
};
}
fn onInit(dev: *DevServer, _: *Request, resp: AnyResponse) bun.JSOOM!void {
// TODO: auto install code here
const entry_point_string = bun.String.createUTF8(
brk: {
if (bun.getenvZ("BUN_WUMBO_PLUGIN")) |plugin| {
break :brk plugin;
}
break :brk bun.Environment.base_path ++ "/packages/bun-wumbo/plugin.ts";
},
);
defer entry_point_string.deref();
const promise = JSBundlerPlugin__loadAndResolveEditPlugin(
dev.vm.global,
dev.server.?.jsValue() orelse brk: {
bun.debugAssert(false);
break :brk .undefined;
},
JSC.JSValue.fromPtr(dev),
&entry_point_string,
JSC.JSFunction.create(dev.vm.global, "", addRoutes, 0, .{}),
JSC.JSFunction.create(dev.vm.global, "", addCallbacks, 0, .{}),
);
if (dev.vm.global.hasException()) {
return error.JSError;
}
dev.server.?.onPendingRequest();
const init = bun.create(dev.allocator, Init, .{
.dev = dev,
.resp = resp,
.aborted = false,
});
resp.onAborted(*Init, Init.onAbort, init);
promise.setHandled(dev.vm.jsc);
promise.asValue(dev.vm.global).then(
dev.vm.global,
init,
onInitSetupResolve,
onInitSetupReject,
);
}
extern fn JSBundlerPlugin__loadAndResolveEditPlugin(
global: *JSC.JSGlobalObject,
server: JSC.JSValue,
dev_server: JSC.JSValue,
path: *const bun.String,
fn_1: JSC.JSValue,
fn_2: JSC.JSValue,
) *JSC.JSPromise;
pub fn onInitSetupResolve(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
_, const js_promise = callframe.argumentsAsArray(2);
const ctx = js_promise.asPtr(Init);
defer ctx.dev.allocator.destroy(ctx);
const dev = ctx.dev;
const entry = dev.addPreload(.client, brk: {
if (bun.getenvZ("BUN_WUMBO_FRONTEND")) |frontend| {
break :brk frontend;
}
break :brk bun.Environment.base_path ++ "/packages/bun-wumbo/frontend.ts";
}) catch bun.outOfMemory();
if (!ctx.aborted) {
ctx.resp.clearAborted();
ctx.resp.corked(onHttp200, .{ ctx.resp, dev.relativePath(entry.key) });
dev.releaseRelativePathBuf();
}
dev.server.?.onStaticRequestComplete();
return .undefined;
}
pub fn onInitSetupReject(_: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const err, const js_promise = callframe.argumentsAsArray(2);
const ctx = js_promise.asPtr(Init);
defer ctx.dev.allocator.destroy(ctx);
const dev = ctx.dev;
dev.vm.printErrorLikeObjectToConsole(err.toError() orelse err);
ctx.resp.corked(onHttp500, .{ctx.resp});
dev.server.?.onStaticRequestComplete();
return .undefined;
}
fn onHttp500(resp: AnyResponse) void {
resp.writeStatus("500 Internal Server Error");
resp.end("Internal Server Error", false);
}
fn onHttp200(resp: AnyResponse, str: []const u8) void {
resp.writeStatus("200 OK");
resp.end(str, false);
}
fn addRoutes(global: *JSC.JSGlobalObject, call_frame: *JSC.CallFrame) bun.JSOOM!JSC.JSValue {
const dev_encoded, const routes_value = call_frame.argumentsAsArray(2);
const dev = dev_encoded.asPtr(DevServer);
bun.assert(dev.server.?.devServer() == dev); // sanity
const routes = routes_value.getObject() orelse return global.throwInvalidArguments("Routes must be an object of functions", .{});
const any_server = dev.server.?;
const Ptr = JSC.API.AnyServer.Ptr;
switch (any_server.ptr.tag()) {
else => @panic("unexpected tag"),
inline Ptr.case(JSC.API.HTTPServer),
Ptr.case(JSC.API.HTTPSServer),
Ptr.case(JSC.API.DebugHTTPServer),
Ptr.case(JSC.API.DebugHTTPSServer),
=> |tag| {
var iter = try JSC.JSPropertyIterator(.{
.skip_empty_name = true,
.include_value = true,
}).init(global, routes);
defer iter.deinit();
const server = switch (tag) {
Ptr.case(JSC.API.HTTPServer) => any_server.ptr.as(JSC.API.HTTPServer),
Ptr.case(JSC.API.HTTPSServer) => any_server.ptr.as(JSC.API.HTTPSServer),
Ptr.case(JSC.API.DebugHTTPServer) => any_server.ptr.as(JSC.API.DebugHTTPServer),
Ptr.case(JSC.API.DebugHTTPSServer) => any_server.ptr.as(JSC.API.DebugHTTPSServer),
else => @compileError(unreachable),
};
const start_len = server.plugin_routes.items.len;
errdefer {
for (server.plugin_routes.items[start_len..]) |*route| {
route.cb.deinit();
bun.default_allocator.free(route.path);
}
server.plugin_routes.items.len = start_len;
_ = server.reloadStaticRoutes() catch bun.outOfMemory();
}
while (try iter.next()) |key| {
const path, const is_ascii = key.toOwnedSliceReturningAllASCII(bun.default_allocator) catch bun.outOfMemory();
errdefer bun.default_allocator.free(path);
const value: JSC.JSValue = iter.value;
if (value.isUndefined()) {
continue;
}
if (path.len == 0 or (path[0] != '/')) {
return global.throwInvalidArguments("Invalid route {}. Path must start with '/'", .{bun.fmt.quote(path)});
}
if (!is_ascii) {
return global.throwInvalidArguments("Invalid route {}. Please encode all non-ASCII characters in the path.", .{bun.fmt.quote(path)});
}
if (!value.isCallable()) {
return global.throwInvalidArguments("Invalid route {}. Must be a function.", .{bun.fmt.quote(path)});
}
try server.plugin_routes.append(bun.default_allocator, .{
.cb = .create(value, global),
.path = path,
.server = server,
});
}
_ = try server.reloadStaticRoutes();
},
}
return .undefined;
}
fn addCallbacks(global: *JSC.JSGlobalObject, call_frame: *JSC.CallFrame) bun.JSOOM!JSC.JSValue {
const dev_encoded, const err_callback, const event_callback = call_frame.argumentsAsArray(3);
const dev = dev_encoded.asPtr(DevServer);
bun.assert(dev.server.?.devServer() == dev); // sanity
dev.on_event_callback_hack.set(global, event_callback);
dev.on_error_callback_hack.set(global, err_callback);
return .undefined;
}
const bun = @import("root").bun;
const DevServer = bun.bake.DevServer;
const uws = bun.uws;
const Request = uws.Request;
const AnyResponse = bun.uws.AnyResponse;
const JSC = bun.JSC;
comptime {
@export(
&JSC.toJSHostFunction(onInitSetupResolve),
.{ .name = "DevServer__onInitSetupResolve" },
);
@export(
&JSC.toJSHostFunction(onInitSetupReject),
.{ .name = "DevServer__onInitSetupReject" },
);
}

View File

@@ -600,10 +600,6 @@ pub const JSBundler = struct {
import_record_index: u32 = 0,
range: logger.Range = logger.Range.None,
original_target: Target,
// pub inline fn loader(_: *const MiniImportRecord) ?options.Loader {
// return null;
// }
};
pub fn init(bv2: *bun.BundleV2, record: MiniImportRecord) Resolve {
@@ -860,7 +856,6 @@ pub const JSBundler = struct {
},
error.JSError => {},
}
@panic("Unexpected: source_code is not a string");
};
this.value = .{

View File

@@ -7205,6 +7205,8 @@ const ServePlugins = struct {
/// Promise may be empty if the plugin load finishes synchronously.
plugin: *bun.JSC.API.JSBundler.Plugin,
promise: JSC.JSPromise.Strong,
// In practice, only one of these two will be populated.
html_bundle_routes: std.ArrayListUnmanaged(*HTMLBundle.Route),
dev_server: ?*bun.bake.DevServer,
},
@@ -7242,7 +7244,7 @@ const ServePlugins = struct {
pub fn getOrStartLoad(this: *ServePlugins, global: *JSC.JSGlobalObject, cb: Callback) bun.OOM!GetOrStartLoadResult {
sw: switch (this.state) {
.unqueued => {
this.loadAndResolvePlugins(global);
this.loadAndResolvePlugins(global, cb == .dev_server);
continue :sw this.state; // could jump to any branch if synchronously resolved
},
.pending => |*pending| {
@@ -7267,9 +7269,10 @@ const ServePlugins = struct {
plugin: *bun.JSC.API.JSBundler.Plugin,
plugins: JSC.JSValue,
bunfig_folder: JSC.JSValue,
has_hmr: bool,
) JSValue;
fn loadAndResolvePlugins(this: *ServePlugins, global: *JSC.JSGlobalObject) void {
fn loadAndResolvePlugins(this: *ServePlugins, global: *JSC.JSGlobalObject, has_hmr: bool) void {
bun.assert(this.state == .unqueued);
const plugin_list = this.state.unqueued;
const bunfig_folder = bun.path.dirname(global.bunVM().transpiler.options.bunfig_path, .auto);
@@ -7296,7 +7299,7 @@ const ServePlugins = struct {
} };
global.bunVM().eventLoop().enter();
const result = JSBundlerPlugin__loadAndResolvePluginsForServe(plugin, plugin_js_array, bunfig_folder_bunstr);
const result = JSBundlerPlugin__loadAndResolvePluginsForServe(plugin, plugin_js_array, bunfig_folder_bunstr, has_hmr);
global.bunVM().eventLoop().exit();
// handle the case where js synchronously throws an error
@@ -7457,6 +7460,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
} = .{},
plugins: ?*ServePlugins = null,
plugin_routes: std.ArrayListUnmanaged(PluginRoute) = .empty,
dev_server: ?*bun.bake.DevServer,
@@ -7484,6 +7488,8 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
}
};
const PluginRoute = struct { cb: JSC.Strong, path: []const u8, server: *ThisServer };
/// Returns:
/// - .ready if no plugin has to be loaded
/// - .err if there is a cached failure. Currently, this requires restarting the entire server.
@@ -7580,7 +7586,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
}
pub fn publish(this: *ThisServer, globalThis: *JSC.JSGlobalObject, topic: ZigString, message_value: JSValue, compress_value: ?JSValue) bun.JSError!JSValue {
if (this.config.websocket == null)
if (this.config.websocket == null and (this.dev_server == null or this.dev_server.?.active_websocket_connections.size == 0))
return JSValue.jsNumber(0);
const app = this.app.?;
@@ -8922,6 +8928,22 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
this.handleRequest(&should_deinit_context, prepared, req, response_value);
}
pub fn onPluginRouteRequest(plugin_route: *PluginRoute, req: *uws.Request, resp: *App.Response) void {
var should_deinit_context = false;
const this = plugin_route.server;
const prepared = this.prepareJsRequestContext(req, resp, &should_deinit_context, true) orelse return;
const js_value = this.jsValueAssertAlive();
const response_value = plugin_route.cb.get().?.call(
this.globalThis,
js_value,
&.{ prepared.js_request, js_value },
) catch |err|
this.globalThis.takeException(err);
this.handleRequest(&should_deinit_context, prepared, req, response_value);
}
pub fn onRequestFromSaved(
this: *ThisServer,
req: SavedRequest.Union,
@@ -9206,7 +9228,6 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
fn setRoutes(this: *ThisServer) JSC.JSValue {
var route_list_value = JSC.JSValue.zero;
// TODO: move devserver and plugin logic away
const app = this.app.?;
const any_server = AnyServer.from(this);
const dev_server = this.dev_server;
@@ -9324,6 +9345,10 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
}
}
for (this.plugin_routes.items) |*plugin_route| {
app.any(plugin_route.path, *PluginRoute, plugin_route, onPluginRouteRequest);
}
// If there are plugins, initialize the ServePlugins object in
// an unqueued state. The first thing (HTML Bundle, DevServer)
// that needs plugins will cause the load to happen.
@@ -9629,7 +9654,7 @@ pub const DebugHTTPSServer = NewServer(JSC.Codegen.JSDebugHTTPSServer, true, tru
pub const AnyServer = struct {
ptr: Ptr,
const Ptr = bun.TaggedPointerUnion(.{
pub const Ptr = bun.TaggedPointerUnion(.{
HTTPServer,
HTTPSServer,
DebugHTTPServer,
@@ -9840,6 +9865,16 @@ pub const AnyServer = struct {
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
pub fn jsValue(this: AnyServer) ?JSC.JSValue {
return switch (this.ptr.tag()) {
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).js_value.get(),
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).js_value.get(),
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).js_value.get(),
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).js_value.get(),
else => bun.unreachablePanic("Invalid pointer tag", .{}),
};
}
};
const welcome_page_html_gz = @embedFile("welcome-page.html.gz");

View File

@@ -593,7 +593,7 @@ extern "C" Bun::JSBundlerPlugin* JSBundlerPlugin__create(Zig::GlobalObject* glob
target);
}
extern "C" JSC::EncodedJSValue JSBundlerPlugin__loadAndResolvePluginsForServe(Bun::JSBundlerPlugin* plugin, JSC::EncodedJSValue encodedPlugins, JSC::EncodedJSValue encodedBunfigFolder)
extern "C" JSC::EncodedJSValue JSBundlerPlugin__loadAndResolvePluginsForServe(Bun::JSBundlerPlugin* plugin, JSC::EncodedJSValue encodedPlugins, JSC::EncodedJSValue encodedBunfigFolder, bool hasHmr)
{
auto& vm = plugin->vm();
auto scope = DECLARE_CATCH_SCOPE(vm);
@@ -610,10 +610,38 @@ extern "C" JSC::EncodedJSValue JSBundlerPlugin__loadAndResolvePluginsForServe(Bu
arguments.append(JSValue::decode(encodedPlugins));
arguments.append(JSValue::decode(encodedBunfigFolder));
arguments.append(runSetupFn);
arguments.append(JSC::jsBoolean(hasHmr));
return JSC::JSValue::encode(JSC::profiledCall(plugin->globalObject(), ProfilingReason::API, loadAndResolvePluginsForServeBuiltinFn, callData, plugin, arguments));
}
extern "C" JSC::EncodedJSValue JSBundlerPlugin__loadAndResolveEditPlugin(
JSC::JSGlobalObject* global,
JSC::EncodedJSValue server,
JSC::EncodedJSValue devServer,
const BunString* path,
JSC::EncodedJSValue fn1,
JSC::EncodedJSValue fn2)
{
auto& vm = global->vm();
auto scope = DECLARE_CATCH_SCOPE(vm);
auto* builtinFn = JSC::JSFunction::create(vm, global, WebCore::bundlerPluginLoadEditPluginCodeGenerator(vm), global);
JSC::CallData callData = JSC::getCallData(builtinFn);
if (UNLIKELY(callData.type == JSC::CallData::Type::None))
return JSValue::encode(jsUndefined());
MarkedArgumentBuffer arguments;
arguments.append(JSC::jsString(vm, path->toWTFString()));
arguments.append(JSValue::decode(server));
arguments.append(JSValue::decode(devServer));
arguments.append(JSValue::decode(fn1));
arguments.append(JSValue::decode(fn2));
return JSC::JSValue::encode(JSC::profiledCall(global, ProfilingReason::API, builtinFn, callData, jsNull(), arguments));
}
extern "C" JSC::EncodedJSValue JSBundlerPlugin__runSetupFunction(
Bun::JSBundlerPlugin* plugin,
JSC::EncodedJSValue encodedSetupFunction,

View File

@@ -888,9 +888,7 @@ pub const JSValue = enum(i64) {
return JSC__JSValue__createObject2(global, key1, key2, value1, value2);
}
pub fn asPromisePtr(this: JSValue, comptime T: type) *T {
return asPtr(this, T);
}
pub const asPromisePtr = asPtr;
extern fn JSC__JSValue__createRopeString(this: JSValue, rhs: JSValue, globalThis: *JSC.JSGlobalObject) JSValue;
pub fn createRopeString(this: JSValue, rhs: JSValue, globalThis: *JSC.JSGlobalObject) JSValue {

View File

@@ -4635,6 +4635,10 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h
return GlobalObject::PromiseFunctions::Bun__FileStreamWrapper__onResolveRequestStream;
} else if (handler == Bun__FileStreamWrapper__onRejectRequestStream) {
return GlobalObject::PromiseFunctions::Bun__FileStreamWrapper__onRejectRequestStream;
} else if (handler == DevServer__onInitSetupResolve) {
return GlobalObject::PromiseFunctions::DevServer__onInitSetupResolve;
} else if (handler == DevServer__onInitSetupReject) {
return GlobalObject::PromiseFunctions::DevServer__onInitSetupReject;
} else {
RELEASE_ASSERT_NOT_REACHED();
}

View File

@@ -354,6 +354,8 @@ public:
Bun__S3UploadStream__onResolveRequestStream,
Bun__FileStreamWrapper__onRejectRequestStream,
Bun__FileStreamWrapper__onResolveRequestStream,
DevServer__onInitSetupResolve,
DevServer__onInitSetupReject,
};
static constexpr size_t promiseFunctionsSize = 34;

View File

@@ -886,5 +886,7 @@ BUN_DECLARE_HOST_FUNCTION(Bun__S3UploadStream__onRejectRequestStream);
BUN_DECLARE_HOST_FUNCTION(Bun__FileStreamWrapper__onResolveRequestStream);
BUN_DECLARE_HOST_FUNCTION(Bun__FileStreamWrapper__onRejectRequestStream);
BUN_DECLARE_HOST_FUNCTION(DevServer__onInitSetupResolve);
BUN_DECLARE_HOST_FUNCTION(DevServer__onInitSetupReject);
#endif

View File

@@ -161,6 +161,10 @@ pub const AnyTask = struct {
ctx: ?*anyopaque,
callback: *const (fn (*anyopaque) void),
pub fn enqueue(this: *@This(), event_loop: *JSC.EventLoop) void {
event_loop.enqueueTask(Task.init(this));
}
pub fn task(this: *AnyTask) Task {
return Task.init(this);
}

View File

@@ -953,8 +953,8 @@ export function cAbiTypeName(type: CAbiType) {
{
"*anyopaque": "void*",
"*JSGlobalObject": "JSC::JSGlobalObject*",
"JSValue": "JSValue",
"JSValue.MaybeException": "JSValue",
"JSValue": "JSC::EncodedJSValue",
"JSValue.MaybeException": "JSC::EncodedJSValue",
"bool": "bool",
"u8": "uint8_t",
"u16": "uint16_t",

View File

@@ -142,10 +142,16 @@ function sliceTemplateLiteralSourceCode(contents: string, replace: boolean) {
contents = contents.slice(1);
break;
} else if (contents.startsWith("$")) {
const { result: result2, rest } = sliceSourceCode(contents.slice(1), replace);
result += "$" + result2;
contents = rest;
continue;
try {
const { result: result2, rest } = sliceSourceCode(contents.slice(1), replace);
result += "$" + result2;
contents = rest;
continue;
} catch (e) {
console.warn("in template literal", contents.slice(0, 100));
console.error(e);
throw e;
}
} else {
throw new Error("TODO");
}

View File

@@ -1,4 +1,12 @@
import type { BuildConfig, BunPlugin, OnLoadCallback, OnResolveCallback, PluginBuilder, PluginConstraints } from "bun";
import type {
BuildConfig,
BunPlugin,
OnLoadCallback,
OnResolveCallback,
PluginBuilder,
PluginConstraints,
Server,
} from "bun";
type AnyFunction = (...args: any[]) => any;
/**
@@ -42,7 +50,78 @@ interface PluginBuilderExt extends PluginBuilder {
esbuild: any;
}
type BeforeOnParseExternal = unknown;
/**
* Used in wumbo.zig + DevServer to load and resolve plugins.
* TODO: if this experiment is successful, we can very easily move these
* functions all to the actual plugin api. for now, we special case things.
*/
export function loadEditPlugin(path, server, devServer, setRoutes, setCallbacks) {
let promiseResult = (async (
path: string,
server: Server,
devServer: any,
setRoutes: (server: any, routes: Record<string, any>) => void,
setCallbacks: (server: any, eventEmit: Function, error: Function) => void,
) => {
let pluginModuleRaw = await import(path);
if (!pluginModuleRaw.default)
throw new TypeError(`Expected "${path}" to be a module which default exports a plugin.`);
const setup = pluginModuleRaw.default.setup;
if (!$isCallable(setup)) throw new TypeError(`Expected "${path}" to be a module which exports a "setup" function.`);
const ee = new (require("node:events"))();
let pendingErrors: any[] | undefined = undefined;
await setup({
unstable_devServer: {
server,
addRoutes: (routes: Record<string, any>) => void setRoutes(devServer, routes),
onEvent: (tag, data) => void ee.on(tag, data),
send: (tag, data) => {
server.publish("bun:dev:user:" + tag, "j" + tag + "\0" + JSON.stringify(data));
},
},
});
setCallbacks(
devServer,
// this function is absurdly overloaded for no good reason other than to ship the demo
// ----
// array -> build errors
// undefined -> end of build
// single object -> browser error
// value true -> build start
errorsOrUndefined => {
if (errorsOrUndefined != undefined) {
if (Array.isArray(pendingErrors)) {
pendingErrors.push(...errorsOrUndefined);
} else if (pendingErrors) {
if (typeof pendingErrors === "object") {
ee.emit("bun:browserError", pendingErrors);
} else if (pendingErrors === true) {
ee.emit("bun:buildStart");
} else {
$assert(false);
}
} else {
pendingErrors = errorsOrUndefined;
}
} else {
if (pendingErrors) {
ee.emit("bun:buildError", pendingErrors);
pendingErrors = undefined;
} else {
ee.emit("bun:successfulBuild", []);
}
}
},
(tag, data) => void ee.emit(tag, data),
);
})(path, server, devServer, setRoutes, setCallbacks);
return promiseResult;
}
/**
* Used by Bun.serve() to resolve and load plugins.
@@ -52,14 +131,16 @@ export function loadAndResolvePluginsForServe(
plugins: string[],
bunfig_folder: string,
runSetupFn: typeof runSetupFunction,
hasHmr: boolean,
) {
// Same config as created in HTMLBundle.init
let config: BuildConfigExt = {
let config = {
experimentalCss: true,
experimentalHtml: true,
target: "browser",
root: bunfig_folder,
};
hot: hasHmr,
} as any as BuildConfigExt;
class InvalidBundlerPluginError extends TypeError {
pluginName: string;
@@ -137,7 +218,7 @@ export function runSetupFunction(
if (map === onBeforeParsePlugins) {
isOnBeforeParse = true;
// TODO: how to check if it a napi module here?
if (!callback || !$isObject(callback) || !callback.$napiDlopenHandle) {
if (!callback || !$isObject(callback) || !(callback as any).$napiDlopenHandle) {
throw new TypeError(
"onBeforeParse `napiModule` must be a Napi module which exports the `BUN_PLUGIN_NAME` symbol.",
);
@@ -148,7 +229,7 @@ export function runSetupFunction(
}
} else {
if (!callback || !$isCallable(callback)) {
throw new TypeError("lmao callback must be a function");
throw new TypeError("callback must be a function");
}
}
@@ -183,12 +264,12 @@ export function runSetupFunction(
}
}
function onLoad(this: PluginBuilder, filterObject: PluginConstraints, callback: OnLoadCallback): PluginBuilder {
function onLoad(filterObject: PluginConstraints, callback: OnLoadCallback): PluginBuilder {
validate(filterObject, callback, onLoadPlugins, undefined, undefined);
return this;
}
function onResolve(this: PluginBuilder, filterObject: PluginConstraints, callback): PluginBuilder {
function onResolve(filterObject: PluginConstraints, callback): PluginBuilder {
validate(filterObject, callback, onResolvePlugins, undefined, undefined);
return this;
}
@@ -297,8 +378,19 @@ export function runSetupFunction(
onBeforeParse,
onStart,
resolve: notImplementedIssueFn(2771, "build.resolve()"),
module: () => {
throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime");
module(specifier: string, callback: Function) {
if (typeof specifier !== "string") {
throw new TypeError("module() specifier must be a string");
}
if (typeof callback !== "function") {
throw new TypeError("module() callback must be a function");
}
const filter = new RegExp(`^${specifier.replace(/[^\w\s\d-]/g, "\\$&")}$`);
onResolve({ filter }, ({ path }) => ({
path: path,
namespace: "virtual-module",
}));
onLoad({ namespace: "virtual-module", filter }, () => callback());
},
addPreload: () => {
throw new TypeError("addPreload() is not supported in Bun.build() yet.");
@@ -316,7 +408,7 @@ export function runSetupFunction(
platform: config.target === "bun" ? "node" : config.target,
},
esbuild: {},
} as PluginBuilderExt);
} as any);
if (setupResult && $isPromise(setupResult)) {
if ($getPromiseInternalField(setupResult, $promiseFieldFlags) & $promiseStateFulfilled) {

View File

@@ -16366,7 +16366,10 @@ fn NewParser_(
var props: std.ArrayListUnmanaged(G.Property) = e_.properties.list();
const maybe_key_value: ?ExprNodeIndex =
if (e_.key_prop_index > -1) props.orderedRemove(@intCast(e_.key_prop_index)).value else null;
if (e_.key_prop_index > -1)
props.orderedRemove(@intCast(e_.key_prop_index)).value
else
null;
// arguments needs to be like
// {
@@ -16443,17 +16446,37 @@ fn NewParser_(
// is the return type of the first child an array?
// It's dynamic
// Else, it's static
args[3] = Expr{
args[3] = .{
.loc = expr.loc,
.data = .{
.e_boolean = .{
.value = is_static_jsx,
},
.e_boolean = .{ .value = is_static_jsx },
},
};
args[4] = p.newExpr(E.Undefined{}, expr.loc);
args[5] = Expr{ .data = Prefill.Data.This, .loc = expr.loc };
args[4] = if (p.options.features.hot_module_reloading)
// This object is kind of silly. Maybe we add a new AST node for printing line + column + fileName
p.newExpr(E.Object{
.properties = G.Property.List.fromSlice(p.allocator, &.{
.{
.key = p.newExpr(E.String{ .data = "fileName" }, expr.loc),
.value = p.newExpr(E.Dot{
.target = Expr.initIdentifier(p.hmr_api_ref, expr.loc),
.name = "id",
.name_loc = expr.loc,
}, expr.loc),
},
// Parser does not know line and column numbers, only an offset
// As a workaround, we temporarily emit this namespaced nonstandard
// field. We can clean this up if the experiment succeeds.
.{
.key = p.newExpr(E.String{ .data = "bunByteOffset" }, expr.loc),
.value = p.newExpr(E.Number{ .value = @floatFromInt(expr.loc.start) }, expr.loc),
},
}) catch bun.outOfMemory(),
}, expr.loc)
else
.{ .data = .{ .e_undefined = .{} }, .loc = expr.loc };
args[5] = .{ .data = .{ .e_this = .{} }, .loc = expr.loc };
}
return p.newExpr(E.Call{