mirror of
https://github.com/oven-sh/bun
synced 2026-02-25 11:07:19 +01:00
Compare commits
45 Commits
claude/fix
...
zack/ssg-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59f12d30b3 | ||
|
|
f0d4fa8b63 | ||
|
|
3fb0a824cb | ||
|
|
ab3566627d | ||
|
|
3906407e5d | ||
|
|
33447ef2db | ||
|
|
3760407908 | ||
|
|
c1f0ce277d | ||
|
|
bfe3041179 | ||
|
|
5b6344cf3c | ||
|
|
b4fdf41ea5 | ||
|
|
b9da6b71f9 | ||
|
|
87487468f3 | ||
|
|
cfdeb42023 | ||
|
|
20e4c094ac | ||
|
|
17be416250 | ||
|
|
9745f01041 | ||
|
|
16131f92e1 | ||
|
|
59a4d0697b | ||
|
|
78a2ae44aa | ||
|
|
7f295919a9 | ||
|
|
1d0984b5c4 | ||
|
|
dfa93a8ede | ||
|
|
c8773c5e30 | ||
|
|
0f74fafc59 | ||
|
|
47d6e161fe | ||
|
|
160625c37c | ||
|
|
1b9b686772 | ||
|
|
6f3e098bac | ||
|
|
4c6b296a7c | ||
|
|
2ab962bf6b | ||
|
|
f556fc987c | ||
|
|
3a1b12ee61 | ||
|
|
a952b4200e | ||
|
|
24485fb432 | ||
|
|
b10fda0487 | ||
|
|
740cdaba3d | ||
|
|
68be15361a | ||
|
|
c57be8dcdb | ||
|
|
5115a88126 | ||
|
|
e992b804c8 | ||
|
|
b92555e099 | ||
|
|
381848cd69 | ||
|
|
61f9845f80 | ||
|
|
abc52da7bb |
@@ -3,6 +3,7 @@ packages/bun-usockets/src/crypto/sni_tree.cpp
|
||||
src/bake/BakeGlobalObject.cpp
|
||||
src/bake/BakeProduction.cpp
|
||||
src/bake/BakeSourceProvider.cpp
|
||||
src/bake/DevServerSourceProvider.cpp
|
||||
src/bun.js/bindings/ActiveDOMCallback.cpp
|
||||
src/bun.js/bindings/AsymmetricKeyValue.cpp
|
||||
src/bun.js/bindings/AsyncContextFrame.cpp
|
||||
@@ -391,6 +392,7 @@ src/bun.js/bindings/webcore/ReadableStreamDefaultController.cpp
|
||||
src/bun.js/bindings/webcore/ReadableStreamSink.cpp
|
||||
src/bun.js/bindings/webcore/ReadableStreamSource.cpp
|
||||
src/bun.js/bindings/webcore/ResourceTiming.cpp
|
||||
src/bun.js/bindings/webcore/ResponseHelpers.cpp
|
||||
src/bun.js/bindings/webcore/RFC7230.cpp
|
||||
src/bun.js/bindings/webcore/SerializedScriptValue.cpp
|
||||
src/bun.js/bindings/webcore/ServerTiming.cpp
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
src/js/builtins.d.ts
|
||||
src/js/builtins/Bake.ts
|
||||
src/js/builtins/BakeSSRResponse.ts
|
||||
src/js/builtins/BundlerPlugin.ts
|
||||
src/js/builtins/ByteLengthQueuingStrategy.ts
|
||||
src/js/builtins/CommonJS.ts
|
||||
|
||||
@@ -252,6 +252,12 @@ pub inline fn downcast(a: Allocator) ?*AllocationScope {
|
||||
null;
|
||||
}
|
||||
|
||||
pub fn leakSlice(scope: *AllocationScope, memory: anytype) void {
|
||||
if (comptime !enabled) return;
|
||||
_ = @typeInfo(@TypeOf(memory)).pointer;
|
||||
bun.assert(!scope.trackExternalFree(memory, null));
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
|
||||
@@ -1439,9 +1439,20 @@ pub fn VisitExpr(
|
||||
// Why? Because we *don't* want to check for uses of
|
||||
// `useState` _inside_ React, and we know React uses
|
||||
// commonjs so it will never be `.e_import_identifier`.
|
||||
e_.target.data == .e_import_identifier) {
|
||||
check_for_usestate: {
|
||||
if (e_.target.data == .e_import_identifier) break :check_for_usestate true;
|
||||
// Also check for `React.useState(...)`
|
||||
if (e_.target.data == .e_dot and e_.target.data.e_dot.target.data == .e_import_identifier) {
|
||||
const id = e_.target.data.e_dot.target.data.e_import_identifier;
|
||||
const name = p.symbols.items[id.ref.innerIndex()].original_name;
|
||||
break :check_for_usestate bun.strings.eqlComptime(name, "React");
|
||||
}
|
||||
break :check_for_usestate false;
|
||||
}) {
|
||||
bun.assert(p.options.features.server_components.isServerSide());
|
||||
if (bun.strings.eqlComptime(original_name, "useState")) {
|
||||
if (!bun.strings.startsWith(p.source.path.pretty, "node_modules") and
|
||||
bun.strings.eqlComptime(original_name, "useState"))
|
||||
{
|
||||
p.log.addError(
|
||||
p.source,
|
||||
expr.loc,
|
||||
|
||||
35
src/bake.zig
35
src/bake.zig
@@ -27,9 +27,6 @@ pub const UserOptions = struct {
|
||||
|
||||
/// Currently, this function must run at the top of the event loop.
|
||||
pub fn fromJS(config: JSValue, global: *jsc.JSGlobalObject) !UserOptions {
|
||||
if (!config.isObject()) {
|
||||
return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{});
|
||||
}
|
||||
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
errdefer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
@@ -38,6 +35,38 @@ pub const UserOptions = struct {
|
||||
errdefer allocations.free();
|
||||
var bundler_options = SplitBundlerOptions.empty;
|
||||
|
||||
if (!config.isObject()) {
|
||||
// Allow users to do `export default { app: 'react' }` for convenience
|
||||
if (config.isString()) {
|
||||
const bunstr = try config.toBunString(global);
|
||||
defer bunstr.deref();
|
||||
const utf8_string = bunstr.toUTF8(bun.default_allocator);
|
||||
defer utf8_string.deinit();
|
||||
|
||||
if (bun.strings.eql(utf8_string.byteSlice(), "react")) {
|
||||
const root = bun.getcwdAlloc(alloc) catch |err| switch (err) {
|
||||
error.OutOfMemory => {
|
||||
return global.throwOutOfMemory();
|
||||
},
|
||||
else => {
|
||||
return global.throwError(err, "while querying current working directory");
|
||||
},
|
||||
};
|
||||
|
||||
const framework = try Framework.react(alloc);
|
||||
|
||||
return UserOptions{
|
||||
.arena = arena,
|
||||
.allocations = allocations,
|
||||
.root = try alloc.dupeZ(u8, root),
|
||||
.framework = framework,
|
||||
.bundler_options = bundler_options,
|
||||
};
|
||||
}
|
||||
}
|
||||
return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{});
|
||||
}
|
||||
|
||||
if (try config.getOptional(global, "bundlerOptions", JSValue)) |js_options| {
|
||||
if (try js_options.getOptional(global, "server", JSValue)) |server_options| {
|
||||
bundler_options.server = try BuildConfigSubset.fromJS(global, server_options);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// clang-format off
|
||||
#include "BakeSourceProvider.h"
|
||||
#include "DevServerSourceProvider.h"
|
||||
#include "BakeGlobalObject.h"
|
||||
#include "JavaScriptCore/CallData.h"
|
||||
#include "JavaScriptCore/Completion.h"
|
||||
@@ -78,6 +79,34 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS
|
||||
return JSC::JSValue::encode(result);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, const char* sourceMapJSONPtr, size_t sourceMapJSONLength) {
|
||||
JSC::VM&vm = global->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
String string = "bake://server.patch.js"_s;
|
||||
JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(string));
|
||||
|
||||
// Use DevServerSourceProvider with the source map JSON
|
||||
auto provider = DevServerSourceProvider::create(
|
||||
global,
|
||||
source.toWTFString(),
|
||||
sourceMapJSONPtr,
|
||||
sourceMapJSONLength,
|
||||
origin,
|
||||
WTFMove(string),
|
||||
WTF::TextPosition(),
|
||||
JSC::SourceProviderSourceType::Program
|
||||
);
|
||||
|
||||
JSC::SourceCode sourceCode = JSC::SourceCode(provider);
|
||||
|
||||
JSC::JSValue result = vm.interpreter.executeProgram(sourceCode, global, global);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
RELEASE_ASSERT(result);
|
||||
return JSC::JSValue::encode(result);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue BakeGetModuleNamespace(
|
||||
JSC::JSGlobalObject* global,
|
||||
JSC::JSValue keyValue
|
||||
|
||||
@@ -839,6 +839,7 @@ fn onJsRequest(dev: *DevServer, req: *Request, resp: AnyResponse) void {
|
||||
arena.allocator(),
|
||||
source_id.kind,
|
||||
dev.allocator,
|
||||
.client,
|
||||
) catch bun.outOfMemory();
|
||||
const response = StaticRoute.initFromAnyBlob(&.fromOwnedSlice(dev.allocator, json_bytes), .{
|
||||
.server = dev.server,
|
||||
@@ -950,7 +951,7 @@ fn ensureRouteIsBundled(
|
||||
dev: *DevServer,
|
||||
route_bundle_index: RouteBundle.Index,
|
||||
kind: DeferredRequest.Handler.Kind,
|
||||
req: *Request,
|
||||
req: ReqOrSaved,
|
||||
resp: AnyResponse,
|
||||
) bun.JSError!void {
|
||||
assert(dev.magic == .valid);
|
||||
@@ -1065,35 +1066,60 @@ fn ensureRouteIsBundled(
|
||||
);
|
||||
},
|
||||
.loaded => switch (kind) {
|
||||
.server_handler => try dev.onFrameworkRequestWithBundle(route_bundle_index, .{ .stack = req }, resp),
|
||||
.bundled_html_page => dev.onHtmlRequestWithBundle(route_bundle_index, resp, bun.http.Method.which(req.method()) orelse .POST),
|
||||
.server_handler => try dev.onFrameworkRequestWithBundle(route_bundle_index, if (req == .req) .{ .stack = req.req } else .{ .saved = req.saved }, resp),
|
||||
.bundled_html_page => dev.onHtmlRequestWithBundle(route_bundle_index, resp, req.method()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const ReqOrSaved = union(enum) {
|
||||
req: *Request,
|
||||
saved: bun.jsc.API.SavedRequest,
|
||||
|
||||
pub fn method(this: *const @This()) bun.http.Method {
|
||||
return switch (this.*) {
|
||||
.req => |req| bun.http.Method.which(req.method()) orelse .POST,
|
||||
.saved => |saved| saved.request.method,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn deferRequest(
|
||||
dev: *DevServer,
|
||||
requests_array: *DeferredRequest.List,
|
||||
route_bundle_index: RouteBundle.Index,
|
||||
kind: DeferredRequest.Handler.Kind,
|
||||
req: *Request,
|
||||
req: ReqOrSaved,
|
||||
resp: AnyResponse,
|
||||
) !void {
|
||||
const deferred = dev.deferred_request_pool.get();
|
||||
const method = bun.http.Method.which(req.method()) orelse .POST;
|
||||
debug.log("DeferredRequest(0x{x}).init", .{@intFromPtr(&deferred.data)});
|
||||
const method = req.method();
|
||||
deferred.data = .{
|
||||
.route_bundle_index = route_bundle_index,
|
||||
.dev = dev,
|
||||
.ref_count = .init(),
|
||||
.handler = switch (kind) {
|
||||
.bundled_html_page => .{ .bundled_html_page = .{ .response = resp, .method = method } },
|
||||
.server_handler => .{
|
||||
.server_handler = dev.server.?.prepareAndSaveJsRequestContext(req, resp, dev.vm.global, method) orelse return,
|
||||
.bundled_html_page => brk: {
|
||||
resp.onAborted(*DeferredRequest, DeferredRequest.onAbort, &deferred.data);
|
||||
break :brk .{ .bundled_html_page = .{ .response = resp, .method = method } };
|
||||
},
|
||||
.server_handler => brk: {
|
||||
const server_handler = switch (req) {
|
||||
.req => |r| dev.server.?.prepareAndSaveJsRequestContext(r, resp, dev.vm.global, method) orelse {
|
||||
dev.deferred_request_pool.put(deferred);
|
||||
return;
|
||||
},
|
||||
.saved => |saved| saved,
|
||||
};
|
||||
server_handler.ctx.setAbortCallback(DeferredRequest.onAbortWrapper, &deferred.data);
|
||||
break :brk .{
|
||||
.server_handler = server_handler,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
deferred.data.ref();
|
||||
resp.onAborted(*DeferredRequest, DeferredRequest.onAbort, &deferred.data);
|
||||
requests_array.prepend(deferred);
|
||||
}
|
||||
|
||||
@@ -1181,7 +1207,7 @@ fn onFrameworkRequestWithBundle(
|
||||
const route_bundle = dev.routeBundlePtr(route_bundle_index);
|
||||
assert(route_bundle.data == .framework);
|
||||
|
||||
const bundle = &route_bundle.data.framework;
|
||||
const framework_bundle = &route_bundle.data.framework;
|
||||
|
||||
// Extract route params by re-matching the URL
|
||||
var params: FrameworkRouter.MatchedParams = undefined;
|
||||
@@ -1230,7 +1256,7 @@ fn onFrameworkRequestWithBundle(
|
||||
const value_str = bun.String.cloneUTF8(param.value);
|
||||
defer value_str.deref();
|
||||
|
||||
obj.put(global, key_str, value_str.toJS(global));
|
||||
_ = try obj.putBunStringOneOrArray(global, &key_str, value_str.toJS(global));
|
||||
}
|
||||
break :blk obj;
|
||||
} else JSValue.null;
|
||||
@@ -1238,13 +1264,31 @@ fn onFrameworkRequestWithBundle(
|
||||
const server_request_callback = dev.server_fetch_function_callback.get() orelse
|
||||
unreachable; // did not initialize server code
|
||||
|
||||
const router_type = dev.router.typePtr(dev.router.routePtr(bundle.route_index).type);
|
||||
const router_type = dev.router.typePtr(dev.router.routePtr(framework_bundle.route_index).type);
|
||||
|
||||
dev.server.?.onRequestFromSaved(
|
||||
// FIXME: We should not create these on every single request
|
||||
// Wrapper functions for AsyncLocalStorage that match JSHostFnZig signature
|
||||
const SetAsyncLocalStorageWrapper = struct {
|
||||
pub fn call(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
return VirtualMachine.VirtualMachine__setDevServerAsyncLocalStorage(global, callframe);
|
||||
}
|
||||
};
|
||||
|
||||
const GetAsyncLocalStorageWrapper = struct {
|
||||
pub fn call(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
return VirtualMachine.VirtualMachine__getDevServerAsyncLocalStorage(global, callframe);
|
||||
}
|
||||
};
|
||||
|
||||
// Create the setter and getter functions for AsyncLocalStorage
|
||||
const setAsyncLocalStorage = jsc.JSFunction.create(dev.vm.global, "setDevServerAsyncLocalStorage", SetAsyncLocalStorageWrapper.call, 1, .{});
|
||||
const getAsyncLocalStorage = jsc.JSFunction.create(dev.vm.global, "getDevServerAsyncLocalStorage", GetAsyncLocalStorageWrapper.call, 0, .{});
|
||||
|
||||
dev.server.?.onSavedRequest(
|
||||
req,
|
||||
resp,
|
||||
server_request_callback,
|
||||
5,
|
||||
7,
|
||||
.{
|
||||
// routerTypeMain
|
||||
router_type.server_file_string.get() orelse str: {
|
||||
@@ -1256,17 +1300,17 @@ fn onFrameworkRequestWithBundle(
|
||||
break :str str;
|
||||
},
|
||||
// routeModules
|
||||
bundle.cached_module_list.get() orelse arr: {
|
||||
framework_bundle.cached_module_list.get() orelse arr: {
|
||||
const global = dev.vm.global;
|
||||
const keys = dev.server_graph.bundled_files.keys();
|
||||
var n: usize = 1;
|
||||
var route = dev.router.routePtr(bundle.route_index);
|
||||
var route = dev.router.routePtr(framework_bundle.route_index);
|
||||
while (true) {
|
||||
if (route.file_layout != .none) n += 1;
|
||||
route = dev.router.routePtr(route.parent.unwrap() orelse break);
|
||||
}
|
||||
const arr = try JSValue.createEmptyArray(global, n);
|
||||
route = dev.router.routePtr(bundle.route_index);
|
||||
route = dev.router.routePtr(framework_bundle.route_index);
|
||||
{
|
||||
const relative_path_buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(relative_path_buf);
|
||||
@@ -1287,11 +1331,11 @@ fn onFrameworkRequestWithBundle(
|
||||
}
|
||||
route = dev.router.routePtr(route.parent.unwrap() orelse break);
|
||||
}
|
||||
bundle.cached_module_list = .create(arr, global);
|
||||
framework_bundle.cached_module_list = .create(arr, global);
|
||||
break :arr arr;
|
||||
},
|
||||
// clientId
|
||||
bundle.cached_client_bundle_url.get() orelse str: {
|
||||
framework_bundle.cached_client_bundle_url.get() orelse str: {
|
||||
const bundle_index: u32 = route_bundle_index.get();
|
||||
const generation: u32 = route_bundle.client_script_generation;
|
||||
const str = bun.String.createFormat(client_prefix ++ "/route-{}{}.js", .{
|
||||
@@ -1300,17 +1344,21 @@ fn onFrameworkRequestWithBundle(
|
||||
}) catch bun.outOfMemory();
|
||||
defer str.deref();
|
||||
const js = str.toJS(dev.vm.global);
|
||||
bundle.cached_client_bundle_url = .create(js, dev.vm.global);
|
||||
framework_bundle.cached_client_bundle_url = .create(js, dev.vm.global);
|
||||
break :str js;
|
||||
},
|
||||
// styles
|
||||
bundle.cached_css_file_array.get() orelse arr: {
|
||||
framework_bundle.cached_css_file_array.get() orelse arr: {
|
||||
const js = dev.generateCssJSArray(route_bundle) catch bun.outOfMemory();
|
||||
bundle.cached_css_file_array = .create(js, dev.vm.global);
|
||||
framework_bundle.cached_css_file_array = .create(js, dev.vm.global);
|
||||
break :arr js;
|
||||
},
|
||||
// params
|
||||
params_js_value,
|
||||
// setDevServerAsyncLocalStorage function
|
||||
setAsyncLocalStorage,
|
||||
// getDevServerAsyncLocalStorage function
|
||||
getAsyncLocalStorage,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1476,7 +1524,7 @@ fn generateJavaScriptCodeForHTMLFile(
|
||||
|
||||
pub fn onJsRequestWithBundle(dev: *DevServer, bundle_index: RouteBundle.Index, resp: AnyResponse, method: bun.http.Method) void {
|
||||
const route_bundle = dev.routeBundlePtr(bundle_index);
|
||||
const blob = route_bundle.client_bundle orelse generate: {
|
||||
const client_bundle = route_bundle.client_bundle orelse generate: {
|
||||
const payload = dev.generateClientBundle(route_bundle) catch bun.outOfMemory();
|
||||
errdefer dev.allocator.free(payload);
|
||||
route_bundle.client_bundle = StaticRoute.initFromAnyBlob(
|
||||
@@ -1489,7 +1537,7 @@ pub fn onJsRequestWithBundle(dev: *DevServer, bundle_index: RouteBundle.Index, r
|
||||
break :generate route_bundle.client_bundle.?;
|
||||
};
|
||||
dev.source_maps.addWeakRef(route_bundle.sourceMapId());
|
||||
blob.onWithMethod(method, resp);
|
||||
client_bundle.onWithMethod(method, resp);
|
||||
}
|
||||
|
||||
pub fn onSrcRequest(dev: *DevServer, req: *uws.Request, resp: anytype) void {
|
||||
@@ -1539,7 +1587,11 @@ pub const DeferredRequest = struct {
|
||||
pub const List = std.SinglyLinkedList(DeferredRequest);
|
||||
pub const Node = List.Node;
|
||||
|
||||
const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinitImpl, .{});
|
||||
const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinitImpl, .{
|
||||
.debug_name = "DeferredRequest",
|
||||
});
|
||||
|
||||
const debugLog = bun.Output.Scoped("DlogeferredRequest", .hidden).log;
|
||||
|
||||
route_bundle_index: RouteBundle.Index,
|
||||
handler: Handler,
|
||||
@@ -1572,8 +1624,18 @@ pub const DeferredRequest = struct {
|
||||
};
|
||||
};
|
||||
|
||||
fn onAbort(this: *DeferredRequest, resp: AnyResponse) void {
|
||||
_ = resp;
|
||||
fn onAbortWrapper(this: *anyopaque) void {
|
||||
const self: *DeferredRequest = @alignCast(@ptrCast(this));
|
||||
self.onAbortImpl();
|
||||
}
|
||||
|
||||
fn onAbort(this: *DeferredRequest, _: AnyResponse) void {
|
||||
this.onAbortImpl();
|
||||
}
|
||||
|
||||
fn onAbortImpl(this: *DeferredRequest) void {
|
||||
debugLog("DeferredRequest(0x{x}) onAbort", .{@intFromPtr(this)});
|
||||
|
||||
this.abort();
|
||||
assert(this.handler == .aborted);
|
||||
}
|
||||
@@ -1584,6 +1646,7 @@ pub const DeferredRequest = struct {
|
||||
/// such as for bundling failures or aborting the server.
|
||||
/// Does not free the underlying `DeferredRequest.Node`
|
||||
fn deinitImpl(this: *DeferredRequest) void {
|
||||
debugLog("DeferredRequest(0x{x}) deinitImpl", .{@intFromPtr(this)});
|
||||
this.ref_count.assertNoRefs();
|
||||
|
||||
defer this.dev.deferred_request_pool.put(@fieldParentPtr("data", this));
|
||||
@@ -1595,11 +1658,11 @@ pub const DeferredRequest = struct {
|
||||
|
||||
/// Deinitializes state by aborting the connection.
|
||||
fn abort(this: *DeferredRequest) void {
|
||||
debugLog("DeferredRequest(0x{x}) abort", .{@intFromPtr(this)});
|
||||
var handler = this.handler;
|
||||
this.handler = .aborted;
|
||||
switch (handler) {
|
||||
.server_handler => |*saved| {
|
||||
saved.ctx.onAbort(saved.response);
|
||||
saved.js_request.deinit();
|
||||
},
|
||||
.bundled_html_page => |r| {
|
||||
@@ -2266,14 +2329,70 @@ pub fn finalizeBundle(
|
||||
|
||||
// Load all new chunks into the server runtime.
|
||||
if (!dev.frontend_only and dev.server_graph.current_chunk_len > 0) {
|
||||
const server_bundle = try dev.server_graph.takeJSBundle(&.{ .kind = .hmr_chunk });
|
||||
// Generate a script_id for server bundles
|
||||
// Use high bit set to distinguish from client bundles, and include generation
|
||||
const server_script_id = SourceMapStore.Key.init((1 << 63) | @as(u64, dev.generation));
|
||||
|
||||
// Get the source map if available and render to JSON
|
||||
var source_map_json = if (dev.server_graph.current_chunk_source_maps.items.len > 0) json: {
|
||||
// Create a temporary source map entry to render
|
||||
var source_map_entry = SourceMapStore.Entry{
|
||||
.ref_count = 1,
|
||||
.paths = &.{},
|
||||
.files = .empty,
|
||||
.overlapping_memory_cost = 0,
|
||||
};
|
||||
|
||||
// Fill the source map entry
|
||||
var arena = std.heap.ArenaAllocator.init(dev.allocator);
|
||||
defer arena.deinit();
|
||||
try dev.server_graph.takeSourceMap(arena.allocator(), dev.allocator, &source_map_entry);
|
||||
defer {
|
||||
source_map_entry.ref_count = 0;
|
||||
source_map_entry.deinit(dev);
|
||||
}
|
||||
|
||||
const json_data = try source_map_entry.renderJSON(
|
||||
dev,
|
||||
arena.allocator(),
|
||||
.hmr_chunk,
|
||||
dev.allocator,
|
||||
.server,
|
||||
);
|
||||
break :json json_data;
|
||||
} else null;
|
||||
defer if (source_map_json) |json| bun.default_allocator.free(json);
|
||||
|
||||
const server_bundle = try dev.server_graph.takeJSBundle(&.{
|
||||
.kind = .hmr_chunk,
|
||||
.script_id = server_script_id,
|
||||
});
|
||||
defer dev.allocator.free(server_bundle);
|
||||
|
||||
const server_modules = c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| {
|
||||
// No user code has been evaluated yet, since everything is to
|
||||
// be wrapped in a function clousure. This means that the likely
|
||||
// error is going to be a syntax error, or other mistake in the
|
||||
// bundler.
|
||||
// TODO: is this the best place to set this? Would it be better to
|
||||
// transpile the server modules to replace `new Response(...)` with `new
|
||||
// ResponseBake(...)`??
|
||||
dev.vm.setAllowJSXInResponseConstructor(true);
|
||||
|
||||
const server_modules = if (bun.take(&source_map_json)) |json| blk: {
|
||||
// This memory will be owned by the `DevServerSourceProvider` in C++
|
||||
// from here on out
|
||||
dev.allocation_scope.leakSlice(json);
|
||||
|
||||
break :blk c.BakeLoadServerHmrPatchWithSourceMap(
|
||||
@ptrCast(dev.vm.global),
|
||||
bun.String.cloneUTF8(server_bundle),
|
||||
json.ptr,
|
||||
json.len,
|
||||
) catch |err| {
|
||||
// No user code has been evaluated yet, since everything is to
|
||||
// be wrapped in a function clousure. This means that the likely
|
||||
// error is going to be a syntax error, or other mistake in the
|
||||
// bundler.
|
||||
dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err));
|
||||
@panic("Error thrown while evaluating server code. This is always a bug in the bundler.");
|
||||
};
|
||||
} else c.BakeLoadServerHmrPatch(@ptrCast(dev.vm.global), bun.String.cloneLatin1(server_bundle)) catch |err| {
|
||||
dev.vm.printErrorLikeObjectToConsole(dev.vm.global.takeException(err));
|
||||
@panic("Error thrown while evaluating server code. This is always a bug in the bundler.");
|
||||
};
|
||||
@@ -2819,7 +2938,7 @@ fn onRequest(dev: *DevServer, req: *Request, resp: anytype) void {
|
||||
dev.ensureRouteIsBundled(
|
||||
dev.getOrPutRouteBundle(.{ .framework = route_index }) catch bun.outOfMemory(),
|
||||
.server_handler,
|
||||
req,
|
||||
.{ .req = req },
|
||||
AnyResponse.init(resp),
|
||||
) catch bun.outOfMemory();
|
||||
return;
|
||||
@@ -2834,7 +2953,31 @@ fn onRequest(dev: *DevServer, req: *Request, resp: anytype) void {
|
||||
}
|
||||
|
||||
pub fn respondForHTMLBundle(dev: *DevServer, html: *HTMLBundle.HTMLBundleRoute, req: *uws.Request, resp: AnyResponse) !void {
|
||||
try dev.ensureRouteIsBundled(try dev.getOrPutRouteBundle(.{ .html = html }), .bundled_html_page, req, resp);
|
||||
try dev.ensureRouteIsBundled(try dev.getOrPutRouteBundle(.{ .html = html }), .bundled_html_page, .{ .req = req }, resp);
|
||||
}
|
||||
|
||||
// TODO: path params
|
||||
pub fn handleRenderRedirect(
|
||||
dev: *DevServer,
|
||||
saved_request: bun.jsc.API.SavedRequest,
|
||||
render_path: []const u8,
|
||||
resp: AnyResponse,
|
||||
) !void {
|
||||
// Match the render path against the router
|
||||
var params: FrameworkRouter.MatchedParams = undefined;
|
||||
if (dev.router.matchSlow(render_path, ¶ms)) |route_index| {
|
||||
// Found a matching route, bundle it and handle the request
|
||||
dev.ensureRouteIsBundled(
|
||||
dev.getOrPutRouteBundle(.{ .framework = route_index }) catch bun.outOfMemory(),
|
||||
.server_handler,
|
||||
.{ .saved = saved_request },
|
||||
resp,
|
||||
) catch bun.outOfMemory();
|
||||
return;
|
||||
}
|
||||
|
||||
// No matching route found - render 404
|
||||
sendBuiltInNotFound(resp);
|
||||
}
|
||||
|
||||
fn getOrPutRouteBundle(dev: *DevServer, route: RouteBundle.UnresolvedIndex) !RouteBundle.Index {
|
||||
@@ -3591,6 +3734,11 @@ const c = struct {
|
||||
return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code });
|
||||
}
|
||||
|
||||
fn BakeLoadServerHmrPatchWithSourceMap(global: *jsc.JSGlobalObject, code: bun.String, source_map_json_ptr: [*]const u8, source_map_json_len: usize) bun.JSError!JSValue {
|
||||
const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, [*]const u8, usize) callconv(.c) JSValue, .{ .name = "BakeLoadServerHmrPatchWithSourceMap" }).*;
|
||||
return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, source_map_json_ptr, source_map_json_len });
|
||||
}
|
||||
|
||||
fn BakeLoadInitialServerCode(global: *jsc.JSGlobalObject, code: bun.String, separate_ssr_graph: bool) bun.JSError!JSValue {
|
||||
const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bool) callconv(.c) JSValue, .{ .name = "BakeLoadInitialServerCode" }).*;
|
||||
return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, separate_ssr_graph });
|
||||
|
||||
@@ -76,6 +76,12 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
.server => void,
|
||||
},
|
||||
|
||||
/// Source maps for server chunks
|
||||
current_chunk_source_maps: if (side == .server) ArrayListUnmanaged(PackedMap.RefOrEmpty) else void = if (side == .server) .empty,
|
||||
|
||||
/// File indices for server chunks to track which file each chunk comes from
|
||||
current_chunk_file_indices: if (side == .server) ArrayListUnmanaged(FileIndex) else void = if (side == .server) .empty,
|
||||
|
||||
pub const empty: @This() = .{
|
||||
.bundled_files = .empty,
|
||||
.stale_files = .empty,
|
||||
@@ -89,6 +95,8 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
.current_chunk_parts = .empty,
|
||||
|
||||
.current_css_files = if (side == .client) .empty,
|
||||
.current_chunk_source_maps = if (side == .server) .empty else {},
|
||||
.current_chunk_file_indices = if (side == .server) .empty else {},
|
||||
};
|
||||
|
||||
pub const File = switch (side) {
|
||||
@@ -324,6 +332,13 @@ 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),
|
||||
.current_chunk_source_maps = if (side == .server) {
|
||||
for (g.current_chunk_source_maps.items) |source_map| {
|
||||
source_map.deref(&g.owner().*);
|
||||
}
|
||||
g.current_chunk_source_maps.deinit(allocator);
|
||||
},
|
||||
.current_chunk_file_indices = if (side == .server) g.current_chunk_file_indices.deinit(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -356,6 +371,14 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
.empty => {},
|
||||
}
|
||||
}
|
||||
} else if (side == .server) {
|
||||
graph += DevServer.memoryCostArrayList(g.current_chunk_source_maps);
|
||||
graph += DevServer.memoryCostArrayList(g.current_chunk_file_indices);
|
||||
for (g.current_chunk_source_maps.items) |source_map| {
|
||||
if (source_map == .ref) {
|
||||
source_maps += source_map.ref.data.memoryCostWithDedupe(new_dedupe_bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
return .{
|
||||
.graph = graph,
|
||||
@@ -488,7 +511,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
.js => |js| {
|
||||
// Insert new source map or patch existing empty source map.
|
||||
const source_map: PackedMap.RefOrEmpty = brk: {
|
||||
if (js.source_map) |source_map| {
|
||||
if (js.source_map) |*source_map| {
|
||||
bun.debugAssert(!flags.is_html_route); // suspect behind #17956
|
||||
if (source_map.chunk.buffer.len() > 0) {
|
||||
flags.source_map_state = .ref;
|
||||
@@ -585,12 +608,40 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
if (content == .js) {
|
||||
try g.current_chunk_parts.append(dev.allocator, content.js.code);
|
||||
g.current_chunk_len += content.js.code.len;
|
||||
if (content.js.source_map) |source_map| {
|
||||
var take = source_map.chunk.buffer;
|
||||
take.deinit();
|
||||
if (source_map.escaped_source) |escaped_source| {
|
||||
bun.default_allocator.free(escaped_source);
|
||||
|
||||
// Track the file index for this chunk
|
||||
try g.current_chunk_file_indices.append(dev.allocator, file_index);
|
||||
|
||||
// TODO: we probably want to store SSR chunks but not
|
||||
// server chunks, but not 100% sure
|
||||
const should_immediately_free_sourcemap = false;
|
||||
if (should_immediately_free_sourcemap) {
|
||||
if (content.js.source_map) |source_map| {
|
||||
var take = source_map.chunk.buffer;
|
||||
take.deinit();
|
||||
if (source_map.escaped_source) |escaped_source| {
|
||||
bun.default_allocator.free(escaped_source);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (content.js.source_map) |source_map| append_empty: {
|
||||
const packed_map = PackedMap.newNonEmpty(source_map.chunk, source_map.escaped_source orelse break :append_empty);
|
||||
try g.current_chunk_source_maps.append(dev.allocator, .{
|
||||
.ref = packed_map,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Must precompute this. Otherwise, source maps won't have
|
||||
// the info needed to concatenate VLQ mappings.
|
||||
const count: u32 = @intCast(bun.strings.countChar(content.js.code, '\n'));
|
||||
try g.current_chunk_source_maps.append(dev.allocator, PackedMap.RefOrEmpty{
|
||||
.empty = .{
|
||||
.line_count = .init(count),
|
||||
// TODO: not sure if this is correct
|
||||
.html_bundle_route_index = .none,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1577,7 +1628,12 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
g.owner().graph_safety_lock.assertLocked();
|
||||
g.current_chunk_len = 0;
|
||||
g.current_chunk_parts.clearRetainingCapacity();
|
||||
if (side == .client) g.current_css_files.clearRetainingCapacity();
|
||||
if (side == .client) {
|
||||
g.current_css_files.clearRetainingCapacity();
|
||||
} else if (side == .server) {
|
||||
g.current_chunk_source_maps.clearRetainingCapacity();
|
||||
g.current_chunk_file_indices.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
|
||||
const TakeJSBundleOptions = switch (side) {
|
||||
@@ -1590,6 +1646,7 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
},
|
||||
.server => struct {
|
||||
kind: ChunkKind,
|
||||
script_id: SourceMapStore.Key,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1732,44 +1789,70 @@ pub fn IncrementalGraph(side: bake.Side) type {
|
||||
};
|
||||
|
||||
/// Uses `arena` as a temporary allocator, fills in all fields of `out` except ref_count
|
||||
pub fn takeSourceMap(g: *@This(), arena: std.mem.Allocator, gpa: Allocator, out: *SourceMapStore.Entry) bun.OOM!void {
|
||||
if (side == .server) @compileError("not implemented");
|
||||
|
||||
pub fn takeSourceMap(g: *@This(), _: std.mem.Allocator, gpa: Allocator, out: *SourceMapStore.Entry) bun.OOM!void {
|
||||
const paths = g.bundled_files.keys();
|
||||
const files = g.bundled_files.values();
|
||||
|
||||
// This buffer is temporary, holding the quoted source paths, joined with commas.
|
||||
var source_map_strings = std.ArrayList(u8).init(arena);
|
||||
defer source_map_strings.deinit();
|
||||
switch (side) {
|
||||
.client => {
|
||||
const files = g.bundled_files.values();
|
||||
|
||||
const buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(buf);
|
||||
const buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(buf);
|
||||
|
||||
var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len);
|
||||
errdefer file_paths.deinit(gpa);
|
||||
var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty;
|
||||
try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len);
|
||||
errdefer contained_maps.deinit(gpa);
|
||||
var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len);
|
||||
errdefer file_paths.deinit(gpa);
|
||||
var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty;
|
||||
try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len);
|
||||
errdefer contained_maps.deinit(gpa);
|
||||
|
||||
var overlapping_memory_cost: u32 = 0;
|
||||
var overlapping_memory_cost: u32 = 0;
|
||||
|
||||
for (g.current_chunk_parts.items) |file_index| {
|
||||
file_paths.appendAssumeCapacity(paths[file_index.get()]);
|
||||
const source_map = files[file_index.get()].sourceMap();
|
||||
contained_maps.appendAssumeCapacity(source_map.dupeRef());
|
||||
if (source_map == .ref) {
|
||||
overlapping_memory_cost += @intCast(source_map.ref.data.memoryCost());
|
||||
}
|
||||
for (g.current_chunk_parts.items) |file_index| {
|
||||
file_paths.appendAssumeCapacity(paths[file_index.get()]);
|
||||
const source_map = files[file_index.get()].sourceMap();
|
||||
contained_maps.appendAssumeCapacity(source_map.dupeRef());
|
||||
if (source_map == .ref) {
|
||||
overlapping_memory_cost += @intCast(source_map.ref.data.memoryCost());
|
||||
}
|
||||
}
|
||||
|
||||
overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items));
|
||||
|
||||
out.* = .{
|
||||
.ref_count = out.ref_count,
|
||||
.paths = file_paths.items,
|
||||
.files = contained_maps,
|
||||
.overlapping_memory_cost = overlapping_memory_cost,
|
||||
};
|
||||
},
|
||||
.server => {
|
||||
var file_paths = try ArrayListUnmanaged([]const u8).initCapacity(gpa, g.current_chunk_parts.items.len);
|
||||
errdefer file_paths.deinit(gpa);
|
||||
var contained_maps: bun.MultiArrayList(PackedMap.RefOrEmpty) = .empty;
|
||||
try contained_maps.ensureTotalCapacity(gpa, g.current_chunk_parts.items.len);
|
||||
errdefer contained_maps.deinit(gpa);
|
||||
|
||||
var overlapping_memory_cost: u32 = 0;
|
||||
|
||||
// For server, we use the tracked file indices to get the correct paths
|
||||
for (g.current_chunk_file_indices.items, g.current_chunk_source_maps.items) |file_index, source_map| {
|
||||
file_paths.appendAssumeCapacity(paths[file_index.get()]);
|
||||
contained_maps.appendAssumeCapacity(source_map.dupeRef());
|
||||
if (source_map == .ref) {
|
||||
overlapping_memory_cost += @intCast(source_map.ref.data.memoryCost());
|
||||
}
|
||||
}
|
||||
|
||||
overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items));
|
||||
|
||||
out.* = .{
|
||||
.ref_count = out.ref_count,
|
||||
.paths = file_paths.items,
|
||||
.files = contained_maps,
|
||||
.overlapping_memory_cost = overlapping_memory_cost,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
overlapping_memory_cost += @intCast(contained_maps.memoryCost() + DevServer.memoryCostSlice(file_paths.items));
|
||||
|
||||
out.* = .{
|
||||
.ref_count = out.ref_count,
|
||||
.paths = file_paths.items,
|
||||
.files = contained_maps,
|
||||
.overlapping_memory_cost = overlapping_memory_cost,
|
||||
};
|
||||
}
|
||||
|
||||
fn disconnectAndDeleteFile(g: *@This(), file_index: FileIndex) void {
|
||||
|
||||
@@ -75,11 +75,11 @@ pub const Entry = struct {
|
||||
pub fn renderMappings(map: Entry, kind: ChunkKind, arena: Allocator, gpa: Allocator) ![]u8 {
|
||||
var j: StringJoiner = .{ .allocator = arena };
|
||||
j.pushStatic("AAAA");
|
||||
try joinVLQ(&map, kind, &j, arena);
|
||||
try joinVLQ(&map, kind, &j, arena, .client);
|
||||
return j.done(gpa);
|
||||
}
|
||||
|
||||
pub fn renderJSON(map: *const Entry, dev: *DevServer, arena: Allocator, kind: ChunkKind, gpa: Allocator) ![]u8 {
|
||||
pub fn renderJSON(map: *const Entry, dev: *DevServer, arena: Allocator, kind: ChunkKind, gpa: Allocator, side: bake.Side) ![]u8 {
|
||||
const map_files = map.files.slice();
|
||||
const paths = map.paths;
|
||||
|
||||
@@ -105,13 +105,22 @@ pub const Entry = struct {
|
||||
|
||||
if (std.fs.path.isAbsolute(path)) {
|
||||
const is_windows_drive_path = Environment.isWindows and path[0] != '/';
|
||||
try source_map_strings.appendSlice(if (is_windows_drive_path)
|
||||
"\"file:///"
|
||||
else
|
||||
"\"file://");
|
||||
|
||||
// On the client we prefix the sourcemap path with "file://" and
|
||||
// percent encode it
|
||||
if (side == .client) {
|
||||
try source_map_strings.appendSlice(if (is_windows_drive_path)
|
||||
"\"file:///"
|
||||
else
|
||||
"\"file://");
|
||||
} else {
|
||||
try source_map_strings.append('"');
|
||||
}
|
||||
|
||||
if (Environment.isWindows and !is_windows_drive_path) {
|
||||
// UNC namespace -> file://server/share/path.ext
|
||||
bun.strings.percentEncodeWrite(
|
||||
encodeSourceMapPath(
|
||||
side,
|
||||
if (path.len > 2 and path[0] == '/' and path[1] == '/')
|
||||
path[2..]
|
||||
else
|
||||
@@ -126,7 +135,7 @@ pub const Entry = struct {
|
||||
// -> file:///path/to/file.js
|
||||
// windows drive letter paths have the extra slash added
|
||||
// -> file:///C:/path/to/file.js
|
||||
bun.strings.percentEncodeWrite(path, &source_map_strings) catch |err| switch (err) {
|
||||
encodeSourceMapPath(side, path, &source_map_strings) catch |err| switch (err) {
|
||||
error.IncompleteUTF8 => @panic("Unexpected: asset with incomplete UTF-8 as file path"),
|
||||
error.OutOfMemory => |e| return e,
|
||||
};
|
||||
@@ -174,14 +183,14 @@ pub const Entry = struct {
|
||||
j.pushStatic(
|
||||
\\],"names":[],"mappings":"AAAA
|
||||
);
|
||||
try joinVLQ(map, kind, &j, arena);
|
||||
try joinVLQ(map, kind, &j, arena, side);
|
||||
|
||||
const json_bytes = try j.doneWithEnd(gpa, "\"}");
|
||||
errdefer @compileError("last try should be the final alloc");
|
||||
|
||||
if (bun.FeatureFlags.bake_debugging_features) if (dev.dump_dir) |dump_dir| {
|
||||
const rel_path_escaped = "latest_chunk.js.map";
|
||||
dumpBundle(dump_dir, .client, rel_path_escaped, json_bytes, false) catch |err| {
|
||||
const rel_path_escaped = if (side == .client) "latest_chunk.js.map" else "latest_hmr.js.map";
|
||||
dumpBundle(dump_dir, if (side == .client) .client else .server, rel_path_escaped, json_bytes, false) catch |err| {
|
||||
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
||||
Output.warn("Could not dump bundle: {}", .{err});
|
||||
};
|
||||
@@ -190,13 +199,22 @@ pub const Entry = struct {
|
||||
return json_bytes;
|
||||
}
|
||||
|
||||
fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator) !void {
|
||||
const map_files = map.files.slice();
|
||||
fn encodeSourceMapPath(
|
||||
side: bake.Side,
|
||||
utf8_input: []const u8,
|
||||
writer: *std.ArrayList(u8),
|
||||
) error{ OutOfMemory, IncompleteUTF8 }!void {
|
||||
// On the client, percent encode everything so it works in the browser
|
||||
if (side == .client) {
|
||||
return bun.strings.percentEncodeWrite(utf8_input, writer);
|
||||
}
|
||||
|
||||
const runtime: bake.HmrRuntime = switch (kind) {
|
||||
.initial_response => bun.bake.getHmrRuntime(.client),
|
||||
.hmr_chunk => comptime .init("self[Symbol.for(\"bun:hmr\")]({\n"),
|
||||
};
|
||||
// On the server, we don't need to do anything
|
||||
try writer.appendSlice(utf8_input);
|
||||
}
|
||||
|
||||
fn joinVLQ(map: *const Entry, kind: ChunkKind, j: *StringJoiner, arena: Allocator, side: bake.Side) !void {
|
||||
const map_files = map.files.slice();
|
||||
|
||||
var prev_end_state: SourceMap.SourceMapState = .{
|
||||
.generated_line = 0,
|
||||
@@ -206,8 +224,20 @@ pub const Entry = struct {
|
||||
.original_column = 0,
|
||||
};
|
||||
|
||||
// +2 because the magic fairy in my dreams said it would align the source maps.
|
||||
var lines_between: u32 = runtime.line_count + 2;
|
||||
var lines_between: u32 = lines_between: {
|
||||
if (side == .client) {
|
||||
const runtime: bake.HmrRuntime = switch (kind) {
|
||||
.initial_response => bun.bake.getHmrRuntime(.client),
|
||||
.hmr_chunk => comptime .init("self[Symbol.for(\"bun:hmr\")]({\n"),
|
||||
};
|
||||
// +2 because the magic fairy in my dreams said it would align the source maps.
|
||||
// TODO: why the fuck is this 2?
|
||||
const lines_between: u32 = runtime.line_count + 2;
|
||||
break :lines_between lines_between;
|
||||
}
|
||||
|
||||
break :lines_between 0;
|
||||
};
|
||||
|
||||
// Join all of the mappings together.
|
||||
for (map_files.items(.tags), map_files.items(.data), 1..) |tag, chunk, source_index| switch (tag) {
|
||||
@@ -223,7 +253,7 @@ pub const Entry = struct {
|
||||
continue;
|
||||
},
|
||||
.ref => {
|
||||
const content = chunk.ref.data;
|
||||
const content: *PackedMap = chunk.ref.data;
|
||||
const start_state: SourceMap.SourceMapState = .{
|
||||
.source_index = @intCast(source_index),
|
||||
.generated_line = @intCast(lines_between),
|
||||
|
||||
17
src/bake/DevServerSourceProvider.cpp
Normal file
17
src/bake/DevServerSourceProvider.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "DevServerSourceProvider.h"
|
||||
#include "BunBuiltinNames.h"
|
||||
#include "BunString.h"
|
||||
|
||||
// The Zig implementation will be provided to handle registration
|
||||
extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, Bake::DevServerSourceProvider* opaque_source_provider, BunString* specifier);
|
||||
|
||||
// Export functions for Zig to access DevServerSourceProvider
|
||||
extern "C" BunString DevServerSourceProvider__getSourceSlice(Bake::DevServerSourceProvider* provider)
|
||||
{
|
||||
return Bun::toStringView(provider->source());
|
||||
}
|
||||
|
||||
extern "C" Bake::SourceMapData DevServerSourceProvider__getSourceMapJSON(Bake::DevServerSourceProvider* provider)
|
||||
{
|
||||
return provider->sourceMapJSON();
|
||||
}
|
||||
118
src/bake/DevServerSourceProvider.h
Normal file
118
src/bake/DevServerSourceProvider.h
Normal file
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
#include "root.h"
|
||||
#include "headers-handwritten.h"
|
||||
#include "JavaScriptCore/SourceOrigin.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include <mimalloc.h>
|
||||
|
||||
namespace Bake {
|
||||
|
||||
class DevServerSourceProvider;
|
||||
|
||||
class SourceMapJSONString {
|
||||
public:
|
||||
SourceMapJSONString(const char* ptr, size_t length)
|
||||
: m_ptr(ptr)
|
||||
, m_length(length)
|
||||
{
|
||||
}
|
||||
|
||||
~SourceMapJSONString()
|
||||
{
|
||||
if (m_ptr) {
|
||||
mi_free(const_cast<char*>(m_ptr));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete copy constructor and assignment operator to prevent double free
|
||||
SourceMapJSONString(const SourceMapJSONString&) = delete;
|
||||
SourceMapJSONString& operator=(const SourceMapJSONString&) = delete;
|
||||
|
||||
// Move constructor and assignment
|
||||
SourceMapJSONString(SourceMapJSONString&& other) noexcept
|
||||
: m_ptr(other.m_ptr)
|
||||
, m_length(other.m_length)
|
||||
{
|
||||
other.m_ptr = nullptr;
|
||||
other.m_length = 0;
|
||||
}
|
||||
|
||||
SourceMapJSONString& operator=(SourceMapJSONString&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
if (m_ptr) {
|
||||
mi_free(const_cast<char*>(m_ptr));
|
||||
}
|
||||
m_ptr = other.m_ptr;
|
||||
m_length = other.m_length;
|
||||
other.m_ptr = nullptr;
|
||||
other.m_length = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
const char* ptr() const { return m_ptr; }
|
||||
size_t length() const { return m_length; }
|
||||
|
||||
private:
|
||||
const char* m_ptr;
|
||||
size_t m_length;
|
||||
};
|
||||
|
||||
// Struct to return source map data to Zig
|
||||
struct SourceMapData {
|
||||
const char* ptr;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
// Function to be implemented in Zig to register the source provider
|
||||
extern "C" void Bun__addDevServerSourceProvider(void* bun_vm, DevServerSourceProvider* opaque_source_provider, BunString* specifier);
|
||||
|
||||
class DevServerSourceProvider final : public JSC::StringSourceProvider {
|
||||
public:
|
||||
static Ref<DevServerSourceProvider> create(
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
const String& source,
|
||||
const char* sourceMapJSONPtr,
|
||||
size_t sourceMapJSONLength,
|
||||
const JSC::SourceOrigin& sourceOrigin,
|
||||
String&& sourceURL,
|
||||
const TextPosition& startPosition,
|
||||
JSC::SourceProviderSourceType sourceType)
|
||||
{
|
||||
auto provider = adoptRef(*new DevServerSourceProvider(source, sourceMapJSONPtr, sourceMapJSONLength, sourceOrigin, WTFMove(sourceURL), startPosition, sourceType));
|
||||
auto* zigGlobalObject = jsCast<::Zig::GlobalObject*>(globalObject);
|
||||
auto specifier = Bun::toString(provider->sourceURL());
|
||||
Bun__addDevServerSourceProvider(zigGlobalObject->bunVM(), provider.ptr(), &specifier);
|
||||
return provider;
|
||||
}
|
||||
|
||||
SourceMapData sourceMapJSON() const
|
||||
{
|
||||
return SourceMapData { m_sourceMapJSON.ptr(), m_sourceMapJSON.length() };
|
||||
}
|
||||
|
||||
private:
|
||||
DevServerSourceProvider(
|
||||
const String& source,
|
||||
const char* sourceMapJSONPtr,
|
||||
size_t sourceMapJSONLength,
|
||||
const JSC::SourceOrigin& sourceOrigin,
|
||||
String&& sourceURL,
|
||||
const TextPosition& startPosition,
|
||||
JSC::SourceProviderSourceType sourceType)
|
||||
: StringSourceProvider(
|
||||
source,
|
||||
sourceOrigin,
|
||||
JSC::SourceTaintedOrigin::Untainted,
|
||||
WTFMove(sourceURL),
|
||||
startPosition,
|
||||
sourceType)
|
||||
, m_sourceMapJSON(sourceMapJSONPtr, sourceMapJSONLength)
|
||||
{
|
||||
}
|
||||
|
||||
SourceMapJSONString m_sourceMapJSON;
|
||||
};
|
||||
|
||||
} // namespace Bake
|
||||
@@ -3,6 +3,8 @@ import { renderToHtml, renderToStaticHtml } from "bun-framework-react/ssr.tsx" w
|
||||
import { serverManifest } from "bun:bake/server";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { renderToPipeableStream } from "react-server-dom-bun/server.node.unbundled.js";
|
||||
import type { AsyncLocalStorage } from "node:async_hooks";
|
||||
import type { RequestContext } from "../hmr-runtime-server";
|
||||
|
||||
function assertReactComponent(Component: any) {
|
||||
if (typeof Component !== "function") {
|
||||
@@ -12,8 +14,8 @@ function assertReactComponent(Component: any) {
|
||||
}
|
||||
|
||||
// This function converts the route information into a React component tree.
|
||||
function getPage(meta: Bake.RouteMetadata, styles: readonly string[]) {
|
||||
let route = component(meta.pageModule, meta.params);
|
||||
function getPage(meta: Bake.RouteMetadata & { request?: Request }, styles: readonly string[]) {
|
||||
let route = component(meta.pageModule, meta.params, meta.request);
|
||||
for (const layout of meta.layouts) {
|
||||
const Layout = layout.default;
|
||||
if (import.meta.env.DEV) assertReactComponent(Layout);
|
||||
@@ -35,7 +37,7 @@ function getPage(meta: Bake.RouteMetadata, styles: readonly string[]) {
|
||||
);
|
||||
}
|
||||
|
||||
function component(mod: any, params: Record<string, string> | null) {
|
||||
function component(mod: any, params: Record<string, string> | null, request?: Request) {
|
||||
const Page = mod.default;
|
||||
let props = {};
|
||||
if (import.meta.env.DEV) assertReactComponent(Page);
|
||||
@@ -49,12 +51,21 @@ function component(mod: any, params: Record<string, string> | null) {
|
||||
props = method();
|
||||
}
|
||||
|
||||
// Pass request prop if mode is 'ssr'
|
||||
if (mod.mode === "ssr" && request) {
|
||||
props.request = request;
|
||||
}
|
||||
|
||||
return <Page params={params} {...props} />;
|
||||
}
|
||||
|
||||
// `server.tsx` exports a function to be used for handling user routes. It takes
|
||||
// in the Request object, the route's module, and extra route metadata.
|
||||
export async function render(request: Request, meta: Bake.RouteMetadata): Promise<Response> {
|
||||
// in the Request object, the route's module, extra route metadata, and the AsyncLocalStorage instance.
|
||||
export async function render(
|
||||
request: Request,
|
||||
meta: Bake.RouteMetadata,
|
||||
als?: AsyncLocalStorage<RequestContext>,
|
||||
): Promise<Response> {
|
||||
// The framework generally has two rendering modes.
|
||||
// - Standard browser navigation
|
||||
// - Client-side navigation
|
||||
@@ -64,6 +75,9 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis
|
||||
// This is signaled by `client.tsx` via the `Accept` header.
|
||||
const skipSSR = request.headers.get("Accept")?.includes("text/x-component");
|
||||
|
||||
// Check if the page module has a streaming export, default to false
|
||||
const streaming = meta.pageModule.streaming ?? false;
|
||||
|
||||
// Do not render <link> tags if the request is skipping SSR.
|
||||
const page = getPage(meta, skipSSR ? [] : meta.styles);
|
||||
|
||||
@@ -83,34 +97,119 @@ export async function render(request: Request, meta: Bake.RouteMetadata): Promis
|
||||
|
||||
// This renders Server Components to a ReadableStream "RSC Payload"
|
||||
let pipe;
|
||||
const signal: MiniAbortSignal = { aborted: false, abort: null! };
|
||||
const signal: MiniAbortSignal = { aborted: undefined, abort: null! };
|
||||
({ pipe, abort: signal.abort } = renderToPipeableStream(page, serverManifest, {
|
||||
onError: err => {
|
||||
// console.error("onError renderToPipeableStream", !!signal.aborted);
|
||||
if (signal.aborted) return;
|
||||
console.error(err);
|
||||
|
||||
// Mark as aborted and call the abort function
|
||||
signal.aborted = err;
|
||||
// @ts-expect-error
|
||||
signal.abort(err);
|
||||
rscPayload.destroy(err);
|
||||
},
|
||||
filterStackFrame: () => false,
|
||||
}));
|
||||
pipe(rscPayload);
|
||||
|
||||
rscPayload.on("error", err => {
|
||||
if (signal.aborted) return;
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if (skipSSR) {
|
||||
const responseOptions = als?.getStore()?.responseOptions || {};
|
||||
return new Response(rscPayload as any, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/x-component" },
|
||||
...responseOptions,
|
||||
});
|
||||
}
|
||||
|
||||
// The RSC payload is rendered into HTML
|
||||
return new Response(await renderToHtml(rscPayload, meta.modules, signal), {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf8",
|
||||
},
|
||||
});
|
||||
if (streaming) {
|
||||
const responseOptions = als?.getStore()?.responseOptions || {};
|
||||
if (als) {
|
||||
const state = als.getStore();
|
||||
if (state) state.streamingStarted = true;
|
||||
}
|
||||
// Stream the response as before
|
||||
return new Response(renderToHtml(rscPayload, meta.modules, signal), {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf8",
|
||||
},
|
||||
...responseOptions,
|
||||
});
|
||||
} else {
|
||||
// FIXME: this is bad and could be done way better
|
||||
// FIXME: why are we even doing stream stuff is `streaming=false`, is there a way to do RSC without stream
|
||||
|
||||
// Set up the render abort handler for non-streaming mode
|
||||
if (als) {
|
||||
const store = als.getStore();
|
||||
if (store) {
|
||||
store.renderAbort = (path: string, params: Record<string, any> | null) => {
|
||||
// Create the abort error
|
||||
const abortError = new (globalThis as any).RenderAbortError(path, params);
|
||||
// Abort the current render
|
||||
signal.aborted = abortError;
|
||||
signal.abort(abortError);
|
||||
rscPayload.destroy(abortError);
|
||||
throw abortError;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer the entire response and return it all at once
|
||||
const htmlStream = renderToHtml(rscPayload, meta.modules, signal);
|
||||
const chunks: Uint8Array[] = [];
|
||||
const reader = htmlStream.getReader();
|
||||
|
||||
try {
|
||||
let keepGoing = true;
|
||||
do {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
// Check if the render was aborted with an error
|
||||
if (signal.aborted) {
|
||||
// If it's a RenderAbortError, re-throw it to be handled upstream
|
||||
if (signal.aborted instanceof (globalThis as any).RenderAbortError) {
|
||||
throw signal.aborted;
|
||||
}
|
||||
// For some reason in react-server-dom the `stream.on("error")`
|
||||
// handler creates a new Error???
|
||||
if (signal.aborted.message !== "Connection closed.") {
|
||||
// For other errors, we can handle them here or re-throw
|
||||
throw signal.aborted;
|
||||
}
|
||||
}
|
||||
|
||||
keepGoing = !done;
|
||||
if (!done) {
|
||||
chunks.push(value);
|
||||
}
|
||||
} while (keepGoing);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
// Combine all chunks into a single response
|
||||
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
const opts = als?.getStore()?.responseOptions ?? { headers: {} };
|
||||
const { headers, ...response_options } = opts;
|
||||
|
||||
return new Response(result, {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf8",
|
||||
"Set-Cookie": request.cookies.toSetCookieHeaders(),
|
||||
...headers,
|
||||
},
|
||||
...response_options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When a production build is performed, pre-rendering is invoked here. If this
|
||||
@@ -128,7 +227,13 @@ export async function prerender(meta: Bake.RouteMetadata) {
|
||||
let rscChunks: Array<BlobPart> = [int.buffer as ArrayBuffer, meta.styles.join("\n")];
|
||||
rscPayload.on("data", chunk => rscChunks.push(chunk));
|
||||
|
||||
const html = await renderToStaticHtml(rscPayload, meta.modules);
|
||||
let html;
|
||||
try {
|
||||
html = await renderToStaticHtml(rscPayload, meta.modules);
|
||||
} catch (err) {
|
||||
//console.error("ah fuck");
|
||||
return undefined;
|
||||
}
|
||||
const rsc = new Blob(rscChunks, { type: "text/x-component" });
|
||||
|
||||
return {
|
||||
@@ -184,7 +289,7 @@ export const contentTypeToStaticFile = {
|
||||
|
||||
/** Instead of using AbortController, this is used */
|
||||
export interface MiniAbortSignal {
|
||||
aborted: boolean;
|
||||
aborted: Error | undefined;
|
||||
/** Caller must set `aborted` to true before calling. */
|
||||
abort: () => void;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ export function renderToHtml(
|
||||
// with `use`, and then returning the parsed React component for the UI.
|
||||
const Root: any = () => React.use(promise);
|
||||
|
||||
// If the signal is already aborted, we should not proceed
|
||||
if (signal.aborted) {
|
||||
controller.close(signal.aborted);
|
||||
return Promise.reject(signal.aborted);
|
||||
}
|
||||
|
||||
// `renderToPipeableStream` is what actually generates HTML.
|
||||
// Here is where React is told what script tags to inject.
|
||||
let pipe: (stream: any) => void;
|
||||
@@ -63,7 +69,13 @@ export function renderToHtml(
|
||||
bootstrapModules,
|
||||
onError(error) {
|
||||
if (!signal.aborted) {
|
||||
console.error(error);
|
||||
// Abort the rendering and close the stream
|
||||
signal.aborted = error;
|
||||
abort();
|
||||
if (signal.abort) signal.abort();
|
||||
if (stream) {
|
||||
stream.controller.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -74,10 +86,12 @@ export function renderToHtml(
|
||||
// Promise resolved after all data is combined.
|
||||
return stream.finished;
|
||||
},
|
||||
cancel() {
|
||||
signal.aborted = true;
|
||||
signal.abort();
|
||||
abort?.();
|
||||
cancel(err) {
|
||||
if (!signal.aborted) {
|
||||
signal.aborted = err;
|
||||
signal.abort(err);
|
||||
}
|
||||
abort?.(err);
|
||||
},
|
||||
} as Bun.DirectUnderlyingSource as any);
|
||||
}
|
||||
@@ -133,19 +147,28 @@ class RscInjectionStream extends EventEmitter {
|
||||
/** Resolved when all data is written */
|
||||
finished: Promise<void>;
|
||||
finalize: () => void;
|
||||
reject: (err: any) => void;
|
||||
|
||||
constructor(rscPayload: Readable, controller: ReadableStreamDirectController) {
|
||||
super();
|
||||
this.controller = controller;
|
||||
|
||||
const { resolve, promise } = Promise.withResolvers<void>();
|
||||
const { resolve, promise, reject } = Promise.withResolvers<void>();
|
||||
this.finished = promise;
|
||||
this.finalize = resolve;
|
||||
this.reject = reject;
|
||||
|
||||
rscPayload.on("data", this.writeRscData.bind(this));
|
||||
rscPayload.on("end", () => {
|
||||
this.rscHasEnded = true;
|
||||
});
|
||||
rscPayload.on("error", err => {
|
||||
this.rscHasEnded = true;
|
||||
// Close the controller
|
||||
controller.close();
|
||||
// Reject the promise instead of resolving it
|
||||
this.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
write(data: Uint8Array) {
|
||||
@@ -284,7 +307,8 @@ class StaticRscInjectionStream extends EventEmitter {
|
||||
}
|
||||
|
||||
destroy(error) {
|
||||
console.error(error);
|
||||
// We don't need to console.error here as react does it itself
|
||||
// console.error(error);
|
||||
this.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,11 @@ export class HMRModule {
|
||||
: null;
|
||||
}
|
||||
|
||||
// Module Ids are pre-resolved by the bundler
|
||||
requireResolve(id: Id): Id {
|
||||
return id;
|
||||
}
|
||||
|
||||
require(id: Id) {
|
||||
try {
|
||||
const mod = loadModuleSync(id, true, this);
|
||||
|
||||
@@ -3,11 +3,48 @@
|
||||
import type { Bake } from "bun";
|
||||
import "./debug";
|
||||
import { loadExports, replaceModules, serverManifest, ssrManifest } from "./hmr-module";
|
||||
// import { AsyncLocalStorage } from "node:async_hooks";
|
||||
const { AsyncLocalStorage } = require("node:async_hooks");
|
||||
|
||||
if (typeof IS_BUN_DEVELOPMENT !== "boolean") {
|
||||
throw new Error("DCE is configured incorrectly");
|
||||
}
|
||||
|
||||
export type RequestContext = {
|
||||
responseOptions: ResponseInit;
|
||||
streamingStarted?: boolean;
|
||||
renderAbort?: (path: string, params: Record<string, any> | null) => never;
|
||||
};
|
||||
|
||||
// Create the AsyncLocalStorage instance for propagating response options
|
||||
const responseOptionsALS = new AsyncLocalStorage();
|
||||
|
||||
/// Created when the user does `return Response.render(...)`
|
||||
class RenderAbortError extends Error {
|
||||
constructor(
|
||||
public path: string,
|
||||
public params: Record<string, any> | null,
|
||||
public response: Response,
|
||||
) {
|
||||
super("Response.render() called");
|
||||
this.name = "RenderAbortError";
|
||||
}
|
||||
}
|
||||
|
||||
/// Created when the user does `return Response.redirect(...)`
|
||||
class RedirectAbortError extends Error {
|
||||
constructor(
|
||||
public response: Response,
|
||||
) {
|
||||
super("Response.redirect() called");
|
||||
this.name = "RedirectAbortError";
|
||||
}
|
||||
}
|
||||
|
||||
// Make RenderAbortError and RedirectAbortError globally available for other modules
|
||||
(globalThis as any).RenderAbortError = RenderAbortError;
|
||||
(globalThis as any).RedirectAbortError = RedirectAbortError;
|
||||
|
||||
interface Exports {
|
||||
handleRequest: (
|
||||
req: Request,
|
||||
@@ -16,6 +53,8 @@ interface Exports {
|
||||
clientEntryUrl: string,
|
||||
styles: string[],
|
||||
params: Record<string, string> | null,
|
||||
setAsyncLocalStorage: Function,
|
||||
getAsyncLocalStorage: Function,
|
||||
) => any;
|
||||
registerUpdate: (
|
||||
modules: any,
|
||||
@@ -26,7 +65,20 @@ interface Exports {
|
||||
|
||||
declare let server_exports: Exports;
|
||||
server_exports = {
|
||||
async handleRequest(req, routerTypeMain, routeModules, clientEntryUrl, styles, params) {
|
||||
async handleRequest(
|
||||
req,
|
||||
routerTypeMain,
|
||||
routeModules,
|
||||
clientEntryUrl,
|
||||
styles,
|
||||
params,
|
||||
setAsyncLocalStorage,
|
||||
getAsyncLocalStorage,
|
||||
) {
|
||||
// FIXME: We should only have to do this once
|
||||
// Set the AsyncLocalStorage instance in the VM
|
||||
setAsyncLocalStorage(responseOptionsALS);
|
||||
|
||||
if (IS_BUN_DEVELOPMENT && process.env.BUN_DEBUG_BAKE_JS) {
|
||||
console.log("handleRequest", {
|
||||
routeModules,
|
||||
@@ -48,20 +100,67 @@ server_exports = {
|
||||
}
|
||||
|
||||
const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports));
|
||||
const response = await serverRenderer(req, {
|
||||
styles: styles,
|
||||
modules: [clientEntryUrl],
|
||||
layouts,
|
||||
pageModule,
|
||||
modulepreload: [],
|
||||
params,
|
||||
});
|
||||
|
||||
if (!(response instanceof Response)) {
|
||||
throw new Error(`Server-side request handler was expected to return a Response object.`);
|
||||
// Add cookies to request when mode is 'ssr'
|
||||
let requestWithCookies = req;
|
||||
if (pageModule.mode === "ssr") {
|
||||
requestWithCookies.cookies = req.cookies || new Bun.CookieMap(req.headers.get("Cookie") || "");
|
||||
}
|
||||
|
||||
return response;
|
||||
let storeValue: RequestContext = {
|
||||
responseOptions: {},
|
||||
};
|
||||
|
||||
try {
|
||||
// Run the renderer inside the AsyncLocalStorage context
|
||||
// This allows Response constructors to access the stored options
|
||||
const response = await responseOptionsALS.run(storeValue, async () => {
|
||||
return await serverRenderer(
|
||||
requestWithCookies,
|
||||
{
|
||||
styles: styles,
|
||||
modules: [clientEntryUrl],
|
||||
layouts,
|
||||
pageModule,
|
||||
modulepreload: [],
|
||||
params,
|
||||
// Pass request in metadata when mode is 'ssr'
|
||||
request: pageModule.mode === "ssr" ? requestWithCookies : undefined,
|
||||
},
|
||||
responseOptionsALS,
|
||||
);
|
||||
});
|
||||
|
||||
if (!(response instanceof Response)) {
|
||||
throw new Error(`Server-side request handler was expected to return a Response object.`);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Handle Response.render() aborts
|
||||
if (error instanceof RenderAbortError) {
|
||||
// TODO: Implement route resolution to get the new route modules
|
||||
// For now, we'll need to get this information from the native side
|
||||
// The native code will need to resolve the route and call handleRequest again
|
||||
|
||||
// Store the render error info so native code can access it
|
||||
(globalThis as any).__lastRenderAbort = {
|
||||
path: error.path,
|
||||
params: error.params,
|
||||
};
|
||||
|
||||
// return it so the Zig code can handle it and re-render new route
|
||||
return error.response;
|
||||
}
|
||||
|
||||
// Handle Response.redirect() aborts
|
||||
if (error instanceof RedirectAbortError) {
|
||||
// Return the redirect response directly
|
||||
return error.response;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async registerUpdate(modules, componentManifestAdd, componentManifestDelete) {
|
||||
replaceModules(modules);
|
||||
|
||||
@@ -184,11 +184,33 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
const default = BakeGetDefaultExportFromModule(vm.global, config_entry_point_string.toJS(vm.global));
|
||||
|
||||
if (!default.isObject()) {
|
||||
Output.panic("TODO: print this error better, default export is not an object", .{});
|
||||
return global.throwInvalidArguments(
|
||||
\\Your config file's default export must be an object.
|
||||
\\
|
||||
\\Example:
|
||||
\\ export default {
|
||||
\\ app: {
|
||||
\\ framework: "react",
|
||||
\\ }
|
||||
\\ }
|
||||
\\
|
||||
\\Learn more at https://bun.com/docs/ssg
|
||||
, .{});
|
||||
}
|
||||
|
||||
const app = try default.get(vm.global, "app") orelse {
|
||||
Output.panic("TODO: print this error better, default export needs an 'app' object", .{});
|
||||
return global.throwInvalidArguments(
|
||||
\\Your config file's default export must contain an "app" property.
|
||||
\\
|
||||
\\Example:
|
||||
\\ export default {
|
||||
\\ app: {
|
||||
\\ framework: "react",
|
||||
\\ }
|
||||
\\ }
|
||||
\\
|
||||
\\Learn more at https://bun.com/docs/ssg
|
||||
, .{});
|
||||
};
|
||||
|
||||
break :config try bake.UserOptions.fromJS(app, vm.global);
|
||||
@@ -409,7 +431,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
},
|
||||
.asset => {},
|
||||
.bytecode => {},
|
||||
.sourcemap => @panic("TODO: register source map"),
|
||||
.sourcemap => {},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ pub const Value = bun.TaggedPointerUnion(.{
|
||||
SavedMappings,
|
||||
SourceProviderMap,
|
||||
BakeSourceProvider,
|
||||
DevServerSourceProvider,
|
||||
});
|
||||
|
||||
pub const MissingSourceMapNoteInfo = struct {
|
||||
@@ -108,6 +109,10 @@ pub fn putBakeSourceProvider(this: *SavedSourceMap, opaque_source_provider: *Bak
|
||||
this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
pub fn putDevServerSourceProvider(this: *SavedSourceMap, opaque_source_provider: *DevServerSourceProvider, path: []const u8) void {
|
||||
this.putValue(path, Value.init(opaque_source_provider)) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
pub fn putZigSourceProvider(this: *SavedSourceMap, opaque_source_provider: *anyopaque, path: []const u8) void {
|
||||
const source_provider: *SourceProviderMap = @ptrCast(opaque_source_provider);
|
||||
this.putValue(path, Value.init(source_provider)) catch bun.outOfMemory();
|
||||
@@ -279,6 +284,33 @@ fn getWithContent(
|
||||
MissingSourceMapNoteInfo.path = storage;
|
||||
return .{};
|
||||
},
|
||||
@field(Value.Tag, @typeName(DevServerSourceProvider)) => {
|
||||
// TODO: This is a copy-paste of above branch
|
||||
const ptr: *DevServerSourceProvider = Value.from(mapping.value_ptr.*).as(DevServerSourceProvider);
|
||||
this.unlock();
|
||||
|
||||
// Do not lock the mutex while we're parsing JSON!
|
||||
if (ptr.getSourceMap(path, .none, hint)) |parse| {
|
||||
if (parse.map) |map| {
|
||||
map.ref();
|
||||
// The mutex is not locked. We have to check the hash table again.
|
||||
this.putValue(path, Value.init(map)) catch bun.outOfMemory();
|
||||
|
||||
return parse;
|
||||
}
|
||||
}
|
||||
|
||||
this.lock();
|
||||
defer this.unlock();
|
||||
// does not have a valid source map. let's not try again
|
||||
_ = this.map.remove(hash);
|
||||
|
||||
// Store path for a user note.
|
||||
const storage = MissingSourceMapNoteInfo.storage[0..path.len];
|
||||
@memcpy(storage, path);
|
||||
MissingSourceMapNoteInfo.path = storage;
|
||||
return .{};
|
||||
},
|
||||
else => {
|
||||
if (Environment.allow_assert) {
|
||||
@panic("Corrupt pointer tag");
|
||||
@@ -333,5 +365,6 @@ const logger = bun.logger;
|
||||
|
||||
const SourceMap = bun.sourcemap;
|
||||
const BakeSourceProvider = bun.sourcemap.BakeSourceProvider;
|
||||
const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider;
|
||||
const ParsedSourceMap = SourceMap.ParsedSourceMap;
|
||||
const SourceProviderMap = SourceMap.SourceProviderMap;
|
||||
|
||||
@@ -196,6 +196,58 @@ has_mutated_built_in_extensions: u32 = 0,
|
||||
|
||||
initial_script_execution_context_identifier: i32,
|
||||
|
||||
dev_server_async_local_storage: jsc.Strong.Optional = .empty,
|
||||
|
||||
pub fn setAllowJSXInResponseConstructor(this: *VirtualMachine, value: bool) void {
|
||||
// When enabled, we create an AsyncLocalStorage instance
|
||||
// When disabled, we clear it
|
||||
if (value) {
|
||||
// We'll set this from JavaScript when we create the AsyncLocalStorage instance
|
||||
// For now, just keep track of the flag internally
|
||||
} else {
|
||||
this.dev_server_async_local_storage.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allowJSXInResponseConstructor(this: *VirtualMachine) bool {
|
||||
// Check if the AsyncLocalStorage instance exists
|
||||
return this.dev_server_async_local_storage.has();
|
||||
}
|
||||
|
||||
pub fn getDevServerAsyncLocalStorage(this: *VirtualMachine) ?jsc.JSValue {
|
||||
return this.dev_server_async_local_storage.get();
|
||||
}
|
||||
|
||||
pub fn setDevServerAsyncLocalStorage(this: *VirtualMachine, global: *jsc.JSGlobalObject, value: jsc.JSValue) void {
|
||||
if (value == .zero) {
|
||||
this.dev_server_async_local_storage.deinit();
|
||||
} else if (this.dev_server_async_local_storage.has()) {
|
||||
this.dev_server_async_local_storage.set(global, value);
|
||||
} else {
|
||||
this.dev_server_async_local_storage = jsc.Strong.Optional.create(value, global);
|
||||
}
|
||||
}
|
||||
|
||||
// JavaScript binding to set the AsyncLocalStorage instance
|
||||
pub export fn VirtualMachine__setDevServerAsyncLocalStorage(global: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) jsc.JSValue {
|
||||
const arguments = callframe.arguments_old(1).slice();
|
||||
const vm = global.bunVM();
|
||||
|
||||
if (arguments.len < 1) {
|
||||
_ = global.throwInvalidArguments("setDevServerAsyncLocalStorage expects 1 argument", .{}) catch {};
|
||||
return .zero;
|
||||
}
|
||||
|
||||
vm.setDevServerAsyncLocalStorage(global, arguments[0]);
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
// JavaScript binding to get the AsyncLocalStorage instance
|
||||
pub export fn VirtualMachine__getDevServerAsyncLocalStorage(global: *jsc.JSGlobalObject, _: *jsc.CallFrame) jsc.JSValue {
|
||||
const vm = global.bunVM();
|
||||
return vm.getDevServerAsyncLocalStorage() orelse .js_undefined;
|
||||
}
|
||||
|
||||
pub const ProcessAutoKiller = @import("./ProcessAutoKiller.zig");
|
||||
pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSGlobalObject, JSValue) void;
|
||||
|
||||
|
||||
@@ -1040,7 +1040,7 @@ pub fn serve(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.J
|
||||
&config,
|
||||
&args,
|
||||
.{
|
||||
.allow_bake_config = bun.FeatureFlags.bake() and callframe.isFromBunMain(globalObject.vm()),
|
||||
.allow_bake_config = bun.FeatureFlags.bake(),
|
||||
.is_fetch_required = true,
|
||||
.has_user_routes = false,
|
||||
},
|
||||
|
||||
@@ -297,6 +297,10 @@ pub const Stdio = union(enum) {
|
||||
},
|
||||
|
||||
.Blob, .WTFStringImpl, .InternalBlob => unreachable, // handled above.
|
||||
.Render => {
|
||||
// Render bodies should not be used as stdio
|
||||
return globalThis.throwInvalidArguments("Response.render() result cannot be used as stdio", .{});
|
||||
},
|
||||
.Locked => {
|
||||
if (is_sync) {
|
||||
return globalThis.throwInvalidArguments("ReadableStream cannot be used in sync mode", .{});
|
||||
|
||||
@@ -2086,7 +2086,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
this.handleRequest(&should_deinit_context, prepared, req, response_value);
|
||||
}
|
||||
|
||||
pub fn onRequestFromSaved(
|
||||
pub fn onSavedRequest(
|
||||
this: *ThisServer,
|
||||
req: SavedRequest.Union,
|
||||
resp: *App.Response,
|
||||
@@ -2874,7 +2874,17 @@ pub const SavedRequest = struct {
|
||||
}
|
||||
|
||||
pub const Union = union(enum) {
|
||||
/// Direct pointer to a µWebSockets request that is still on the stack.
|
||||
/// Used for synchronous request handling where the request can be processed
|
||||
/// immediately within the current call frame. This avoids heap allocation
|
||||
/// and is more efficient for simple, fast operations.
|
||||
stack: *uws.Request,
|
||||
|
||||
/// A heap-allocated copy of the request data that persists beyond the
|
||||
/// initial request handler. Used when request processing needs to be
|
||||
/// deferred (e.g., async operations, waiting for framework initialization).
|
||||
/// Contains strong references to JavaScript objects and all context needed
|
||||
/// to complete the request later.
|
||||
saved: bun.jsc.API.SavedRequest,
|
||||
};
|
||||
};
|
||||
@@ -3147,7 +3157,7 @@ pub const AnyServer = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onRequestFromSaved(
|
||||
pub fn onSavedRequest(
|
||||
this: AnyServer,
|
||||
req: SavedRequest.Union,
|
||||
resp: uws.AnyResponse,
|
||||
@@ -3156,10 +3166,10 @@ pub const AnyServer = struct {
|
||||
extra_args: [extra_arg_count]JSValue,
|
||||
) void {
|
||||
return switch (this.ptr.tag()) {
|
||||
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onRequestFromSaved(req, resp.TCP, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onRequestFromSaved(req, resp.SSL, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onRequestFromSaved(req, resp.TCP, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onRequestFromSaved(req, resp.SSL, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(HTTPServer) => this.ptr.as(HTTPServer).onSavedRequest(req, resp.TCP, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(HTTPSServer) => this.ptr.as(HTTPSServer).onSavedRequest(req, resp.SSL, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(DebugHTTPServer) => this.ptr.as(DebugHTTPServer).onSavedRequest(req, resp.TCP, callback, extra_arg_count, extra_args),
|
||||
Ptr.case(DebugHTTPSServer) => this.ptr.as(DebugHTTPSServer).onSavedRequest(req, resp.SSL, callback, extra_arg_count, extra_args),
|
||||
else => bun.unreachablePanic("Invalid pointer tag", .{}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,32 @@ pub fn init(request_ctx: anytype) AnyRequestContext {
|
||||
return .{ .tagged_pointer = Pointer.init(request_ctx) };
|
||||
}
|
||||
|
||||
pub fn setAbortCallback(self: AnyRequestContext, cb: *const fn (this: *anyopaque) void, data: *anyopaque) void {
|
||||
if (self.tagged_pointer.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (self.tagged_pointer.tag()) {
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => {
|
||||
self.tagged_pointer.as(HTTPServer.RequestContext).onAbortCb = cb;
|
||||
self.tagged_pointer.as(HTTPServer.RequestContext).onAbortData = data;
|
||||
},
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => {
|
||||
self.tagged_pointer.as(HTTPSServer.RequestContext).onAbortCb = cb;
|
||||
self.tagged_pointer.as(HTTPSServer.RequestContext).onAbortData = data;
|
||||
},
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => {
|
||||
self.tagged_pointer.as(DebugHTTPServer.RequestContext).onAbortCb = cb;
|
||||
self.tagged_pointer.as(DebugHTTPServer.RequestContext).onAbortData = data;
|
||||
},
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => {
|
||||
self.tagged_pointer.as(DebugHTTPSServer.RequestContext).onAbortCb = cb;
|
||||
self.tagged_pointer.as(DebugHTTPSServer.RequestContext).onAbortData = data;
|
||||
},
|
||||
else => @panic("Unexpected AnyRequestContext tag"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memoryCost(self: AnyRequestContext) usize {
|
||||
if (self.tagged_pointer.isNull()) {
|
||||
return 0;
|
||||
|
||||
@@ -55,6 +55,9 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
/// Defer finalization until after the request handler task is completed?
|
||||
defer_deinit_until_callback_completes: ?*bool = null,
|
||||
|
||||
onAbortCb: ?*const fn (this: *anyopaque) void = null,
|
||||
onAbortData: ?*anyopaque = null,
|
||||
|
||||
// TODO: support builtin compression
|
||||
const can_sendfile = !ssl_enabled and !Environment.isWindows;
|
||||
|
||||
@@ -183,7 +186,23 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
// 5 - is the only reference of the context
|
||||
// 6 - is not waiting for request body
|
||||
// 7 - did not call sendfile
|
||||
return this.resp != null and !this.flags.aborted and !this.flags.has_marked_complete and !this.flags.has_marked_pending and this.ref_count == 1 and !this.flags.is_waiting_for_request_body and !this.flags.has_sendfile_ctx;
|
||||
ctxLog("RequestContext(0x{x}).shouldRenderMissing {s} {s} {s} {s} {s} {s} {s}", .{
|
||||
@intFromPtr(this),
|
||||
if (this.resp != null) "has response" else "no response",
|
||||
if (this.flags.aborted) "aborted" else "not aborted",
|
||||
if (this.flags.has_marked_complete) "marked complete" else "not marked complete",
|
||||
if (this.flags.has_marked_pending) "marked pending" else "not marked pending",
|
||||
if (this.ref_count == 1) "only reference" else "not only reference",
|
||||
if (this.flags.is_waiting_for_request_body) "waiting for request body" else "not waiting for request body",
|
||||
if (this.flags.has_sendfile_ctx) "has sendfile context" else "no sendfile context",
|
||||
});
|
||||
return this.resp != null and
|
||||
!this.flags.aborted and
|
||||
!this.flags.has_marked_complete and
|
||||
!this.flags.has_marked_pending and
|
||||
this.ref_count == 1 and
|
||||
!this.flags.is_waiting_for_request_body and
|
||||
!this.flags.has_sendfile_ctx;
|
||||
}
|
||||
|
||||
pub fn isDeadRequest(this: *RequestContext) bool {
|
||||
@@ -201,6 +220,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
|
||||
/// destroy RequestContext, should be only called by deref or if defer_deinit_until_callback_completes is ref is set to true
|
||||
pub fn deinit(this: *RequestContext) void {
|
||||
ctxLog("deinit", .{});
|
||||
this.detachResponse();
|
||||
this.endRequestStreamingAndDrain();
|
||||
// TODO: has_marked_complete is doing something?
|
||||
@@ -337,6 +357,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
|
||||
pub fn renderDefaultError(
|
||||
this: *RequestContext,
|
||||
arena_allocator: std.mem.Allocator,
|
||||
log: *logger.Log,
|
||||
err: anyerror,
|
||||
exceptions: []Api.JsException,
|
||||
@@ -351,12 +372,10 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
}
|
||||
}
|
||||
|
||||
const allocator = this.allocator;
|
||||
|
||||
const fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable;
|
||||
defer allocator.destroy(fallback_container);
|
||||
const fallback_container = arena_allocator.create(Api.FallbackMessageContainer) catch unreachable;
|
||||
defer arena_allocator.destroy(fallback_container);
|
||||
fallback_container.* = Api.FallbackMessageContainer{
|
||||
.message = std.fmt.allocPrint(allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable,
|
||||
.message = std.fmt.allocPrint(arena_allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable,
|
||||
.router = null,
|
||||
.reason = .fetch_event_handler,
|
||||
.cwd = VirtualMachine.get().transpiler.fs.top_level_dir,
|
||||
@@ -364,18 +383,19 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
.code = @as(u16, @truncate(@intFromError(err))),
|
||||
.name = @errorName(err),
|
||||
.exceptions = exceptions,
|
||||
.build = log.toAPI(allocator) catch unreachable,
|
||||
.build = log.toAPI(arena_allocator) catch unreachable,
|
||||
},
|
||||
};
|
||||
|
||||
if (comptime fmt.len > 0) Output.prettyErrorln(fmt, args);
|
||||
Output.flush();
|
||||
|
||||
var bb = std.ArrayList(u8).init(allocator);
|
||||
// Explicitly use `this.allocator` and *not* the arena
|
||||
var bb = std.ArrayList(u8).init(this.allocator);
|
||||
const bb_writer = bb.writer();
|
||||
|
||||
Fallback.renderBackend(
|
||||
allocator,
|
||||
arena_allocator,
|
||||
fallback_container,
|
||||
@TypeOf(bb_writer),
|
||||
bb_writer,
|
||||
@@ -436,6 +456,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
}
|
||||
|
||||
pub fn end(this: *RequestContext, data: []const u8, closeConnection: bool) void {
|
||||
ctxLog("end", .{});
|
||||
if (this.resp) |resp| {
|
||||
defer this.deref();
|
||||
|
||||
@@ -461,6 +482,7 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
}
|
||||
|
||||
pub fn endWithoutBody(this: *RequestContext, closeConnection: bool) void {
|
||||
ctxLog("endWithoutBody", .{});
|
||||
if (this.resp) |resp| {
|
||||
defer this.deref();
|
||||
|
||||
@@ -556,11 +578,15 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
}
|
||||
|
||||
pub fn onAbort(this: *RequestContext, resp: *App.Response) void {
|
||||
ctxLog("onAbort", .{});
|
||||
assert(this.resp == resp);
|
||||
assert(!this.flags.aborted);
|
||||
assert(this.server != null);
|
||||
// mark request as aborted
|
||||
this.flags.aborted = true;
|
||||
if (this.onAbortData != null and this.onAbortCb != null) {
|
||||
this.onAbortCb.?(this.onAbortData.?);
|
||||
}
|
||||
|
||||
this.detachResponse();
|
||||
var any_js_calls = false;
|
||||
@@ -1439,6 +1465,12 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
resp.writeHeaderInt("content-length", 0);
|
||||
this.endWithoutBody(this.shouldCloseConnection());
|
||||
},
|
||||
.Render => {
|
||||
// Render should have been handled elsewhere, this is unexpected
|
||||
this.renderMetadata();
|
||||
resp.writeHeaderInt("content-length", 0);
|
||||
this.endWithoutBody(this.shouldCloseConnection());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1675,6 +1707,55 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(req.allocator);
|
||||
defer exception_list.deinit();
|
||||
server.vm.runErrorHandler(err, &exception_list);
|
||||
|
||||
// Render the error fallback HTML page like renderDefaultError does
|
||||
if (!req.flags.has_written_status) {
|
||||
req.flags.has_written_status = true;
|
||||
if (req.resp) |resp| {
|
||||
resp.writeStatus("500 Internal Server Error");
|
||||
resp.writeHeader("content-type", MimeType.html.value);
|
||||
}
|
||||
}
|
||||
|
||||
const allocator = req.allocator;
|
||||
const fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable;
|
||||
defer allocator.destroy(fallback_container);
|
||||
|
||||
// Create error message for the stream rejection
|
||||
const error_message = "Stream error during server-side rendering";
|
||||
|
||||
fallback_container.* = Api.FallbackMessageContainer{
|
||||
.message = allocator.dupe(u8, error_message) catch unreachable,
|
||||
.router = null,
|
||||
.reason = .fetch_event_handler,
|
||||
.cwd = server.vm.transpiler.fs.top_level_dir,
|
||||
.problems = Api.Problems{
|
||||
.code = 500,
|
||||
.name = "StreamError",
|
||||
.exceptions = exception_list.items,
|
||||
.build = .{
|
||||
.msgs = &.{},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var bb = std.ArrayList(u8).init(allocator);
|
||||
defer bb.clearAndFree();
|
||||
const bb_writer = bb.writer();
|
||||
|
||||
Fallback.renderBackend(
|
||||
allocator,
|
||||
fallback_container,
|
||||
@TypeOf(bb_writer),
|
||||
bb_writer,
|
||||
) catch unreachable;
|
||||
|
||||
if (req.resp) |resp| {
|
||||
_ = resp.write(bb.items);
|
||||
}
|
||||
|
||||
req.endStream(req.shouldCloseConnection());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1803,6 +1884,74 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
|
||||
return;
|
||||
},
|
||||
.Render => |render_body| {
|
||||
// Handle Response.render() case - need to call DevServer with new path
|
||||
defer bun.default_allocator.free(render_body.path);
|
||||
|
||||
if (this.server) |server| {
|
||||
if (@hasField(@TypeOf(server.*), "dev_server")) {
|
||||
if (server.dev_server) |dev_server_| {
|
||||
const dev_server: *bun.bake.DevServer = dev_server_;
|
||||
// Use the current response from the RequestContext
|
||||
const response = if (this.resp) |resp|
|
||||
bun.uws.AnyResponse.init(resp)
|
||||
else {
|
||||
this.renderMissing();
|
||||
return;
|
||||
};
|
||||
|
||||
const signal = if (this.signal) |signal| signal.ref() else null;
|
||||
|
||||
const resp = this.resp;
|
||||
this.detachResponse();
|
||||
|
||||
const new_request_ctx = server.request_pool_allocator.tryGet() catch bun.outOfMemory();
|
||||
new_request_ctx.* = .{
|
||||
.allocator = server.allocator,
|
||||
.resp = resp,
|
||||
.req = this.req,
|
||||
.method = this.method,
|
||||
.server = server,
|
||||
.defer_deinit_until_callback_completes = null,
|
||||
.signal = signal,
|
||||
};
|
||||
|
||||
const url = bun.String.cloneUTF8(render_body.path);
|
||||
const body: jsc.WebCore.Body.Value = .{ .Null = {} };
|
||||
|
||||
const new_request = Request.new(.{
|
||||
.method = .GET, // TODO: use the correct one
|
||||
.request_context = AnyRequestContext.init(new_request_ctx),
|
||||
.https = ssl_enabled,
|
||||
.signal = if (signal) |s| s.ref() else null,
|
||||
.body = server.vm.initRequestBodyValue(body) catch bun.outOfMemory(),
|
||||
.url = url,
|
||||
});
|
||||
|
||||
new_request_ctx.request_weakref = .initRef(new_request);
|
||||
|
||||
server.onPendingRequest();
|
||||
// Call DevServer with the render path
|
||||
dev_server.handleRenderRedirect(bun.jsc.API.SavedRequest{
|
||||
.js_request = .create(new_request.toJS(server.globalThis), server.globalThis),
|
||||
.ctx = AnyRequestContext.init(new_request_ctx),
|
||||
.request = new_request,
|
||||
.response = bun.uws.AnyResponse.init(resp.?),
|
||||
}, render_body.path, response) catch {
|
||||
// On error, render missing
|
||||
this.renderMissing();
|
||||
return;
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to rendering missing
|
||||
this.renderMissing();
|
||||
return;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
@@ -1945,7 +2094,10 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
var vm: *jsc.VirtualMachine = this.server.?.vm;
|
||||
const globalThis = this.server.?.globalThis;
|
||||
if (comptime debug_mode) {
|
||||
var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(this.allocator);
|
||||
var arena = std.heap.ArenaAllocator.init(this.allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(allocator);
|
||||
defer exception_list.deinit();
|
||||
const prev_exception_list = vm.onUnhandledRejectionExceptionList;
|
||||
vm.onUnhandledRejectionExceptionList = &exception_list;
|
||||
@@ -1953,9 +2105,10 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
vm.onUnhandledRejectionExceptionList = prev_exception_list;
|
||||
|
||||
this.renderDefaultError(
|
||||
allocator,
|
||||
vm.log,
|
||||
error.ExceptionOcurred,
|
||||
exception_list.toOwnedSlice() catch @panic("TODO"),
|
||||
exception_list.items,
|
||||
"<r><red>{s}<r> - <b>{}<r> failed",
|
||||
.{ @as(string, @tagName(this.method)), this.ensurePathname() },
|
||||
);
|
||||
|
||||
@@ -749,6 +749,43 @@ extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__WTFStringImpl__ensureHash(WTF::Stri
|
||||
str->hash();
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue JSC__JSValue__upsertBunStringArray(
|
||||
JSC::EncodedJSValue encodedTarget,
|
||||
JSC::JSGlobalObject* global,
|
||||
const BunString* key,
|
||||
JSC::EncodedJSValue encodedValue)
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(global->vm());
|
||||
JSC::JSObject* target = JSC::JSValue::decode(encodedTarget).getObject();
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
JSC::JSValue newValue = JSC::JSValue::decode(encodedValue);
|
||||
auto& vm = global->vm();
|
||||
WTF::String str = key->tag == BunStringTag::Empty ? WTF::emptyString() : key->toWTFString();
|
||||
Identifier id = Identifier::fromString(vm, str);
|
||||
auto existingValue = target->getIfPropertyExists(global, id);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (!existingValue.isEmpty()) {
|
||||
// If existing value is already an array, push to it
|
||||
if (existingValue.isObject() && existingValue.getObject()->inherits<JSC::JSArray>()) {
|
||||
JSC::JSArray* array = jsCast<JSC::JSArray*>(existingValue.getObject());
|
||||
array->push(global, newValue);
|
||||
} else {
|
||||
// Create new array with both values
|
||||
JSC::JSArray* array = JSC::constructEmptyArray(global, nullptr, 2);
|
||||
array->putDirectIndex(global, 0, existingValue);
|
||||
array->putDirectIndex(global, 1, newValue);
|
||||
target->putDirect(vm, id, array, 0);
|
||||
}
|
||||
} else {
|
||||
// No existing value, just put the new value directly
|
||||
target->putDirect(vm, id, newValue, 0);
|
||||
}
|
||||
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
extern "C" void JSC__JSValue__putBunString(
|
||||
JSC::EncodedJSValue encodedTarget,
|
||||
JSC::JSGlobalObject* global,
|
||||
|
||||
@@ -12,6 +12,10 @@ pub const JSGlobalObject = opaque {
|
||||
return error.JSError;
|
||||
}
|
||||
|
||||
pub fn allowJSXInResponseConstructor(this: *JSGlobalObject) bool {
|
||||
return this.bunVM().allowJSXInResponseConstructor();
|
||||
}
|
||||
|
||||
extern fn JSGlobalObject__createOutOfMemoryError(this: *JSGlobalObject) JSValue;
|
||||
pub fn createOutOfMemoryError(this: *JSGlobalObject) JSValue {
|
||||
return JSGlobalObject__createOutOfMemoryError(this);
|
||||
|
||||
@@ -45,6 +45,26 @@ pub const JSValue = enum(i64) {
|
||||
return jsc.JSObject.getIndex(this, globalThis, i);
|
||||
}
|
||||
|
||||
extern fn JSC__JSValue__isJSXElement(JSValue, *JSGlobalObject) bool;
|
||||
pub fn isJSXElement(this: JSValue, globalThis: *jsc.JSGlobalObject) JSError!bool {
|
||||
return bun.jsc.fromJSHostCallGeneric(
|
||||
globalThis,
|
||||
@src(),
|
||||
JSC__JSValue__isJSXElement,
|
||||
.{ this, globalThis },
|
||||
);
|
||||
}
|
||||
|
||||
extern fn JSC__JSValue__transformToReactElement(responseValue: JSValue, componentValue: JSValue, globalObject: *JSGlobalObject) void;
|
||||
pub fn transformToReactElement(responseValue: JSValue, componentValue: JSValue, globalThis: *jsc.JSGlobalObject) void {
|
||||
JSC__JSValue__transformToReactElement(responseValue, componentValue, globalThis);
|
||||
}
|
||||
|
||||
extern fn JSC__JSValue__transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalObject: *JSGlobalObject) void;
|
||||
pub fn transformToReactElementWithOptions(responseValue: JSValue, componentValue: JSValue, responseOptions: JSValue, globalThis: *jsc.JSGlobalObject) void {
|
||||
JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, responseOptions, globalThis);
|
||||
}
|
||||
|
||||
extern fn JSC__JSValue__getDirectIndex(JSValue, *JSGlobalObject, u32) JSValue;
|
||||
pub fn getDirectIndex(this: JSValue, globalThis: *JSGlobalObject, i: u32) JSValue {
|
||||
return JSC__JSValue__getDirectIndex(this, globalThis, i);
|
||||
@@ -319,6 +339,13 @@ pub const JSValue = enum(i64) {
|
||||
JSC__JSValue__putBunString(value, global, key, result);
|
||||
}
|
||||
|
||||
extern "c" fn JSC__JSValue__upsertBunStringArray(value: JSValue, global: *JSGlobalObject, key: *const bun.String, result: jsc.JSValue) JSValue;
|
||||
|
||||
/// Put key/val pair into `obj`. If `key` is already present on the object, create an array for the values.
|
||||
pub fn putBunStringOneOrArray(obj: JSValue, global: *JSGlobalObject, key: *const bun.String, value: jsc.JSValue) bun.JSError!JSValue {
|
||||
return fromJSHostCall(global, @src(), JSC__JSValue__upsertBunStringArray, .{ obj, global, key, value });
|
||||
}
|
||||
|
||||
pub fn put(value: JSValue, global: *JSGlobalObject, key: anytype, result: jsc.JSValue) void {
|
||||
const Key = @TypeOf(key);
|
||||
if (comptime @typeInfo(Key) == .pointer) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "BunClientData.h"
|
||||
#include "GCDefferalContext.h"
|
||||
#include "WebCoreJSBuiltins.h"
|
||||
|
||||
#include "JavaScriptCore/AggregateError.h"
|
||||
#include "JavaScriptCore/BytecodeIndex.h"
|
||||
@@ -48,6 +49,7 @@
|
||||
#include "JavaScriptCore/JSONObject.h"
|
||||
#include "JavaScriptCore/JSObject.h"
|
||||
#include "JavaScriptCore/JSSet.h"
|
||||
#include "JavaScriptCore/Strong.h"
|
||||
#include "JavaScriptCore/JSSetIterator.h"
|
||||
#include "JavaScriptCore/JSString.h"
|
||||
#include "JavaScriptCore/ProxyObject.h"
|
||||
@@ -5884,12 +5886,307 @@ restart:
|
||||
JSC__JSValue__forEachPropertyImpl<false>(JSValue0, globalObject, arg2, iter);
|
||||
}
|
||||
|
||||
extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
|
||||
// React does this:
|
||||
// export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element');
|
||||
// export const REACT_ELEMENT_TYPE: symbol = renameElementSymbol
|
||||
// ? Symbol.for('react.transitional.element')
|
||||
// : REACT_LEGACY_ELEMENT_TYPE;
|
||||
|
||||
// TODO: cache these, i cri everytim
|
||||
auto react_legacy_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.element"_s));
|
||||
auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s));
|
||||
|
||||
JSC::JSValue value = JSC::JSValue::decode(JSValue0);
|
||||
|
||||
// TODO: primitive values (strings, numbers, booleans, null, undefined) are also valid
|
||||
if (value.isObject()) {
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSC::JSObject* object = value.getObject();
|
||||
auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s);
|
||||
JSC::JSValue typeofValue = object->get(globalObject, typeofProperty);
|
||||
RETURN_IF_EXCEPTION(scope, false);
|
||||
|
||||
if (typeofValue.isSymbol() && (typeofValue == react_legacy_element_symbol || typeofValue == react_element_symbol)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::EncodedJSValue responseOptionsValue, JSC::JSGlobalObject* globalObject);
|
||||
|
||||
extern "C" void JSC__JSValue__transformToReactElement(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
// Call the version with options, passing undefined for options
|
||||
JSC__JSValue__transformToReactElementWithOptions(responseValue, componentValue, JSC::JSValue::encode(JSC::jsUndefined()), globalObject);
|
||||
}
|
||||
|
||||
/// TODO: This could just be a builtin function and be 10x less lines of code why is it in C++
|
||||
/// TODO: this should actually just be a special Response sub-class and the transpiler rewrites the code to use this
|
||||
///
|
||||
/// What this function does is make a Response object pretend to be a React
|
||||
/// component. To do this we have to put a couple properties on it:
|
||||
///
|
||||
/// ```ts
|
||||
/// response["$$typeof"] = REACT_ELEMENT_TYPE;
|
||||
/// response.type = Component;
|
||||
/// response.key = null;
|
||||
/// response.props = {};
|
||||
|
||||
/// // Add the _store object that React expects in dev mode
|
||||
/// response._store = {};
|
||||
/// Object.defineProperty(response._store, 'validated', {
|
||||
/// configurable: false,
|
||||
/// enumerable: false,
|
||||
/// writable: true,
|
||||
/// value: 0 // or 1 to mark it as already validated
|
||||
/// });
|
||||
|
||||
/// // Add debug properties expected in dev mode
|
||||
/// response._owner = null;
|
||||
/// response._debugInfo = null;
|
||||
/// response._debugStack = null; // or new Error() if you want a stack trace
|
||||
/// response._debugTask = null;
|
||||
/// ```
|
||||
extern "C" void JSC__JSValue__transformToReactElementWithOptions(JSC::EncodedJSValue responseValue, JSC::EncodedJSValue componentValue, JSC::EncodedJSValue responseOptionsValue, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_CATCH_SCOPE(vm);
|
||||
|
||||
JSC::JSValue response = JSC::JSValue::decode(responseValue);
|
||||
JSC::JSValue component = JSC::JSValue::decode(componentValue);
|
||||
JSC::JSValue responseOptions = JSC::JSValue::decode(responseOptionsValue);
|
||||
|
||||
if (!response.isObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSC::JSObject* responseObject = response.getObject();
|
||||
|
||||
// Get the React element symbol - same as in isJSXElement
|
||||
// For now, use the transitional element symbol (React 19+)
|
||||
// TODO: check for legacy symbol as fallback
|
||||
auto react_element_symbol = JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey("react.transitional.element"_s));
|
||||
|
||||
// If we have response options, we need to wrap the component
|
||||
// For now, we'll store the response options directly on the response object
|
||||
// The actual wrapping with AsyncLocalStorage update will happen when rendered
|
||||
if (!responseOptions.isUndefinedOrNull() && responseOptions.isObject()) {
|
||||
// Store the response options on the response object for later use
|
||||
// This will be accessed when the component is rendered
|
||||
auto responseOptionsIdentifier = JSC::Identifier::fromString(vm, "__responseOptions"_s);
|
||||
responseObject->putDirect(vm, responseOptionsIdentifier, responseOptions, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
|
||||
// Transform the Response object itself into a React element
|
||||
// The component parameter is what will be stored in the 'type' field
|
||||
|
||||
// Set $$typeof property to mark this as a React element
|
||||
auto typeofIdentifier = JSC::Identifier::fromString(vm, "$$typeof"_s);
|
||||
responseObject->putDirect(vm, typeofIdentifier, react_element_symbol, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
// Set type property
|
||||
auto typeIdentifier = JSC::Identifier::fromString(vm, "type"_s);
|
||||
|
||||
// TODO: this is fucking awful
|
||||
// Check if this is a render redirect (Response.render() case) or regular redirect (Response.redirect() case)
|
||||
// We detect this by checking if component is the special marker string "__bun_render_redirect__" or "__bun_redirect__"
|
||||
bool isRenderRedirect = false;
|
||||
bool isRedirect = false;
|
||||
if (component.isString()) {
|
||||
JSC::JSString* componentString = component.toString(globalObject);
|
||||
if (componentString) {
|
||||
String componentStr = componentString->value(globalObject);
|
||||
if (componentStr == "__bun_render_redirect__"_s) {
|
||||
isRenderRedirect = true;
|
||||
} else if (componentStr == "__bun_redirect__"_s) {
|
||||
isRedirect = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Response.render() case - use BakeSSRResponse builtin to create wrapper
|
||||
if (isRenderRedirect) {
|
||||
// Get the render path and params from the response object
|
||||
auto renderPathIdentifier = JSC::Identifier::fromString(vm, "__renderPath"_s);
|
||||
auto renderParamsIdentifier = JSC::Identifier::fromString(vm, "__renderParams"_s);
|
||||
|
||||
JSC::JSValue renderPath = responseObject->get(globalObject, renderPathIdentifier);
|
||||
JSC::JSValue renderParams = responseObject->get(globalObject, renderParamsIdentifier);
|
||||
|
||||
// Use the BakeSSRResponse builtin's wrapComponent function
|
||||
JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject);
|
||||
|
||||
// Call wrapComponent(path, params, true, responseObject) where true indicates this is a render redirect
|
||||
JSC::MarkedArgumentBuffer args;
|
||||
args.append(renderPath);
|
||||
args.append(renderParams);
|
||||
args.append(JSC::jsBoolean(true)); // This is a render redirect
|
||||
args.append(response); // Pass the Response object
|
||||
|
||||
// Call the wrapComponent function
|
||||
auto callData = JSC::getCallData(wrapComponentFn);
|
||||
JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args);
|
||||
|
||||
if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) {
|
||||
responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
} else {
|
||||
// If there was an error, clear it and set type to null
|
||||
scope.clearException();
|
||||
responseObject->putDirect(vm, typeIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
}
|
||||
// Handle Response.redirect() case - use BakeSSRResponse builtin to create wrapper
|
||||
else if (isRedirect) {
|
||||
// Use the BakeSSRResponse builtin's wrapComponent function
|
||||
JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject);
|
||||
|
||||
// Call wrapComponent(undefined, undefined, "redirect", responseObject)
|
||||
// For redirect, we pass "redirect" as the third parameter and the Response object as the fourth
|
||||
JSC::MarkedArgumentBuffer args;
|
||||
args.append(JSC::jsUndefined()); // No component for redirect
|
||||
args.append(JSC::jsUndefined()); // No response options for redirect
|
||||
|
||||
// Pass "redirect" string as the third parameter to indicate this is a redirect
|
||||
// The responseOptions parameter from transformToReactElementWithOptions contains "redirect"
|
||||
args.append(responseOptions); // This should be the "redirect" string
|
||||
args.append(response); // Pass the Response object
|
||||
|
||||
// Call the wrapComponent function
|
||||
auto callData = JSC::getCallData(wrapComponentFn);
|
||||
JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args);
|
||||
|
||||
if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) {
|
||||
responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
} else {
|
||||
// If there was an error, clear it and set type to null
|
||||
scope.clearException();
|
||||
responseObject->putDirect(vm, typeIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
}
|
||||
// TODO: this is stupid
|
||||
// Check if the component is a JSX element (has $$typeof property)
|
||||
// If it is, we need to wrap it in a function for React to work properly
|
||||
// because `<Component />` is already the result of calling Component()
|
||||
else if (component.isObject()) {
|
||||
JSC::JSObject* componentObject = component.getObject();
|
||||
auto typeofProperty = JSC::Identifier::fromString(vm, "$$typeof"_s);
|
||||
JSC::JSValue typeofValue = componentObject->get(globalObject, typeofProperty);
|
||||
|
||||
if (!scope.exception() && typeofValue.isSymbol()) {
|
||||
// It's a JSX element - wrap it in a function that returns it
|
||||
// If we have response options, use the BakeSSRResponse builtin to wrap the component
|
||||
// so it can update AsyncLocalStorage when rendered
|
||||
|
||||
if (!responseOptions.isUndefinedOrNull() && responseOptions.isObject()) {
|
||||
// Use the BakeSSRResponse builtin's wrapComponent function
|
||||
// This will create a wrapper that updates AsyncLocalStorage before returning the component
|
||||
|
||||
// Create the wrapComponent function from the BakeSSRResponse builtin
|
||||
// The pattern is: <filename><functionName>CodeGenerator
|
||||
// So for BakeSSRResponse.ts with export function wrapComponent:
|
||||
JSC::JSFunction* wrapComponentFn = JSC::JSFunction::create(vm, globalObject, bakeSSRResponseWrapComponentCodeGenerator(vm), globalObject);
|
||||
|
||||
// Call wrapComponent(component, responseOptions, false, undefined)
|
||||
JSC::MarkedArgumentBuffer args;
|
||||
args.append(component);
|
||||
args.append(responseOptions);
|
||||
args.append(JSC::jsBoolean(false)); // Not a render redirect
|
||||
args.append(JSC::jsUndefined()); // No response object for regular case
|
||||
|
||||
// Call the wrapComponent function
|
||||
auto callData = JSC::getCallData(wrapComponentFn);
|
||||
JSC::JSValue wrappedComponent = JSC::call(globalObject, wrapComponentFn, callData, JSC::jsUndefined(), args);
|
||||
|
||||
if (!scope.exception() && !wrappedComponent.isUndefinedOrNull()) {
|
||||
responseObject->putDirect(vm, typeIdentifier, wrappedComponent, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
} else {
|
||||
// Fall back to simple wrapper if there was an exception or undefined result
|
||||
scope.clearException();
|
||||
JSC::Strong<JSC::Unknown> strongComponent(vm, component);
|
||||
auto* wrapperFunction = JSC::JSNativeStdFunction::create(
|
||||
vm,
|
||||
globalObject,
|
||||
0, // arity
|
||||
String(), // name
|
||||
[strongComponent](JSC::JSGlobalObject* execGlobalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue {
|
||||
return JSC::JSValue::encode(strongComponent.get());
|
||||
});
|
||||
|
||||
responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
} else {
|
||||
// No response options - create a simple wrapper
|
||||
JSC::Strong<JSC::Unknown> strongComponent(vm, component);
|
||||
|
||||
auto* wrapperFunction = JSC::JSNativeStdFunction::create(
|
||||
vm,
|
||||
globalObject,
|
||||
0, // arity
|
||||
String(), // name
|
||||
[strongComponent](JSC::JSGlobalObject* execGlobalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue {
|
||||
return JSC::JSValue::encode(strongComponent.get());
|
||||
});
|
||||
|
||||
responseObject->putDirect(vm, typeIdentifier, wrapperFunction, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
} else {
|
||||
// It's not a JSX element, use it directly
|
||||
responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
} else {
|
||||
// It's not an object (could be a function or primitive), use it directly
|
||||
responseObject->putDirect(vm, typeIdentifier, component, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
|
||||
// Set key property to null
|
||||
auto keyIdentifier = JSC::Identifier::fromString(vm, "key"_s);
|
||||
responseObject->putDirect(vm, keyIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
// Set props property to empty object
|
||||
auto propsIdentifier = JSC::Identifier::fromString(vm, "props"_s);
|
||||
responseObject->putDirect(vm, propsIdentifier, JSC::constructEmptyObject(globalObject), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
// Add _store object for dev mode
|
||||
auto storeIdentifier = JSC::Identifier::fromString(vm, "_store"_s);
|
||||
JSC::JSObject* storeObject = JSC::constructEmptyObject(globalObject);
|
||||
|
||||
// Add validated property to _store
|
||||
auto validatedIdentifier = JSC::Identifier::fromString(vm, "validated"_s);
|
||||
storeObject->putDirect(vm, validatedIdentifier, JSC::jsNumber(0), 0);
|
||||
|
||||
responseObject->putDirect(vm, storeIdentifier, storeObject, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
// Add debug properties (all set to null)
|
||||
auto ownerIdentifier = JSC::Identifier::fromString(vm, "_owner"_s);
|
||||
responseObject->putDirect(vm, ownerIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
auto debugInfoIdentifier = JSC::Identifier::fromString(vm, "_debugInfo"_s);
|
||||
responseObject->putDirect(vm, debugInfoIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
auto debugStackIdentifier = JSC::Identifier::fromString(vm, "_debugStack"_s);
|
||||
// TODO: we should put an error here if we want a stack trace apparently
|
||||
responseObject->putDirect(vm, debugStackIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
|
||||
auto debugTaskIdentifier = JSC::Identifier::fromString(vm, "_debugTask"_s);
|
||||
responseObject->putDirect(vm, debugTaskIdentifier, JSC::jsNull(), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0);
|
||||
}
|
||||
|
||||
extern "C" void JSC__JSValue__forEachPropertyNonIndexed(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)(JSC::JSGlobalObject* arg0, void* ctx, ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol))
|
||||
{
|
||||
JSC__JSValue__forEachPropertyImpl<true>(JSValue0, globalObject, arg2, iter);
|
||||
}
|
||||
|
||||
[[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol))
|
||||
|
||||
{
|
||||
JSC::JSValue value = JSC::JSValue::decode(JSValue0);
|
||||
JSC::JSObject* object = value.getObject();
|
||||
|
||||
90
src/bun.js/bindings/webcore/ResponseHelpers.cpp
Normal file
90
src/bun.js/bindings/webcore/ResponseHelpers.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
#include "config.h"
|
||||
#include "JavaScriptCore/JSObject.h"
|
||||
#include "JavaScriptCore/ObjectConstructor.h"
|
||||
#include "JavaScriptCore/JSGlobalObject.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
namespace Bun {
|
||||
|
||||
using namespace JSC;
|
||||
|
||||
// Helper function to merge AsyncLocalStorage context into Response init options
|
||||
// This modifies initOptions in place by adding properties from alsStore that don't exist in initOptions
|
||||
extern "C" void Response__mergeAsyncLocalStorageOptions(
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSC::EncodedJSValue alsStoreValue,
|
||||
JSC::EncodedJSValue initOptionsValue)
|
||||
{
|
||||
VM& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSValue alsStore = JSValue::decode(alsStoreValue);
|
||||
JSValue initOptions = JSValue::decode(initOptionsValue);
|
||||
|
||||
// Both must be objects
|
||||
if (!alsStore.isObject() || !initOptions.isObject()) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSObject* alsStoreObject = asObject(alsStore);
|
||||
JSObject* initOptionsObject = asObject(initOptions);
|
||||
|
||||
// Get properties from alsStore
|
||||
PropertyNameArray properties(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude);
|
||||
alsStoreObject->getOwnPropertyNames(alsStoreObject, globalObject, properties, DontEnumPropertiesMode::Exclude);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
|
||||
// Copy properties from alsStore to initOptions (only if they don't already exist)
|
||||
for (auto& propertyName : properties) {
|
||||
// Check if initOptions already has this property
|
||||
PropertySlot checkSlot(initOptionsObject, PropertySlot::InternalMethodType::Get);
|
||||
if (!initOptionsObject->getOwnPropertySlot(initOptionsObject, globalObject, propertyName, checkSlot)) {
|
||||
// Property doesn't exist in initOptions, copy it from alsStore
|
||||
PropertySlot slot(alsStoreObject, PropertySlot::InternalMethodType::Get);
|
||||
if (alsStoreObject->getOwnPropertySlot(alsStoreObject, globalObject, propertyName, slot)) {
|
||||
JSValue value = slot.getValue(globalObject, propertyName);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
initOptionsObject->putDirect(vm, propertyName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get the store from AsyncLocalStorage instance
|
||||
extern "C" JSC::EncodedJSValue Response__getAsyncLocalStorageStore(
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSC::EncodedJSValue alsValue)
|
||||
{
|
||||
VM& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
JSValue als = JSValue::decode(alsValue);
|
||||
if (!als.isObject()) {
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSObject* alsObject = asObject(als);
|
||||
|
||||
// Call the getStore() method
|
||||
Identifier getStoreId = Identifier::fromString(vm, "getStore"_s);
|
||||
PropertySlot slot(alsObject, PropertySlot::InternalMethodType::Get);
|
||||
if (!alsObject->getPropertySlot(globalObject, getStoreId, slot)) {
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSValue getStoreFunction = slot.getValue(globalObject, getStoreId);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
if (!getStoreFunction.isCallable()) {
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
CallData callData = getCallData(getStoreFunction);
|
||||
MarkedArgumentBuffer args;
|
||||
JSValue result = call(globalObject, getStoreFunction, callData, alsObject, args);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
return JSValue::encode(result);
|
||||
}
|
||||
|
||||
} // namespace Bun
|
||||
@@ -173,6 +173,13 @@ export fn Bun__addBakeSourceProviderSourceMap(vm: *VirtualMachine, opaque_source
|
||||
vm.source_mappings.putBakeSourceProvider(@as(*BakeSourceProvider, @ptrCast(opaque_source_provider)), slice.slice());
|
||||
}
|
||||
|
||||
export fn Bun__addDevServerSourceProvider(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void {
|
||||
var sfb = std.heap.stackFallback(4096, bun.default_allocator);
|
||||
const slice = specifier.toUTF8(sfb.get());
|
||||
defer slice.deinit();
|
||||
vm.source_mappings.putDevServerSourceProvider(@as(*DevServerSourceProvider, @ptrCast(opaque_source_provider)), slice.slice());
|
||||
}
|
||||
|
||||
export fn Bun__addSourceProviderSourceMap(vm: *VirtualMachine, opaque_source_provider: *anyopaque, specifier: *bun.String) void {
|
||||
var sfb = std.heap.stackFallback(4096, bun.default_allocator);
|
||||
const slice = specifier.toUTF8(sfb.get());
|
||||
@@ -209,6 +216,7 @@ const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const BakeSourceProvider = bun.sourcemap.BakeSourceProvider;
|
||||
const DevServerSourceProvider = bun.sourcemap.DevServerSourceProvider;
|
||||
const PluginRunner = bun.transpiler.PluginRunner;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
|
||||
@@ -1322,6 +1322,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
.Empty,
|
||||
.Blob,
|
||||
.Null,
|
||||
.Render,
|
||||
=> {
|
||||
break :brk response.body.use();
|
||||
},
|
||||
@@ -1383,6 +1384,7 @@ pub fn writeFileInternal(globalThis: *jsc.JSGlobalObject, path_or_blob_: *PathOr
|
||||
.Empty,
|
||||
.Blob,
|
||||
.Null,
|
||||
.Render,
|
||||
=> {
|
||||
break :brk request.body.value.use();
|
||||
},
|
||||
|
||||
@@ -268,6 +268,9 @@ pub const Value = union(Tag) {
|
||||
Empty,
|
||||
Error: ValueError,
|
||||
Null,
|
||||
Render: struct {
|
||||
path: []const u8,
|
||||
},
|
||||
|
||||
// We may not have all the data yet
|
||||
// So we can't know for sure if it's empty or not
|
||||
@@ -280,6 +283,7 @@ pub const Value = union(Tag) {
|
||||
.Blob => this.Blob.size == 0,
|
||||
.WTFStringImpl => this.WTFStringImpl.length() == 0,
|
||||
.Error, .Locked => false,
|
||||
.Render => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -440,6 +444,7 @@ pub const Value = union(Tag) {
|
||||
Empty,
|
||||
Error,
|
||||
Null,
|
||||
Render,
|
||||
};
|
||||
|
||||
// pub const empty = Value{ .Empty = {} };
|
||||
@@ -457,6 +462,10 @@ pub const Value = union(Tag) {
|
||||
.Null => {
|
||||
return JSValue.null;
|
||||
},
|
||||
.Render => {
|
||||
// Render variant cannot be converted to a stream
|
||||
return JSValue.null;
|
||||
},
|
||||
.InternalBlob, .Blob, .WTFStringImpl => {
|
||||
var blob = this.use();
|
||||
defer blob.detach();
|
||||
@@ -751,6 +760,7 @@ pub const Value = union(Tag) {
|
||||
.InternalBlob => this.InternalBlob.sliceConst(),
|
||||
.WTFStringImpl => if (this.WTFStringImpl.canUseAsUTF8()) this.WTFStringImpl.latin1Slice() else "",
|
||||
// .InlineBlob => this.InlineBlob.sliceConst(),
|
||||
.Render => "",
|
||||
else => "",
|
||||
};
|
||||
}
|
||||
@@ -1396,6 +1406,7 @@ pub const ValueBufferer = struct {
|
||||
.WTFStringImpl,
|
||||
.InternalBlob,
|
||||
.Blob,
|
||||
.Render,
|
||||
=> {
|
||||
// toBlobIfPossible checks for WTFString needing a conversion.
|
||||
var input = value.useAsAnyBlobAllowNonUTF8String();
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
const Response = @This();
|
||||
|
||||
// C++ helper functions for AsyncLocalStorage integration
|
||||
extern fn Response__getAsyncLocalStorageStore(global: *JSGlobalObject, als: JSValue) JSValue;
|
||||
extern fn Response__mergeAsyncLocalStorageOptions(global: *JSGlobalObject, alsStore: JSValue, initOptions: JSValue) void;
|
||||
|
||||
// Zig function to update AsyncLocalStorage with response options
|
||||
pub fn bakeGetAsyncLocalStorage(global: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
const vm = global.bunVM();
|
||||
|
||||
// Get the AsyncLocalStorage instance from the VM
|
||||
if (vm.getDevServerAsyncLocalStorage()) |als| {
|
||||
return als;
|
||||
}
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
const ResponseMixin = BodyMixin(@This());
|
||||
pub const js = jsc.Codegen.JSResponse;
|
||||
// NOTE: toJS is overridden
|
||||
@@ -14,6 +30,8 @@ redirected: bool = false,
|
||||
/// In the server we use a flag response_protected to protect/unprotect the response
|
||||
ref_count: u32 = 1,
|
||||
|
||||
is_jsx: bool = false,
|
||||
|
||||
// We must report a consistent value for this
|
||||
reported_estimated_size: usize = 0,
|
||||
|
||||
@@ -104,7 +122,7 @@ pub export fn jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer(globa
|
||||
var any_blob = body.useAsAnyBlob();
|
||||
return any_blob.toArrayBufferTransfer(globalObject) catch return .zero;
|
||||
},
|
||||
.Error, .Locked => return .js_undefined,
|
||||
.Error, .Locked, .Render => return .js_undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +321,7 @@ pub fn cloneValue(
|
||||
}
|
||||
|
||||
pub fn clone(this: *Response, globalThis: *JSGlobalObject) bun.JSError!*Response {
|
||||
// TODO: handle clone for jsxElement for bake?
|
||||
return bun.new(Response, try this.cloneValue(globalThis));
|
||||
}
|
||||
|
||||
@@ -497,9 +516,93 @@ pub fn constructRedirect(
|
||||
var headers_ref = response.init.headers.?;
|
||||
try headers_ref.put(.Location, url_string_slice.slice(), globalThis);
|
||||
const ptr = bun.new(Response, response);
|
||||
|
||||
const response_js = ptr.toJS(globalThis);
|
||||
|
||||
// Check if dev_server_async_local_storage is set (indicating we're in Bun dev server)
|
||||
const vm = globalThis.bunVM();
|
||||
if (vm.dev_server_async_local_storage.has()) {
|
||||
// Mark this as a redirect Response that should be handled specially
|
||||
// when used in a React component
|
||||
const redirect_marker = ZigString.init("__bun_redirect__").toJS(globalThis);
|
||||
|
||||
// Transform the Response to act as a React element with special redirect handling
|
||||
// Pass "redirect" as the third parameter to indicate this is a redirect
|
||||
const redirect_flag = ZigString.init("redirect").toJS(globalThis);
|
||||
JSValue.transformToReactElementWithOptions(response_js, redirect_marker, redirect_flag, globalThis);
|
||||
}
|
||||
|
||||
return ptr.toJS(globalThis);
|
||||
return response_js;
|
||||
}
|
||||
|
||||
pub fn constructRender(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) bun.JSError!JSValue {
|
||||
const arguments = callframe.arguments_old(2);
|
||||
const vm = globalThis.bunVM();
|
||||
|
||||
// Check if dev_server_async_local_storage is set
|
||||
if (!vm.dev_server_async_local_storage.has()) {
|
||||
return globalThis.throwInvalidArguments("Response.render() is only available in the Bun dev server", .{});
|
||||
}
|
||||
|
||||
// Validate arguments
|
||||
if (arguments.len < 1) {
|
||||
return globalThis.throwInvalidArguments("Response.render() requires at least a path argument", .{});
|
||||
}
|
||||
|
||||
const path_arg = arguments.ptr[0];
|
||||
if (!path_arg.isString()) {
|
||||
return globalThis.throwInvalidArguments("Response.render() path must be a string", .{});
|
||||
}
|
||||
|
||||
// Get the path string
|
||||
const path_str = try path_arg.toSlice(globalThis, bun.default_allocator);
|
||||
|
||||
// Duplicate the path string so it persists
|
||||
const path_copy = bun.default_allocator.dupe(u8, path_str.slice()) catch {
|
||||
path_str.deinit();
|
||||
return globalThis.throwOutOfMemory();
|
||||
};
|
||||
path_str.deinit();
|
||||
|
||||
// Create a Response with Render body
|
||||
var response = bun.new(Response, Response{
|
||||
.body = Body{
|
||||
.value = .{
|
||||
.Render = .{
|
||||
.path = path_copy,
|
||||
},
|
||||
},
|
||||
},
|
||||
.init = Response.Init{
|
||||
.status_code = 200,
|
||||
},
|
||||
});
|
||||
|
||||
const response_js = response.toJS(globalThis);
|
||||
response_js.ensureStillAlive();
|
||||
|
||||
// Store the render path and params on the response for later use
|
||||
// When React tries to render this component, we'll check for these and throw RenderAbortError
|
||||
response_js.put(globalThis, "__renderPath", path_arg);
|
||||
const params_arg = if (arguments.len >= 2) arguments.ptr[1] else JSValue.jsNull();
|
||||
response_js.put(globalThis, "__renderParams", params_arg);
|
||||
|
||||
// TODO: this is terrible
|
||||
// Create a simple wrapper function that will be called by React
|
||||
// This needs to be handled specially in transformToReactElementWithOptions
|
||||
// We'll pass a special marker as the component to indicate this is a render redirect
|
||||
const render_marker = ZigString.init("__bun_render_redirect__").toJS(globalThis);
|
||||
|
||||
// Transform the Response to act as a React element
|
||||
// The C++ code will need to check for this special marker
|
||||
JSValue.transformToReactElementWithOptions(response_js, render_marker, params_arg, globalThis);
|
||||
|
||||
return response_js;
|
||||
}
|
||||
|
||||
pub fn constructError(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
_: *jsc.CallFrame,
|
||||
@@ -519,9 +622,10 @@ pub fn constructError(
|
||||
return response.toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*Response {
|
||||
const arguments = callframe.argumentsAsArray(2);
|
||||
pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, this_value: jsc.JSValue) bun.JSError!*Response {
|
||||
var arguments = callframe.argumentsAsArray(2);
|
||||
|
||||
var is_jsx = false;
|
||||
if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) {
|
||||
if (arguments[0].as(Blob)) |blob| {
|
||||
if (blob.isS3()) {
|
||||
@@ -554,6 +658,20 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
|
||||
return bun.new(Response, response);
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for bake: allow `return new Response(<jsx> ... </jsx>, { ... }`
|
||||
// inside of a react component
|
||||
if (globalThis.allowJSXInResponseConstructor()) {
|
||||
const arg = arguments[0];
|
||||
// Check if it's a JSX element (object with $$typeof)
|
||||
if (try arg.isJSXElement(globalThis)) {
|
||||
// Pass the response options (arguments[1]) to transformToReactElement
|
||||
// so it can store them for later use when the component is rendered
|
||||
const responseOptions = if (arguments[1].isObject()) arguments[1] else .js_undefined;
|
||||
JSValue.transformToReactElementWithOptions(this_value, arg, responseOptions, globalThis);
|
||||
is_jsx = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
var init: Init = (brk: {
|
||||
if (arguments[1].isUndefinedOrNull()) {
|
||||
@@ -593,6 +711,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
|
||||
var response = bun.new(Response, Response{
|
||||
.body = body,
|
||||
.init = init,
|
||||
.is_jsx = is_jsx,
|
||||
});
|
||||
|
||||
if (response.body.value == .Blob and
|
||||
|
||||
@@ -689,6 +689,7 @@ pub const WriteFileWaitFromLockedValueTask = struct {
|
||||
.Null,
|
||||
.Empty,
|
||||
.Blob,
|
||||
.Render,
|
||||
=> {
|
||||
var blob = value.use();
|
||||
// TODO: this should be one promise not two!
|
||||
|
||||
@@ -83,6 +83,9 @@ export default [
|
||||
error: {
|
||||
fn: "constructError",
|
||||
},
|
||||
render: {
|
||||
fn: "constructRender",
|
||||
},
|
||||
},
|
||||
proto: {
|
||||
url: {
|
||||
@@ -124,6 +127,7 @@ export default [
|
||||
getter: "getBodyUsed",
|
||||
},
|
||||
},
|
||||
constructNeedsThis: true,
|
||||
}),
|
||||
define({
|
||||
name: "Blob",
|
||||
|
||||
@@ -93,6 +93,7 @@ async function run() {
|
||||
entrypoints: [generated_entrypoint],
|
||||
minify: !debug,
|
||||
drop: debug ? [] : ["DEBUG"],
|
||||
target: side === "server" ? "bun" : "browser",
|
||||
});
|
||||
if (!result.success) throw new AggregateError(result.logs);
|
||||
assert(result.outputs.length === 1, "must bundle to a single file");
|
||||
|
||||
@@ -5879,7 +5879,8 @@ const Tokenizer = struct {
|
||||
}
|
||||
|
||||
pub inline fn sliceFrom(this: *Tokenizer, start: usize) []const u8 {
|
||||
return this.src[start..this.position];
|
||||
const position = this.getPosition();
|
||||
return this.src[start..position];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ export async function renderRoutesForProdStatic(
|
||||
pageModule,
|
||||
params,
|
||||
} satisfies Bake.RouteMetadata);
|
||||
if (results === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (results == null) {
|
||||
throw new Error(`Route ${JSON.stringify(sourceRouteFiles[i])} cannot be pre-rendered to a static page.`);
|
||||
}
|
||||
@@ -151,6 +154,13 @@ export async function renderRoutesForProdStatic(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// for (const params of paramGetter.pages) {
|
||||
// const failed = await callRouteGenerator(type, noClient, i, layouts, pageModule, params);
|
||||
// if (failed) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// TODO: error signal here?
|
||||
await Promise.all(
|
||||
paramGetter.pages.map(params => {
|
||||
callRouteGenerator(type, noClient, i, layouts, pageModule, params);
|
||||
|
||||
51
src/js/builtins/BakeSSRResponse.ts
Normal file
51
src/js/builtins/BakeSSRResponse.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Used to make a Response fake being a component
|
||||
// When this is called, it will render the component and then update async local
|
||||
// storage with the options of the Response
|
||||
// For Response.render(), we pass the response as strongComponent and need a 4th parameter
|
||||
// For Response.redirect(), isRenderRedirect will be "redirect" instead of true
|
||||
export function wrapComponent(strongComponent, responseOptions, isRenderRedirect, responseObject) {
|
||||
const bakeGetAsyncLocalStorage = $newZigFunction("bun.js/webcore/Response.zig", "bakeGetAsyncLocalStorage", 0);
|
||||
|
||||
return function() {
|
||||
// For Response.redirect(), we need to throw a RedirectAbortError
|
||||
if (isRenderRedirect === "redirect") {
|
||||
// responseObject is the Response from Response.redirect()
|
||||
const RedirectAbortError = globalThis.RedirectAbortError;
|
||||
if (RedirectAbortError) {
|
||||
throw new RedirectAbortError(responseObject);
|
||||
}
|
||||
// Fallback if RedirectAbortError is not available
|
||||
const error = new Error("Response.redirect() called");
|
||||
error.name = "RedirectAbortError";
|
||||
error.response = responseObject;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// For Response.render(), we need to throw a RenderAbortError
|
||||
if (isRenderRedirect === true || isRenderRedirect === "render") {
|
||||
// strongComponent is the path string, responseOptions is params, responseObject is the Response
|
||||
// We need to get the RenderAbortError from the global
|
||||
const RenderAbortError = globalThis.RenderAbortError;
|
||||
if (RenderAbortError) {
|
||||
throw new RenderAbortError(strongComponent, responseOptions, responseObject);
|
||||
}
|
||||
// Fallback if RenderAbortError is not available
|
||||
const error = new Error("Response.render() called");
|
||||
error.name = "RenderAbortError";
|
||||
error.path = strongComponent;
|
||||
error.params = responseOptions;
|
||||
error.response = responseObject;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// For new Response(<jsx />, {}), update AsyncLocalStorage
|
||||
const async_local_storage = bakeGetAsyncLocalStorage();
|
||||
if (async_local_storage) {
|
||||
const store = async_local_storage.getStore();
|
||||
if (store) {
|
||||
store.responseOptions = responseOptions;
|
||||
}
|
||||
}
|
||||
return strongComponent;
|
||||
};
|
||||
}
|
||||
@@ -2310,6 +2310,9 @@ fn NewPrinter(
|
||||
if (p.options.require_ref) |require_ref| {
|
||||
p.printSymbol(require_ref);
|
||||
p.print(".resolve");
|
||||
} else if (p.options.module_type == .internal_bake_dev) {
|
||||
p.printSymbol(p.options.hmr_ref);
|
||||
p.print(".requireResolve");
|
||||
} else {
|
||||
p.print("require.resolve");
|
||||
}
|
||||
|
||||
@@ -888,15 +888,17 @@ pub const ParsedSourceMap = struct {
|
||||
|
||||
is_standalone_module_graph: bool = false,
|
||||
|
||||
const SourceProviderKind = enum(u1) { zig, bake };
|
||||
const SourceProviderKind = enum(u2) { zig, bake, dev_server };
|
||||
const AnySourceProvider = union(enum) {
|
||||
zig: *SourceProviderMap,
|
||||
bake: *BakeSourceProvider,
|
||||
dev_server: *DevServerSourceProvider,
|
||||
|
||||
pub fn ptr(this: AnySourceProvider) *anyopaque {
|
||||
return switch (this) {
|
||||
.zig => @ptrCast(this.zig),
|
||||
.bake => @ptrCast(this.bake),
|
||||
.dev_server => @ptrCast(this.dev_server),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -909,6 +911,7 @@ pub const ParsedSourceMap = struct {
|
||||
return switch (this) {
|
||||
.zig => this.zig.getSourceMap(source_filename, load_hint, result),
|
||||
.bake => this.bake.getSourceMap(source_filename, load_hint, result),
|
||||
.dev_server => this.dev_server.getSourceMap(source_filename, load_hint, result),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -916,7 +919,7 @@ pub const ParsedSourceMap = struct {
|
||||
const SourceContentPtr = packed struct(u64) {
|
||||
load_hint: SourceMapLoadHint,
|
||||
kind: SourceProviderKind,
|
||||
data: u61,
|
||||
data: u60,
|
||||
|
||||
pub const none: SourceContentPtr = .{ .load_hint = .none, .kind = .zig, .data = 0 };
|
||||
|
||||
@@ -928,10 +931,15 @@ pub const ParsedSourceMap = struct {
|
||||
return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .bake };
|
||||
}
|
||||
|
||||
fn fromDevServerProvider(p: *DevServerSourceProvider) SourceContentPtr {
|
||||
return .{ .load_hint = .none, .data = @intCast(@intFromPtr(p)), .kind = .dev_server };
|
||||
}
|
||||
|
||||
pub fn provider(sc: SourceContentPtr) ?AnySourceProvider {
|
||||
switch (sc.kind) {
|
||||
.zig => return .{ .zig = @ptrFromInt(sc.data) },
|
||||
.bake => return .{ .bake = @ptrFromInt(sc.data) },
|
||||
.dev_server => return .{ .dev_server = @ptrFromInt(sc.data) },
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1021,9 +1029,10 @@ pub const SourceMapLoadHint = enum(u2) {
|
||||
is_external_map,
|
||||
};
|
||||
|
||||
/// Always returns UTF-8
|
||||
fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice {
|
||||
const needle = comptime bun.strings.literal(T, "\n//# sourceMappingURL=");
|
||||
const found = bun.strings.indexOfT(T, source, needle) orelse return null;
|
||||
const found = std.mem.lastIndexOf(T, source, needle) orelse return null;
|
||||
const end = std.mem.indexOfScalarPos(T, source, found + needle.len, '\n') orelse source.len;
|
||||
const url = std.mem.trimRight(T, source[found + needle.len .. end], &.{ ' ', '\r' });
|
||||
return switch (T) {
|
||||
@@ -1036,6 +1045,189 @@ fn findSourceMappingURL(comptime T: type, source: []const T, alloc: std.mem.Allo
|
||||
};
|
||||
}
|
||||
|
||||
fn findSourceMappingURLNah(comptime T: type, source: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice {
|
||||
// According to the spec, we need to find the LAST valid sourceMappingURL
|
||||
// We need to handle both //# and //@ prefixes, and also /* */ comments
|
||||
var last_url: ?bun.jsc.ZigString.Slice = null;
|
||||
var i: usize = 0;
|
||||
|
||||
const solidus = comptime bun.strings.literal(T, "/")[0];
|
||||
const asterisk = comptime bun.strings.literal(T, "*")[0];
|
||||
const newline = comptime bun.strings.literal(T, "\n")[0];
|
||||
const carriage_return = comptime bun.strings.literal(T, "\r")[0];
|
||||
|
||||
// Line terminators as per ECMAScript spec
|
||||
// Note: For UTF-8, these would be multi-byte sequences, so we only check them in UTF-16
|
||||
const line_separator: T = if (T == u16) 0x2028 else newline;
|
||||
const paragraph_separator: T = if (T == u16) 0x2029 else newline;
|
||||
|
||||
while (i < source.len) {
|
||||
// Skip to next potential comment
|
||||
const slash_pos = std.mem.indexOfScalarPos(T, source, i, solidus) orelse break;
|
||||
i = slash_pos + 1;
|
||||
|
||||
if (i >= source.len) break;
|
||||
|
||||
const next_char = source[i];
|
||||
|
||||
// Handle single-line comment //
|
||||
if (next_char == solidus) {
|
||||
i += 1;
|
||||
const comment_start = i;
|
||||
|
||||
// Find end of line
|
||||
var line_end = source.len;
|
||||
var j = comment_start;
|
||||
while (j < source.len) : (j += 1) {
|
||||
const c = source[j];
|
||||
if (c == newline or c == carriage_return or
|
||||
(T == u16 and (c == line_separator or c == paragraph_separator)))
|
||||
{
|
||||
line_end = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const comment = source[comment_start..line_end];
|
||||
if (matchSourceMappingURL(T, comment, alloc)) |url| {
|
||||
// Free previous URL if any
|
||||
if (last_url) |prev| prev.deinit();
|
||||
last_url = url;
|
||||
}
|
||||
|
||||
i = line_end;
|
||||
}
|
||||
// Handle multi-line comment /* */
|
||||
else if (next_char == asterisk) {
|
||||
i += 1;
|
||||
const comment_start = i;
|
||||
|
||||
// Find closing */
|
||||
var found_end = false;
|
||||
while (i + 1 < source.len) : (i += 1) {
|
||||
if (source[i] == asterisk and source[i + 1] == solidus) {
|
||||
const comment = source[comment_start..i];
|
||||
if (matchSourceMappingURL(T, comment, alloc)) |url| {
|
||||
// Free previous URL if any
|
||||
if (last_url) |prev| prev.deinit();
|
||||
last_url = url;
|
||||
}
|
||||
i += 2;
|
||||
found_end = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_end) {
|
||||
// Unclosed comment - ignore rest of file
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Not a comment - check if it's whitespace
|
||||
else {
|
||||
// Back up to check the character before the slash
|
||||
const before_slash = slash_pos;
|
||||
if (before_slash > 0) {
|
||||
var j = before_slash - 1;
|
||||
// Check backwards for non-whitespace on this line
|
||||
while (j > 0) : (j -%= 1) {
|
||||
const c = source[j];
|
||||
if (c == newline or c == carriage_return or
|
||||
(T == u16 and (c == line_separator or c == paragraph_separator)))
|
||||
{
|
||||
// Hit line boundary, this slash starts the line (after whitespace)
|
||||
break;
|
||||
}
|
||||
if (!isWhitespace(T, c)) {
|
||||
// Non-whitespace found - reset last_url per spec
|
||||
if (last_url) |prev| {
|
||||
prev.deinit();
|
||||
last_url = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (j == 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return last_url;
|
||||
}
|
||||
|
||||
// Helper function to match sourceMappingURL pattern in a comment
|
||||
fn matchSourceMappingURL(comptime T: type, comment: []const T, alloc: std.mem.Allocator) ?bun.jsc.ZigString.Slice {
|
||||
// Pattern: ^[@#]\s*sourceMappingURL=(\S*?)\s*$
|
||||
var i: usize = 0;
|
||||
|
||||
// Skip leading whitespace
|
||||
while (i < comment.len and isWhitespace(T, comment[i])) : (i += 1) {}
|
||||
|
||||
if (i >= comment.len) return null;
|
||||
|
||||
// Check for @ or # prefix
|
||||
const at_sign = comptime bun.strings.literal(T, "@")[0];
|
||||
const hash = comptime bun.strings.literal(T, "#")[0];
|
||||
|
||||
if (comment[i] != at_sign and comment[i] != hash) return null;
|
||||
i += 1;
|
||||
|
||||
// Skip whitespace after prefix
|
||||
while (i < comment.len and isWhitespace(T, comment[i])) : (i += 1) {}
|
||||
|
||||
// Check for "sourceMappingURL="
|
||||
const mapping_text = comptime bun.strings.literal(T, "sourceMappingURL=");
|
||||
if (i + mapping_text.len > comment.len) return null;
|
||||
|
||||
const text_part = comment[i .. i + mapping_text.len];
|
||||
if (!std.mem.eql(T, text_part, mapping_text)) return null;
|
||||
|
||||
i += mapping_text.len;
|
||||
|
||||
// Find the URL (non-whitespace characters)
|
||||
const url_start = i;
|
||||
while (i < comment.len and !isWhitespace(T, comment[i])) : (i += 1) {}
|
||||
|
||||
if (url_start == i) return null; // Empty URL
|
||||
|
||||
const url = comment[url_start..i];
|
||||
|
||||
// Verify rest is only whitespace
|
||||
while (i < comment.len) : (i += 1) {
|
||||
if (!isWhitespace(T, comment[i])) return null;
|
||||
}
|
||||
|
||||
// Return the URL as a ZigString.Slice
|
||||
return switch (T) {
|
||||
u8 => bun.jsc.ZigString.Slice.fromUTF8NeverFree(url),
|
||||
u16 => bun.jsc.ZigString.Slice.init(
|
||||
alloc,
|
||||
bun.strings.toUTF8Alloc(alloc, url) catch bun.outOfMemory(),
|
||||
),
|
||||
else => @compileError("Not Supported"),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to check if a character is whitespace
|
||||
fn isWhitespace(comptime T: type, char: T) bool {
|
||||
return switch (char) {
|
||||
'\t', '\n', '\r', ' ', 0x0B, 0x0C => true,
|
||||
else => {
|
||||
if (T == u16) {
|
||||
return switch (char) {
|
||||
0xA0, // non-breaking space
|
||||
0xFEFF, // BOM
|
||||
0x2028, // line separator
|
||||
0x2029, // paragraph separator
|
||||
=> true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// The last two arguments to this specify loading hints
|
||||
pub fn getSourceMapImpl(
|
||||
comptime SourceProviderKind: type,
|
||||
@@ -1066,29 +1258,61 @@ pub fn getSourceMapImpl(
|
||||
defer source.deref();
|
||||
bun.assert(source.tag == .ZigString);
|
||||
|
||||
const found_url = (if (source.is8Bit())
|
||||
findSourceMappingURL(u8, source.latin1(), allocator)
|
||||
else
|
||||
findSourceMappingURL(u16, source.utf16(), allocator)) orelse
|
||||
break :try_inline;
|
||||
const maybe_found_url = found_url: {
|
||||
if (source.is8Bit())
|
||||
break :found_url findSourceMappingURL(u8, source.latin1(), allocator);
|
||||
|
||||
break :found_url findSourceMappingURL(u16, source.utf16(), allocator);
|
||||
};
|
||||
|
||||
const found_url = maybe_found_url orelse break :try_inline;
|
||||
defer found_url.deinit();
|
||||
|
||||
if (bun.strings.hasPrefixComptime(
|
||||
found_url.slice(),
|
||||
"bake://server.map",
|
||||
)) {}
|
||||
|
||||
const parsed = parseUrl(
|
||||
bun.default_allocator,
|
||||
allocator,
|
||||
found_url.slice(),
|
||||
result,
|
||||
) catch |err| {
|
||||
inline_err = err;
|
||||
break :try_inline;
|
||||
};
|
||||
|
||||
break :parsed .{
|
||||
.is_inline_map,
|
||||
parseUrl(
|
||||
bun.default_allocator,
|
||||
allocator,
|
||||
found_url.slice(),
|
||||
result,
|
||||
) catch |err| {
|
||||
inline_err = err;
|
||||
break :try_inline;
|
||||
},
|
||||
parsed,
|
||||
};
|
||||
}
|
||||
|
||||
// try to load a .map file
|
||||
if (load_hint != .is_inline_map) try_external: {
|
||||
if (comptime SourceProviderKind == DevServerSourceProvider) {
|
||||
// For DevServerSourceProvider, get the source map JSON directly
|
||||
const source_map_data = provider.getSourceMapJSON();
|
||||
|
||||
if (source_map_data.length == 0) {
|
||||
break :try_external;
|
||||
}
|
||||
|
||||
const json_slice = source_map_data.ptr[0..source_map_data.length];
|
||||
|
||||
// Parse the JSON source map
|
||||
break :parsed .{
|
||||
.is_external_map,
|
||||
parseJSON(
|
||||
bun.default_allocator,
|
||||
allocator,
|
||||
json_slice,
|
||||
result,
|
||||
) catch return null,
|
||||
};
|
||||
}
|
||||
|
||||
if (comptime SourceProviderKind == BakeSourceProvider) fallback_to_normal: {
|
||||
const global = bun.jsc.VirtualMachine.get().global;
|
||||
// If we're using bake's production build the global object will
|
||||
@@ -1242,6 +1466,39 @@ pub const BakeSourceProvider = opaque {
|
||||
}
|
||||
};
|
||||
|
||||
pub const DevServerSourceProvider = opaque {
|
||||
pub const SourceMapData = extern struct {
|
||||
ptr: [*]const u8,
|
||||
length: usize,
|
||||
};
|
||||
|
||||
extern fn DevServerSourceProvider__getSourceSlice(*DevServerSourceProvider) bun.String;
|
||||
extern fn DevServerSourceProvider__getSourceMapJSON(*DevServerSourceProvider) SourceMapData;
|
||||
|
||||
pub const getSourceSlice = DevServerSourceProvider__getSourceSlice;
|
||||
pub const getSourceMapJSON = DevServerSourceProvider__getSourceMapJSON;
|
||||
|
||||
pub fn toSourceContentPtr(this: *DevServerSourceProvider) ParsedSourceMap.SourceContentPtr {
|
||||
return ParsedSourceMap.SourceContentPtr.fromDevServerProvider(this);
|
||||
}
|
||||
|
||||
/// The last two arguments to this specify loading hints
|
||||
pub fn getSourceMap(
|
||||
provider: *DevServerSourceProvider,
|
||||
source_filename: []const u8,
|
||||
load_hint: SourceMap.SourceMapLoadHint,
|
||||
result: SourceMap.ParseUrlResultHint,
|
||||
) ?SourceMap.ParseUrl {
|
||||
return getSourceMapImpl(
|
||||
DevServerSourceProvider,
|
||||
provider,
|
||||
source_filename,
|
||||
load_hint,
|
||||
result,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// The sourcemap spec says line and column offsets are zero-based
|
||||
pub const LineColumnOffset = struct {
|
||||
/// The zero-based line offset
|
||||
|
||||
150
test/bake/dev/server-sourcemap.test.ts
Normal file
150
test/bake/dev/server-sourcemap.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { expect } from "bun:test";
|
||||
import { devTest } from "../bake-harness";
|
||||
|
||||
devTest("server-side source maps show correct error lines", {
|
||||
files: {
|
||||
"pages/[...slug].tsx": `export default async function MyPage(params) {
|
||||
myFunc();
|
||||
return <h1>{JSON.stringify(params)}</h1>;
|
||||
}
|
||||
|
||||
function myFunc() {
|
||||
throw new Error("Test error for source maps!");
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
{
|
||||
params: {
|
||||
slug: ["test-error"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}`,
|
||||
},
|
||||
framework: "react",
|
||||
async test(dev) {
|
||||
// Make a request that will trigger the error
|
||||
await dev.fetch("/test-error").catch(() => {});
|
||||
|
||||
// Give it a moment to process the error
|
||||
await Bun.sleep(1000);
|
||||
|
||||
// The output we saw shows the stack trace with correct source mapping
|
||||
// We need to check that the error shows the right file:line:column
|
||||
const lines = dev.output.lines.join("\n");
|
||||
|
||||
// Check that we got the error
|
||||
expect(lines).toContain("Test error for source maps!");
|
||||
|
||||
// Check that the stack trace shows correct file and line numbers
|
||||
// The source maps are working if we see the correct patterns
|
||||
// We need to check for the patterns because ANSI codes might be embedded
|
||||
const hasCorrectThrowLine = lines.includes("myFunc") && lines.includes("7") && lines.includes("9");
|
||||
const hasCorrectCallLine = lines.includes("MyPage") && lines.includes("2") && lines.includes("3");
|
||||
const hasCorrectFileName = lines.includes("/pages/[...slug].tsx");
|
||||
|
||||
expect(hasCorrectThrowLine).toBe(true);
|
||||
expect(hasCorrectCallLine).toBe(true);
|
||||
expect(hasCorrectFileName).toBe(true);
|
||||
},
|
||||
timeoutMultiplier: 2, // Give more time for the test
|
||||
});
|
||||
|
||||
devTest("server-side source maps work with HMR updates", {
|
||||
files: {
|
||||
"pages/error-page.tsx": `export default function ErrorPage() {
|
||||
return <div>Initial content</div>;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [{ params: {} }],
|
||||
};
|
||||
}`,
|
||||
},
|
||||
framework: "react",
|
||||
async test(dev) {
|
||||
// First fetch should work
|
||||
const response1 = await dev.fetch("/error-page");
|
||||
expect(response1.status).toBe(200);
|
||||
expect(await response1.text()).toContain("Initial content");
|
||||
|
||||
// Update the file to throw an error
|
||||
await dev.write("pages/error-page.tsx", `export default function ErrorPage() {
|
||||
throwError();
|
||||
return <div>Updated content</div>;
|
||||
}
|
||||
|
||||
function throwError() {
|
||||
throw new Error("HMR error test");
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [{ params: {} }],
|
||||
};
|
||||
}`);
|
||||
|
||||
// Wait for the rebuild
|
||||
await dev.waitForHmr();
|
||||
|
||||
// Second fetch should error
|
||||
await dev.fetch("/error-page").catch(() => {});
|
||||
|
||||
// Wait for error output
|
||||
await dev.output.waitForLine(/HMR error test/);
|
||||
|
||||
// Check source map points to correct lines after HMR
|
||||
const lines = dev.output.lines.join("\n");
|
||||
const hasCorrectThrowLine = lines.includes("throwError") && lines.includes("7") && lines.includes("9");
|
||||
const hasCorrectCallLine = lines.includes("ErrorPage") && lines.includes("2") && lines.includes("3");
|
||||
|
||||
expect(hasCorrectThrowLine).toBe(true);
|
||||
expect(hasCorrectCallLine).toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
devTest("server-side source maps handle nested imports", {
|
||||
files: {
|
||||
"pages/nested.tsx": `import { doSomething } from "../lib/utils";
|
||||
|
||||
export default function NestedPage() {
|
||||
const result = doSomething();
|
||||
return <div>{result}</div>;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [{ params: {} }],
|
||||
};
|
||||
}`,
|
||||
"lib/utils.ts": `export function doSomething() {
|
||||
return helperFunction();
|
||||
}
|
||||
|
||||
function helperFunction() {
|
||||
throw new Error("Nested error");
|
||||
}`,
|
||||
},
|
||||
framework: "react",
|
||||
async test(dev) {
|
||||
// Make request that triggers error
|
||||
await dev.fetch("/nested").catch(() => {});
|
||||
|
||||
// Wait for error output
|
||||
await dev.output.waitForLine(/Nested error/);
|
||||
|
||||
// Check that stack trace shows both files with correct lines
|
||||
const lines = dev.output.lines.join("\n");
|
||||
const hasUtilsThrowLine = lines.includes("helperFunction") && lines.includes("6") && lines.includes("9");
|
||||
const hasUtilsCallLine = lines.includes("doSomething") && lines.includes("2");
|
||||
const hasPageCallLine = lines.includes("NestedPage") && lines.includes("4");
|
||||
|
||||
expect(hasUtilsThrowLine).toBe(true);
|
||||
expect(hasUtilsCallLine).toBe(true);
|
||||
expect(hasPageCallLine).toBe(true);
|
||||
},
|
||||
});
|
||||
@@ -324,3 +324,66 @@ export { Markdoc as default };`,
|
||||
expect(await c1.elemText("h1")).toBe("Welcome to SSG");
|
||||
},
|
||||
});
|
||||
|
||||
devTest("SSG pages router - catch-all routes [...slug]", {
|
||||
framework: "react",
|
||||
files: {
|
||||
"pages/[...slug].tsx": `
|
||||
const CatchAllPage: Bun.SSGPage = ({ params }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Catch-all Route</h1>
|
||||
<p id="params">{JSON.stringify(params)}</p>
|
||||
<ul>
|
||||
{params.slug && Array.isArray(params.slug) ? (
|
||||
params.slug.map((segment, index) => (
|
||||
<li key={index}>{segment}</li>
|
||||
))
|
||||
) : (
|
||||
<li>No slug array</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatchAllPage;
|
||||
|
||||
export const getStaticPaths: Bun.GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: [
|
||||
{ params: { slug: ["docs"] } },
|
||||
{ params: { slug: ["docs", "getting-started"] } },
|
||||
{ params: { slug: ["docs", "api", "reference"] } },
|
||||
{ params: { slug: ["blog", "2024", "january", "new-features"] } },
|
||||
],
|
||||
};
|
||||
};
|
||||
`,
|
||||
},
|
||||
async test(dev) {
|
||||
// Test single segment
|
||||
await using c1 = await dev.client("/docs");
|
||||
expect(await c1.elemText("h1")).toBe("Catch-all Route");
|
||||
expect(await c1.elemText("#params")).toBe('{"slug":"docs"}');
|
||||
expect(await c1.elemsText("li")).toEqual(["No slug array"]);
|
||||
|
||||
// Test two segments
|
||||
await using c2 = await dev.client("/docs/getting-started");
|
||||
expect(await c2.elemText("h1")).toBe("Catch-all Route");
|
||||
expect(await c2.elemText("#params")).toBe('{"slug":["docs","getting-started"]}');
|
||||
expect(await c2.elemsText("li")).toEqual(["docs", "getting-started"]);
|
||||
|
||||
// Test three segments
|
||||
await using c3 = await dev.client("/docs/api/reference");
|
||||
expect(await c3.elemText("h1")).toBe("Catch-all Route");
|
||||
expect(await c3.elemText("#params")).toBe('{"slug":["docs","api","reference"]}');
|
||||
expect(await c3.elemsText("li")).toEqual(["docs", "api", "reference"]);
|
||||
|
||||
// Test four segments
|
||||
await using c4 = await dev.client("/blog/2024/january/new-features");
|
||||
expect(await c4.elemText("h1")).toBe("Catch-all Route");
|
||||
expect(await c4.elemText("#params")).toBe('{"slug":["blog","2024","january","new-features"]}');
|
||||
expect(await c4.elemsText("li")).toEqual(["blog", "2024", "january", "new-features"]);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user