mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 17:08:51 +00:00
Compare commits
11 Commits
dylan/pyth
...
chloe/hmr1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f48fca461 | ||
|
|
7ca8d301f6 | ||
|
|
557f506549 | ||
|
|
e0653134e1 | ||
|
|
c4f59f96f8 | ||
|
|
6d786ef426 | ||
|
|
23ab206b59 | ||
|
|
1d5bdf3a4e | ||
|
|
c9e72cd3cd | ||
|
|
812300637c | ||
|
|
601efe9ede |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -46,7 +46,7 @@
|
||||
},
|
||||
|
||||
// lldb
|
||||
"lldb.launch.initCommands": ["command source ${workspaceFolder}/.lldbinit"],
|
||||
// "lldb.launch.initCommands": ["command source ${workspaceFolder}/.lldbinit"],
|
||||
"lldb.verboseLogging": false,
|
||||
|
||||
// C++
|
||||
|
||||
14
packages/bun-wumbo/frontend.ts
Normal file
14
packages/bun-wumbo/frontend.ts
Normal 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);
|
||||
});
|
||||
36
packages/bun-wumbo/plugin.ts
Normal file
36
packages/bun-wumbo/plugin.ts
Normal 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 });
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
3
src/bake/bake.private.d.ts
vendored
3
src/bake/bake.private.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -667,7 +667,6 @@ declare global {
|
||||
}
|
||||
|
||||
import { BundlerMessageLevel } from "../enums";
|
||||
import { css } from "../macros" with { type: "macro" };
|
||||
import {
|
||||
BundlerMessage,
|
||||
BundlerMessageLocation,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
225
src/bake/wumbo.zig
Normal 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" },
|
||||
);
|
||||
}
|
||||
@@ -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 = .{
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -354,6 +354,8 @@ public:
|
||||
Bun__S3UploadStream__onResolveRequestStream,
|
||||
Bun__FileStreamWrapper__onRejectRequestStream,
|
||||
Bun__FileStreamWrapper__onResolveRequestStream,
|
||||
DevServer__onInitSetupResolve,
|
||||
DevServer__onInitSetupReject,
|
||||
};
|
||||
static constexpr size_t promiseFunctionsSize = 34;
|
||||
|
||||
|
||||
2
src/bun.js/bindings/headers.h
generated
2
src/bun.js/bindings/headers.h
generated
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user