Compare commits

...

2 Commits

Author SHA1 Message Date
Jarred-Sumner
a5cc558dcd bun run prettier 2025-06-04 23:57:48 +00:00
Jarred Sumner
906ae4a947 Handle HTML bundle response events 2025-06-04 16:55:41 -07:00
5 changed files with 79 additions and 9 deletions

View File

@@ -115,7 +115,7 @@ pub const AnyRoute = union(enum) {
if (argument.as(HTMLBundle)) |html_bundle| {
const entry = init_ctx.dedupe_html_bundle_map.getOrPut(html_bundle) catch bun.outOfMemory();
if (!entry.found_existing) {
entry.value_ptr.* = HTMLBundle.Route.init(html_bundle);
entry.value_ptr.* = HTMLBundle.Route.init(html_bundle, true);
return .{ .html = entry.value_ptr.* };
} else {
return .{ .html = entry.value_ptr.dupeRef() };

View File

@@ -63,6 +63,7 @@ pub const Route = struct {
dev_server_id: bun.bake.DevServer.RouteBundle.Index.Optional = .none,
/// When state == .pending, incomplete responses are stored here.
pending_responses: std.ArrayListUnmanaged(*PendingResponse) = .{},
register_static_routes: bool,
method: union(enum) {
any: void,
@@ -77,13 +78,14 @@ pub const Route = struct {
return cost;
}
pub fn init(html_bundle: *HTMLBundle) RefPtr(Route) {
pub fn init(html_bundle: *HTMLBundle, register_static_routes: bool) RefPtr(Route) {
return .new(.{
.bundle = .initRef(html_bundle),
.pending_responses = .{},
.ref_count = .init(),
.server = null,
.state = .pending,
.register_static_routes = register_static_routes,
});
}
@@ -410,16 +412,20 @@ pub const Route = struct {
route_path = route_path[1..];
}
server.appendStaticRoute(route_path, .{ .static = static_route }, .any) catch bun.outOfMemory();
if (this.register_static_routes) {
server.appendStaticRoute(route_path, .{ .static = static_route }, .any) catch bun.outOfMemory();
}
}
const html_route: *StaticRoute = this_html_route orelse @panic("Internal assertion failure: HTML entry point not found in HTMLBundle.");
const html_route_clone = html_route.clone(globalThis) catch bun.outOfMemory();
this.state = .{ .html = html_route_clone };
if (!(server.reloadStaticRoutes() catch bun.outOfMemory())) {
// Server has shutdown, so it won't receive any new requests
// TODO: handle this case
if (this.register_static_routes) {
if (!(server.reloadStaticRoutes() catch bun.outOfMemory())) {
// Server has shutdown, so it won't receive any new requests
// TODO: handle this case
}
}
},
.pending => unreachable,

View File

@@ -1698,6 +1698,23 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
this.renderWithBlobFromBodyValue();
return;
},
.HTMLBundle => |route| {
if (this.isAbortedOrEnded()) {
return;
}
if (this.resp) |resp| {
value.* = .{ .Used = {} };
this.setAbortHandler();
resp.timeout(this.server.?.config.idleTimeout);
route.data.server = this.server.?;
if (this.method == .HEAD) {
route.data.onHEADRequest(this.req.?, resp);
} else {
route.data.onRequest(this.req.?, resp);
}
}
return;
},
.Locked => |*lock| {
if (this.isAbortedOrEnded()) {
return;
@@ -2537,3 +2554,4 @@ const AnyRequestContext = JSC.API.AnyRequestContext;
const VirtualMachine = JSC.VirtualMachine;
const writeStatus = @import("../server.zig").writeStatus;
const Fallback = @import("../../../runtime.zig").Fallback;
const HTMLBundle = JSC.API.HTMLBundle;

View File

@@ -260,6 +260,8 @@ pub const Value = union(Tag) {
/// Single-use Blob
/// Avoids a heap allocation.
InternalBlob: InternalBlob,
/// HTMLBundle route used by Bun.serve()
HTMLBundle: RefPtr(HTMLBundle.Route),
/// Single-use Blob that stores the bytes in the Value itself.
// InlineBlob: InlineBlob,
Locked: PendingValue,
@@ -278,6 +280,7 @@ pub const Value = union(Tag) {
.InternalBlob => this.InternalBlob.slice().len == 0,
.Blob => this.Blob.size == 0,
.WTFStringImpl => this.WTFStringImpl.length() == 0,
.HTMLBundle => false,
.Error, .Locked => false,
};
}
@@ -371,6 +374,7 @@ pub const Value = union(Tag) {
.Blob => this.Blob.size,
.InternalBlob => @as(Blob.SizeType, @truncate(this.InternalBlob.sliceConst().len)),
.WTFStringImpl => @as(Blob.SizeType, @truncate(this.WTFStringImpl.utf8ByteLength())),
.HTMLBundle => 0,
.Locked => this.Locked.sizeHint(),
// .InlineBlob => @truncate(Blob.SizeType, this.InlineBlob.sliceConst().len),
else => 0,
@@ -381,6 +385,7 @@ pub const Value = union(Tag) {
return switch (this.*) {
.InternalBlob => @as(Blob.SizeType, @truncate(this.InternalBlob.sliceConst().len)),
.WTFStringImpl => @as(Blob.SizeType, @truncate(this.WTFStringImpl.byteSlice().len)),
.HTMLBundle => 0,
.Locked => this.Locked.sizeHint(),
// .InlineBlob => @truncate(Blob.SizeType, this.InlineBlob.sliceConst().len),
else => 0,
@@ -391,6 +396,7 @@ pub const Value = union(Tag) {
return switch (this.*) {
.InternalBlob => this.InternalBlob.bytes.items.len,
.WTFStringImpl => this.WTFStringImpl.memoryCost(),
.HTMLBundle => this.HTMLBundle.data.memoryCost(),
.Locked => this.Locked.sizeHint(),
// .InlineBlob => this.InlineBlob.sliceConst().len,
else => 0,
@@ -401,6 +407,7 @@ pub const Value = union(Tag) {
return switch (this.*) {
.InternalBlob => this.InternalBlob.sliceConst().len,
.WTFStringImpl => this.WTFStringImpl.byteSlice().len,
.HTMLBundle => 0,
.Locked => this.Locked.sizeHint(),
// .InlineBlob => this.InlineBlob.sliceConst().len,
else => 0,
@@ -433,6 +440,7 @@ pub const Value = union(Tag) {
Blob,
WTFStringImpl,
InternalBlob,
HTMLBundle,
// InlineBlob,
Locked,
Used,
@@ -596,9 +604,10 @@ pub const Value = union(Tag) {
if (js_type == .DOMWrapper) {
if (value.as(Blob)) |blob| {
return Body.Value{
.Blob = blob.dupe(),
};
return Body.Value{ .Blob = blob.dupe() };
}
if (value.as(HTMLBundle)) |html| {
return Body.Value{ .HTMLBundle = HTMLBundle.Route.init(html, false) };
}
}
@@ -755,6 +764,7 @@ pub const Value = union(Tag) {
.Blob => this.Blob.sharedView(),
.InternalBlob => this.InternalBlob.sliceConst(),
.WTFStringImpl => if (this.WTFStringImpl.canUseAsUTF8()) this.WTFStringImpl.latin1Slice() else "",
.HTMLBundle => "",
// .InlineBlob => this.InlineBlob.sliceConst(),
else => "",
};
@@ -804,6 +814,10 @@ pub const Value = union(Tag) {
this.* = .{ .Used = {} };
return new_blob;
},
.HTMLBundle => {
this.* = .{ .Used = {} };
return Blob.initEmpty(undefined);
},
// .InlineBlob => {
// const cloned = this.InlineBlob.bytes;
// // keep same behavior as InternalBlob but clone the data
@@ -834,6 +848,7 @@ pub const Value = union(Tag) {
.Blob => AnyBlob{ .Blob = this.Blob },
.InternalBlob => AnyBlob{ .InternalBlob = this.InternalBlob },
// .InlineBlob => AnyBlob{ .InlineBlob = this.InlineBlob },
.HTMLBundle => return null,
.Locked => this.Locked.toAnyBlobAllowPromise() orelse return null,
else => return null,
};
@@ -965,6 +980,11 @@ pub const Value = union(Tag) {
this.* = Value{ .Null = {} };
}
if (tag == .HTMLBundle) {
this.HTMLBundle.deref();
this.* = Value{ .Null = {} };
}
if (tag == .Error) {
this.Error.deinit();
}
@@ -1068,6 +1088,10 @@ pub const Value = union(Tag) {
return Value{ .WTFStringImpl = this.WTFStringImpl };
}
if (this.* == .HTMLBundle) {
return Value{ .HTMLBundle = this.HTMLBundle.dupeRef() };
}
if (this.* == .Null) {
return Value{ .Null = {} };
}
@@ -1719,3 +1743,5 @@ const AnyBlob = Blob.Any;
const InternalBlob = Blob.Internal;
const Response = JSC.WebCore.Response;
const streams = JSC.WebCore.streams;
const HTMLBundle = JSC.API.HTMLBundle;
const RefPtr = bun.ptr.RefPtr;

View File

@@ -0,0 +1,20 @@
import { expect, test } from "bun:test";
import { tempDirWithFiles } from "harness";
import { join } from "path";
test("Response(htmlImport)", async () => {
const dir = tempDirWithFiles("response-htmlbundle", {
"index.html": "<!DOCTYPE html><html><body>Hello HTML</body></html>",
});
const { default: html } = await import(join(dir, "index.html"));
using server = Bun.serve({
port: 0,
fetch() {
return new Response(html);
},
});
const res = await fetch(server.url);
expect(await res.text()).toContain("Hello HTML");
const missing = await fetch(server.url + "/index.html");
expect(missing.status).toBe(404);
});