mirror of
https://github.com/oven-sh/bun
synced 2026-02-26 19:47:19 +01:00
Compare commits
155 Commits
claude/fix
...
zack/ssg-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4deef81f19 | ||
|
|
b3fe9c0cd3 | ||
|
|
294368565b | ||
|
|
8b7b4030e7 | ||
|
|
8020258615 | ||
|
|
347742d03c | ||
|
|
7b132db307 | ||
|
|
93c9d9bfcc | ||
|
|
d63defb521 | ||
|
|
2173d308d6 | ||
|
|
dfafe46f71 | ||
|
|
c9f8a02773 | ||
|
|
672fa64d92 | ||
|
|
11eddb2cf1 | ||
|
|
0cc63255b1 | ||
|
|
71a5f9fb26 | ||
|
|
b257967189 | ||
|
|
4e629753cc | ||
|
|
5aa5906ccf | ||
|
|
108f21ae82 | ||
|
|
a591efdb67 | ||
|
|
2648cb7ef6 | ||
|
|
457b4a46b3 | ||
|
|
3b2bea9820 | ||
|
|
58ecff4e0c | ||
|
|
43a7b6518a | ||
|
|
f03a1ab1c9 | ||
|
|
1e3057045c | ||
|
|
e92fd08930 | ||
|
|
deb3e94948 | ||
|
|
1b01f7c0da | ||
|
|
5e256e4b1f | ||
|
|
388f700b11 | ||
|
|
f145d8c30c | ||
|
|
9d679811cd | ||
|
|
cda3eb5396 | ||
|
|
b17dccc6e0 | ||
|
|
dbe15d3020 | ||
|
|
dab797b834 | ||
|
|
731f42ca72 | ||
|
|
f33a852a80 | ||
|
|
f5122bdbf1 | ||
|
|
916d44fc45 | ||
|
|
17a51c93e3 | ||
|
|
421a4f37cd | ||
|
|
a58a87b606 | ||
|
|
99dd08bccb | ||
|
|
2166f0c200 | ||
|
|
1a0a081e75 | ||
|
|
2eb33628d1 | ||
|
|
56e9c92b4a | ||
|
|
34cfdf039a | ||
|
|
1920a7c63c | ||
|
|
d56005b520 | ||
|
|
9c5c4edac4 | ||
|
|
199781bf4f | ||
|
|
ffeb21c49b | ||
|
|
7afcc8416f | ||
|
|
1ef578a0b4 | ||
|
|
8be4fb61d0 | ||
|
|
208ac7fb60 | ||
|
|
29b6faadf8 | ||
|
|
99df2e071f | ||
|
|
a3d91477a8 | ||
|
|
52c3e2e3f8 | ||
|
|
a7e95718ac | ||
|
|
db2960d27b | ||
|
|
bbfac709cc | ||
|
|
41fbeacee1 | ||
|
|
24b2929c9a | ||
|
|
bf992731c6 | ||
|
|
eafc04cc5d | ||
|
|
95cacdc6be | ||
|
|
6cf46e67f6 | ||
|
|
f28670ac68 | ||
|
|
0df21d7f30 | ||
|
|
39e7e55802 | ||
|
|
0919e45c23 | ||
|
|
fd41a41ab9 | ||
|
|
fc06e1cf14 | ||
|
|
1778713cbf | ||
|
|
c10d184448 | ||
|
|
c8b21f207d | ||
|
|
6357978b90 | ||
|
|
9504d14b7a | ||
|
|
43054c9a7f | ||
|
|
2fad71dd45 | ||
|
|
8b35b5634a | ||
|
|
8e0cf4c5e0 | ||
|
|
5dcf8a8076 | ||
|
|
d6b155f056 | ||
|
|
5f8393cc99 | ||
|
|
ee7dfefbe0 | ||
|
|
6d132e628f | ||
|
|
ae9ecc99c9 | ||
|
|
eeecbfa790 | ||
|
|
862f7378e4 | ||
|
|
636e597b60 | ||
|
|
6abb9f81eb | ||
|
|
aa33b11a7a | ||
|
|
21266f5263 | ||
|
|
c5fc729fde | ||
|
|
03d1e48004 | ||
|
|
19b9c4a850 | ||
|
|
842503ecb1 | ||
|
|
cb9c45c26c | ||
|
|
917dcc846f | ||
|
|
0fb277a56e | ||
|
|
c343aca21e | ||
|
|
e89a0f3807 | ||
|
|
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 |
@@ -307,7 +307,7 @@ def __lldb_init_module(debugger, _=None):
|
||||
debugger.HandleCommand('type category define --language c99 bun')
|
||||
|
||||
# Initialize Bun Data Structures
|
||||
add(debugger, category='bun', regex=True, type='^baby_list\\.BabyList\\(.*\\)$', identifier='bun_BabyList', synth=True, expand=True, summary=True)
|
||||
add(debugger, category='bun', regex=True, type='.*baby_list\\.BabyList\\(.*\\)$', identifier='bun_BabyList', synth=True, expand=True, summary=True)
|
||||
|
||||
# Add WTFStringImpl pretty printer - try multiple possible type names
|
||||
add(debugger, category='bun', type='WTFStringImpl', identifier='WTFStringImpl', summary=True)
|
||||
|
||||
@@ -1240,6 +1240,74 @@ pub fn NewParser_(
|
||||
parts: *ListManaged(js_ast.Part),
|
||||
) !void {
|
||||
bun.assert(!p.response_ref.isNull());
|
||||
|
||||
// If this is a bake production build, we don't use the bun_app_namespace_ref
|
||||
if (!p.options.features.hot_module_reloading) {
|
||||
bun.assert(p.bun_app_namespace_ref.isNull());
|
||||
const allocator = p.allocator;
|
||||
|
||||
const import_path = "bun:app";
|
||||
|
||||
const import_record_i = p.addImportRecordByRange(.stmt, logger.Range.None, import_path);
|
||||
|
||||
var declared_symbols = DeclaredSymbol.List{};
|
||||
try declared_symbols.ensureTotalCapacity(allocator, 1);
|
||||
|
||||
var stmts = try allocator.alloc(Stmt, 1);
|
||||
|
||||
const clause_items = try allocator.dupe(js_ast.ClauseItem, &.{
|
||||
js_ast.ClauseItem{
|
||||
.alias = "Response",
|
||||
.original_name = "Response",
|
||||
.alias_loc = logger.Loc{},
|
||||
.name = LocRef{ .ref = p.response_ref, .loc = logger.Loc{} },
|
||||
},
|
||||
});
|
||||
|
||||
declared_symbols.appendAssumeCapacity(DeclaredSymbol{
|
||||
.ref = p.response_ref,
|
||||
.is_top_level = true,
|
||||
});
|
||||
|
||||
// ensure every e_import_identifier holds the namespace
|
||||
// const symbol = &p.symbols.items[p.response_ref.inner_index];
|
||||
// bun.assert(symbol.namespace_alias != null);
|
||||
// symbol.namespace_alias.?.import_record_index = import_record_i;
|
||||
|
||||
try p.is_import_item.put(allocator, p.response_ref, {});
|
||||
try p.named_imports.put(allocator, p.response_ref, js_ast.NamedImport{
|
||||
.alias = "Response",
|
||||
.alias_loc = logger.Loc{},
|
||||
.namespace_ref = null,
|
||||
.import_record_index = import_record_i,
|
||||
});
|
||||
|
||||
stmts[0] = p.s(
|
||||
S.Import{
|
||||
.namespace_ref = Ref.None,
|
||||
.items = clause_items,
|
||||
.import_record_index = import_record_i,
|
||||
.is_single_line = true,
|
||||
},
|
||||
logger.Loc{},
|
||||
);
|
||||
|
||||
var import_records = try allocator.alloc(u32, 1);
|
||||
import_records[0] = import_record_i;
|
||||
|
||||
// This import is placed in a part before the main code, however
|
||||
// the bundler ends up re-ordering this to be after... The order
|
||||
// does not matter as ESM imports are always hoisted.
|
||||
parts.append(js_ast.Part{
|
||||
.stmts = stmts,
|
||||
.declared_symbols = declared_symbols,
|
||||
.import_record_indices = bun.BabyList(u32).fromOwnedSlice(import_records),
|
||||
.tag = .runtime,
|
||||
}) catch unreachable;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bun.assert(!p.bun_app_namespace_ref.isNull());
|
||||
const allocator = p.allocator;
|
||||
|
||||
@@ -2110,16 +2178,18 @@ pub fn NewParser_(
|
||||
.none, .client_side => {},
|
||||
else => {
|
||||
p.response_ref = try p.declareGeneratedSymbol(.import, "Response");
|
||||
p.bun_app_namespace_ref = try p.newSymbol(
|
||||
.other,
|
||||
"import_bun_app",
|
||||
);
|
||||
const symbol = &p.symbols.items[p.response_ref.inner_index];
|
||||
symbol.namespace_alias = .{
|
||||
.namespace_ref = p.bun_app_namespace_ref,
|
||||
.alias = "Response",
|
||||
.import_record_index = std.math.maxInt(u32),
|
||||
};
|
||||
if (p.options.features.hot_module_reloading) {
|
||||
p.bun_app_namespace_ref = try p.newSymbol(
|
||||
.other,
|
||||
"import_bun_app",
|
||||
);
|
||||
const symbol = &p.symbols.items[p.response_ref.inner_index];
|
||||
symbol.namespace_alias = .{
|
||||
.namespace_ref = p.bun_app_namespace_ref,
|
||||
.alias = "Response",
|
||||
.import_record_index = std.math.maxInt(u32),
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
31
src/bake.zig
31
src/bake.zig
@@ -6,6 +6,11 @@ pub const production = @import("./bake/production.zig");
|
||||
pub const DevServer = @import("./bake/DevServer.zig");
|
||||
pub const FrameworkRouter = @import("./bake/FrameworkRouter.zig");
|
||||
|
||||
pub const Manifest = @import("./bake/prod/Manifest.zig");
|
||||
pub const ProductionServerState = @import("./bake/prod/ProductionServerState.zig");
|
||||
pub const SSRRouteList = @import("./bake/prod/SSRRouteList.zig");
|
||||
pub const ProductionServerMethods = @import("./bake/prod/ProductionServerMethods.zig").ProductionServerMethods;
|
||||
|
||||
/// export default { app: ... };
|
||||
pub const api_name = "app";
|
||||
|
||||
@@ -866,6 +871,16 @@ pub fn getHmrRuntime(side: Side) callconv(bun.callconv_inline) HmrRuntime {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getProductionRuntime(side: Side) callconv(bun.callconv_inline) HmrRuntime {
|
||||
return switch (side) {
|
||||
.server => if (Environment.codegen_embed)
|
||||
.init(@embedFile("bake-codegen/bake.production-server.js"))
|
||||
else
|
||||
.init(bun.runtimeEmbedFile(.codegen, "bake.production-server.js")),
|
||||
.client => @panic("Production client runtime not implemented"),
|
||||
};
|
||||
}
|
||||
|
||||
pub const Mode = enum {
|
||||
development,
|
||||
production_dynamic,
|
||||
@@ -969,11 +984,23 @@ pub const PatternBuffer = struct {
|
||||
pb.prepend(text);
|
||||
pb.prepend("/");
|
||||
},
|
||||
.param, .catch_all, .catch_all_optional => |name| {
|
||||
.param => |name| {
|
||||
pb.prepend(name);
|
||||
pb.prepend("/:");
|
||||
},
|
||||
.group => {},
|
||||
.catch_all => |name| {
|
||||
pb.prepend(name);
|
||||
pb.prepend("/:*");
|
||||
},
|
||||
.catch_all_optional => |name| {
|
||||
pb.prepend(name);
|
||||
pb.prepend("/:*?");
|
||||
},
|
||||
.group => {
|
||||
pb.prepend(")");
|
||||
pb.prepend(part.group);
|
||||
pb.prepend("/(");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ extern "C" JSC::JSPromise* BakeRenderRoutesForProdStatic(
|
||||
JSC::JSValue renderStatic,
|
||||
JSC::JSValue getParams,
|
||||
JSC::JSValue clientEntryUrl,
|
||||
JSC::JSValue routerTypeRoots,
|
||||
JSC::JSValue routerTypeServerEntrypoints,
|
||||
JSC::JSValue serverRuntime,
|
||||
JSC::JSValue pattern,
|
||||
JSC::JSValue files,
|
||||
JSC::JSValue typeAndFlags,
|
||||
@@ -31,6 +34,9 @@ extern "C" JSC::JSPromise* BakeRenderRoutesForProdStatic(
|
||||
args.append(renderStatic);
|
||||
args.append(getParams);
|
||||
args.append(clientEntryUrl);
|
||||
args.append(routerTypeRoots);
|
||||
args.append(routerTypeServerEntrypoints);
|
||||
args.append(serverRuntime);
|
||||
args.append(pattern);
|
||||
args.append(files);
|
||||
args.append(typeAndFlags);
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
#include "root.h"
|
||||
#include "headers-handwritten.h"
|
||||
|
||||
namespace JSC {
|
||||
class JSGlobalObject;
|
||||
class JSPromise;
|
||||
class JSValue;
|
||||
} // namespace JSC
|
||||
|
||||
namespace Bake {
|
||||
|
||||
extern "C" JSC::JSPromise* BakeRenderRoutesForProdStatic(
|
||||
JSC::JSGlobalObject* global,
|
||||
BunString outBase,
|
||||
JSC::JSValue allServerFiles,
|
||||
JSC::JSValue renderStatic,
|
||||
JSC::JSValue getParams,
|
||||
JSC::JSValue clientEntryUrl,
|
||||
JSC::JSValue routerTypeRoots,
|
||||
JSC::JSValue routerTypeServerEntrypoints,
|
||||
JSC::JSValue serverRuntime,
|
||||
JSC::JSValue pattern,
|
||||
JSC::JSValue files,
|
||||
JSC::JSValue typeAndFlags,
|
||||
JSC::JSValue sourceRouteFiles,
|
||||
JSC::JSValue paramInformation,
|
||||
JSC::JSValue styles);
|
||||
|
||||
} // namespace Bake
|
||||
|
||||
@@ -53,6 +53,41 @@ extern "C" JSC::EncodedJSValue BakeLoadInitialServerCode(JSC::JSGlobalObject* gl
|
||||
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::profiledCall(global, JSC::ProfilingReason::API, fn, callData, JSC::jsUndefined(), args)));
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue BakeLoadProductionServerCode(JSC::JSGlobalObject* global, BunString source, BunString path) {
|
||||
auto& vm = JSC::getVM(global);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
String pathString = path.toWTFString();
|
||||
String urlString = makeString("file://"_s, pathString);
|
||||
|
||||
JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(urlString));
|
||||
JSC::SourceCode sourceCode = JSC::SourceCode(SourceProvider::create(
|
||||
global,
|
||||
source.toWTFString(),
|
||||
origin,
|
||||
WTFMove(urlString),
|
||||
WTF::TextPosition(),
|
||||
JSC::SourceProviderSourceType::Program
|
||||
));
|
||||
|
||||
// Execute the program to get the IIFE function
|
||||
JSC::JSValue fnValue = vm.interpreter.executeProgram(sourceCode, global, global);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
RELEASE_ASSERT(fnValue);
|
||||
|
||||
// The result should be a function (the IIFE)
|
||||
JSC::JSFunction* fn = jsCast<JSC::JSFunction*>(fnValue);
|
||||
JSC::CallData callData = JSC::getCallData(fn);
|
||||
|
||||
// Pass import.meta as the argument to the IIFE
|
||||
JSC::MarkedArgumentBuffer args;
|
||||
args.append(Zig::ImportMetaObject::create(global, pathString));
|
||||
|
||||
// Call the IIFE with import.meta to get the exports object
|
||||
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::profiledCall(global, JSC::ProfilingReason::API, fn, callData, JSC::jsUndefined(), args)));
|
||||
}
|
||||
|
||||
extern "C" JSC::JSInternalPromise* BakeLoadModuleByKey(GlobalObject* global, JSC::JSString* key) {
|
||||
return global->moduleLoader()->loadAndEvaluateModule(global, key, JSC::jsUndefined(), JSC::jsUndefined());
|
||||
}
|
||||
|
||||
@@ -1313,7 +1313,7 @@ fn appendRouteEntryPointsIfNotStale(dev: *DevServer, entry_points: *EntryPointLi
|
||||
|
||||
extern "C" fn Bake__getEnsureAsyncLocalStorageInstanceJSFunction(global: *bun.jsc.JSGlobalObject) callconv(jsc.conv) bun.jsc.JSValue;
|
||||
extern "C" fn Bake__getBundleNewRouteJSFunction(global: *bun.jsc.JSGlobalObject) callconv(jsc.conv) bun.jsc.JSValue;
|
||||
extern "C" fn Bake__getNewRouteParamsJSFunction(global: *bun.jsc.JSGlobalObject) callconv(jsc.conv) bun.jsc.JSValue;
|
||||
extern "C" fn Bake__getDevNewRouteParamsJSFunction(global: *bun.jsc.JSGlobalObject) callconv(jsc.conv) bun.jsc.JSValue;
|
||||
|
||||
fn computeArgumentsForFrameworkRequest(
|
||||
dev: *DevServer,
|
||||
@@ -1405,7 +1405,7 @@ fn computeArgumentsForFrameworkRequest(
|
||||
// setAsyncLocalStorage
|
||||
.set_async_local_storage = if (first_request) Bake__getEnsureAsyncLocalStorageInstanceJSFunction(dev.vm.global) else JSValue.null,
|
||||
.bundle_new_route = if (first_request) Bake__getBundleNewRouteJSFunction(dev.vm.global) else JSValue.null,
|
||||
.new_route_params = if (first_request) Bake__getNewRouteParamsJSFunction(dev.vm.global) else JSValue.null,
|
||||
.new_route_params = if (first_request) Bake__getDevNewRouteParamsJSFunction(dev.vm.global) else JSValue.null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1438,13 +1438,14 @@ fn onFrameworkRequestWithBundle(
|
||||
defer url.deinit();
|
||||
|
||||
// Extract pathname from URL (remove protocol, host, query, hash)
|
||||
const pathname = extractPathnameFromUrl(url.byteSlice());
|
||||
const pathname = FrameworkRouter.extractPathnameFromUrl(url.byteSlice());
|
||||
|
||||
// Create params JSValue
|
||||
// TODO: lazy structure caching since we are making these objects a lot
|
||||
const params_js_value = if (dev.router.matchSlow(pathname, ¶ms)) |_| blk: {
|
||||
break :blk params.toJS(dev.vm.global);
|
||||
} else JSValue.null;
|
||||
const params_js_value = if (dev.router.matchSlow(pathname, ¶ms)) |_|
|
||||
params.toJS(dev.vm.global)
|
||||
else
|
||||
JSValue.null;
|
||||
|
||||
const server_request_callback = dev.server_fetch_function_callback.get() orelse
|
||||
unreachable; // did not initialize server code
|
||||
@@ -4515,7 +4516,7 @@ fn bundleNewRouteJSFunctionImpl(global: *bun.jsc.JSGlobalObject, request_ptr: *a
|
||||
return global.throw("Request context does not belong to dev server", .{});
|
||||
};
|
||||
// Extract pathname from URL (remove protocol, host, query, hash)
|
||||
const pathname = extractPathnameFromUrl(url.byteSlice());
|
||||
const pathname = FrameworkRouter.extractPathnameFromUrl(url.byteSlice());
|
||||
|
||||
if (pathname.len == 0 or pathname[0] != '/') {
|
||||
return global.throw("Invalid path \"{s}\" it should be non-empty and start with a slash", .{pathname});
|
||||
@@ -4579,7 +4580,7 @@ pub fn createDevServerFrameworkRequestArgsObject(
|
||||
);
|
||||
}
|
||||
|
||||
export fn Bake__getNewRouteParamsJSFunctionImpl(global: *bun.jsc.JSGlobalObject, callframe: *jsc.CallFrame) callconv(jsc.conv) bun.jsc.JSValue {
|
||||
export fn Bake__getDevNewRouteParamsJSFunctionImpl(global: *bun.jsc.JSGlobalObject, callframe: *jsc.CallFrame) callconv(jsc.conv) bun.jsc.JSValue {
|
||||
return bun.jsc.toJSHostCall(global, @src(), newRouteParamsForBundlePromiseForJS, .{ global, callframe });
|
||||
}
|
||||
|
||||
@@ -4617,7 +4618,7 @@ fn newRouteParamsForBundlePromise(
|
||||
const route_bundle = dev.routeBundlePtr(route_bundle_index);
|
||||
const framework_bundle = &route_bundle.data.framework;
|
||||
|
||||
const pathname = extractPathnameFromUrl(url);
|
||||
const pathname = FrameworkRouter.extractPathnameFromUrl(url);
|
||||
|
||||
var params: FrameworkRouter.MatchedParams = undefined;
|
||||
const route_index = dev.router.matchSlow(pathname, ¶ms) orelse return dev.vm.global.throw("No route found for path: {s}", .{pathname});
|
||||
@@ -4643,26 +4644,6 @@ fn newRouteParamsForBundlePromise(
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this is shitty
|
||||
fn extractPathnameFromUrl(url: []const u8) []const u8 {
|
||||
// Extract pathname from URL (remove protocol, host, query, hash)
|
||||
var pathname = if (std.mem.indexOf(u8, url, "://")) |proto_end| blk: {
|
||||
const after_proto = url[proto_end + 3 ..];
|
||||
break :blk after_proto;
|
||||
} else url;
|
||||
|
||||
if (std.mem.indexOfScalar(u8, pathname, '/')) |path_start| {
|
||||
const path_with_query = pathname[path_start..];
|
||||
// Remove query string and hash
|
||||
const query_index = std.mem.indexOfScalar(u8, path_with_query, '?') orelse path_with_query.len;
|
||||
const hash_index = std.mem.indexOfScalar(u8, path_with_query, '#') orelse path_with_query.len;
|
||||
const end = @min(query_index, hash_index);
|
||||
pathname = path_with_query[0..end];
|
||||
}
|
||||
|
||||
return pathname;
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const Output = bun.Output;
|
||||
|
||||
@@ -57,7 +57,7 @@ pattern_string_arena: bun.ArenaAllocator,
|
||||
/// - As little memory indirection as possible.
|
||||
/// - Routes cannot be updated after serilaization.
|
||||
pub const Serialized = struct {
|
||||
// TODO:
|
||||
// TODO
|
||||
};
|
||||
|
||||
const StaticRouteMap = bun.StringArrayHashMapUnmanaged(Route.Index);
|
||||
@@ -496,7 +496,7 @@ pub const Style = union(enum) {
|
||||
pub const UiOrRoutes = enum { ui, routes };
|
||||
const NextRoutingConvention = enum { app, pages };
|
||||
|
||||
pub fn parse(style: Style, file_path: []const u8, ext: []const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern {
|
||||
pub fn parse(style: Style, file_path: []const u8, ext: ?[]const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern {
|
||||
bun.assert(file_path[0] == '/');
|
||||
|
||||
return switch (style) {
|
||||
@@ -513,8 +513,8 @@ pub const Style = union(enum) {
|
||||
|
||||
/// Implements the pages router parser from Next.js:
|
||||
/// https://nextjs.org/docs/getting-started/project-structure#pages-routing-conventions
|
||||
pub fn parseNextJsPages(file_path_raw: []const u8, ext: []const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern {
|
||||
var file_path = file_path_raw[0 .. file_path_raw.len - ext.len];
|
||||
pub fn parseNextJsPages(file_path_raw: []const u8, ext: ?[]const u8, log: *TinyLog, allow_layouts: bool, arena: Allocator) !?ParsedPattern {
|
||||
var file_path = if (ext) |e| file_path_raw[0 .. file_path_raw.len - e.len] else file_path_raw;
|
||||
var kind: ParsedPattern.Kind = .page;
|
||||
if (strings.hasSuffixComptime(file_path, "/index")) {
|
||||
file_path.len -= "/index".len;
|
||||
@@ -537,16 +537,15 @@ pub const Style = union(enum) {
|
||||
/// https://nextjs.org/docs/getting-started/project-structure#app-routing-conventions
|
||||
pub fn parseNextJsApp(
|
||||
file_path_raw: []const u8,
|
||||
ext: []const u8,
|
||||
ext: ?[]const u8,
|
||||
log: *TinyLog,
|
||||
allow_layouts: bool,
|
||||
arena: Allocator,
|
||||
comptime extract: UiOrRoutes,
|
||||
) !?ParsedPattern {
|
||||
const without_ext = file_path_raw[0 .. file_path_raw.len - ext.len];
|
||||
const without_ext = if (ext) |e| file_path_raw[0 .. file_path_raw.len - e.len] else file_path_raw;
|
||||
const basename = std.fs.path.basename(without_ext);
|
||||
const loader = bun.options.Loader.fromString(ext) orelse
|
||||
return null;
|
||||
const loader = if (ext) |e| bun.options.Loader.fromString(e) orelse return null else return null;
|
||||
|
||||
// TODO: opengraph-image and metadata friends
|
||||
if (!loader.isJavaScriptLike())
|
||||
@@ -687,7 +686,31 @@ pub const Style = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
const InsertError = error{ RouteCollision, OutOfMemory };
|
||||
const InsertError = error{ RouteCollision, OutOfMemory, InvalidRouteParam };
|
||||
|
||||
/// Validates that a route part doesn't violate any constraints.
|
||||
/// Currently enforces that route parameters cannot be numeric values (e.g., "0", "12")
|
||||
/// because numeric keys conflict with JSC object structure optimizations.
|
||||
fn validateRoutePart(part: Part) InsertError!void {
|
||||
switch (part) {
|
||||
.param, .catch_all, .catch_all_optional => |name| {
|
||||
// Check if the parameter name is a numeric string
|
||||
if (name.len > 0) {
|
||||
// Check if all characters are digits
|
||||
for (name) |c| {
|
||||
if (c < '0' or c > '9') {
|
||||
// Not a number, it's valid
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we got here, all characters are digits - this is invalid
|
||||
return error.InvalidRouteParam;
|
||||
}
|
||||
},
|
||||
.text, .group => {},
|
||||
}
|
||||
}
|
||||
|
||||
const InsertKind = enum {
|
||||
static,
|
||||
dynamic,
|
||||
@@ -717,7 +740,7 @@ pub fn insert(
|
||||
ctx: InsertionContext,
|
||||
/// When `error.RouteCollision` is returned, this is set to the existing file index.
|
||||
out_colliding_file_id: *OpaqueFileId,
|
||||
) InsertError!void {
|
||||
) InsertError!Route.Index {
|
||||
// The root route is the index of the type
|
||||
const root_route = Type.rootRouteIndex(ty);
|
||||
|
||||
@@ -745,6 +768,7 @@ pub fn insert(
|
||||
}
|
||||
|
||||
// Must add to this child
|
||||
try validateRoutePart(current_part);
|
||||
var new_route_index = try fr.newRoute(alloc, .{
|
||||
.part = current_part,
|
||||
.type = ty,
|
||||
@@ -763,6 +787,7 @@ pub fn insert(
|
||||
// Build each part out as another node in the routing graph. This makes
|
||||
// inserting routes simpler to implement, but could technically be avoided.
|
||||
while (input_it.next()) |next_part| {
|
||||
try validateRoutePart(next_part);
|
||||
const newer_route_index = try fr.newRoute(alloc, .{
|
||||
.part = next_part,
|
||||
.type = ty,
|
||||
@@ -784,7 +809,7 @@ pub fn insert(
|
||||
const new_route = fr.routePtr(new_route_index);
|
||||
if (new_route.filePtr(file_kind).unwrap()) |existing| {
|
||||
if (existing == file_id) {
|
||||
return; // exact match already exists. Hot-reloading code hits this
|
||||
return new_route_index; // exact match already exists. Hot-reloading code hits this
|
||||
}
|
||||
out_colliding_file_id.* = existing;
|
||||
return error.RouteCollision;
|
||||
@@ -810,6 +835,8 @@ pub fn insert(
|
||||
gop.value_ptr.* = new_route_index;
|
||||
},
|
||||
};
|
||||
|
||||
return new_route_index;
|
||||
}
|
||||
|
||||
/// An enforced upper bound of 64 unique patterns allows routing to use no heap allocation
|
||||
@@ -818,11 +845,67 @@ pub const MatchedParams = struct {
|
||||
|
||||
params: bun.BoundedArray(Entry, max_count),
|
||||
|
||||
/// Entries with the same key can exist when there is a catch all part (e.g.
|
||||
/// `[...slug].tsx`)
|
||||
pub const Entry = struct {
|
||||
key: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
pub const Iterator = struct {
|
||||
params: *const MatchedParams,
|
||||
offset: usize,
|
||||
|
||||
pub fn strPtrEql(a: []const u8, b: []const u8) bool {
|
||||
return a.ptr == b.ptr and a.len == b.len;
|
||||
}
|
||||
|
||||
pub fn next(it: *Iterator, values: *bun.BoundedArray([]const u8, max_count)) ?[]const u8 {
|
||||
values.len = 0;
|
||||
if (it.offset >= it.params.params.len) return null;
|
||||
const slice = it.params.params.slice();
|
||||
|
||||
var entry = &slice[it.offset];
|
||||
values.append(entry.value) catch unreachable;
|
||||
while (it.offset + 1 < it.params.params.len and strPtrEql(entry.key, slice[it.offset + 1].key)) {
|
||||
entry = &slice[it.offset + 1];
|
||||
values.append(entry.value) catch unreachable;
|
||||
it.offset += 1;
|
||||
}
|
||||
it.offset += 1;
|
||||
return entry.key;
|
||||
}
|
||||
};
|
||||
|
||||
pub const KeyIterator = struct {
|
||||
params: *const MatchedParams,
|
||||
offset: usize,
|
||||
|
||||
pub fn strPtrEql(a: []const u8, b: []const u8) bool {
|
||||
return a.ptr == b.ptr and a.len == b.len;
|
||||
}
|
||||
|
||||
pub fn next(it: *KeyIterator) ?[]const u8 {
|
||||
if (it.offset >= it.params.params.len) return null;
|
||||
const slice = it.params.params.slice();
|
||||
var entry = &slice[it.offset];
|
||||
while (it.offset + 1 < it.params.params.len and strPtrEql(entry.key, slice[it.offset + 1].key)) {
|
||||
entry = &slice[it.offset + 1];
|
||||
it.offset += 1;
|
||||
}
|
||||
it.offset += 1;
|
||||
return entry.key;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn iterator(self: *const MatchedParams) Iterator {
|
||||
return .{ .params = self, .offset = 0 };
|
||||
}
|
||||
|
||||
pub fn keyIterator(self: *const MatchedParams) KeyIterator {
|
||||
return .{ .params = self, .offset = 0 };
|
||||
}
|
||||
|
||||
/// Convert the matched params to a JavaScript object
|
||||
/// Returns null if there are no params
|
||||
pub fn toJS(self: *const MatchedParams, global: *jsc.JSGlobalObject) JSValue {
|
||||
@@ -844,6 +927,48 @@ pub const MatchedParams = struct {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn toJSWithStructure(self: *const MatchedParams, global: *jsc.JSGlobalObject, structure: JSValue) bun.JSError!jsc.JSValue {
|
||||
var obj = try jsc.JSObject.createWithStructure(global, structure);
|
||||
var values: bun.BoundedArray([]const u8, max_count) = .{};
|
||||
var it = self.iterator();
|
||||
var offset: u32 = 0;
|
||||
while (it.next(&values)) |_| {
|
||||
const to_put = to_put: {
|
||||
if (values.len == 1) {
|
||||
var bunstr = bun.String.init(values.get(0));
|
||||
defer bunstr.deref();
|
||||
break :to_put bunstr.transferToJS(global);
|
||||
}
|
||||
var array = try jsc.JSArray.createEmpty(global, values.len);
|
||||
for (values.slice(), 0..) |value, i| {
|
||||
var bunstr = bun.String.init(value);
|
||||
defer bunstr.deref();
|
||||
try array.putIndex(global, @intCast(i), bunstr.transferToJS(global));
|
||||
}
|
||||
break :to_put array;
|
||||
};
|
||||
try obj.putDirectOffset(global, offset, to_put);
|
||||
offset += 1;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn hash(self: *const MatchedParams) u32 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
for (self.params.slice()) |param| {
|
||||
hasher.update(param.key);
|
||||
switch (param.value) {
|
||||
.single => |val| hasher.update(val),
|
||||
.multiple => |vals| {
|
||||
for (vals.slice()) |val| {
|
||||
hasher.update(val);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return @truncate(hasher.final());
|
||||
}
|
||||
};
|
||||
|
||||
/// Fast enough for development to be seamless, but avoids building a
|
||||
@@ -1130,7 +1255,7 @@ fn scanInner(
|
||||
},
|
||||
};
|
||||
|
||||
result catch |err| switch (err) {
|
||||
_ = result catch |err| switch (err) {
|
||||
error.OutOfMemory => |e| return e,
|
||||
error.RouteCollision => {
|
||||
try ctx.vtable.onRouterCollisionError(
|
||||
@@ -1140,6 +1265,11 @@ fn scanInner(
|
||||
file_kind,
|
||||
);
|
||||
},
|
||||
error.InvalidRouteParam => {
|
||||
// Log error and skip this route
|
||||
bun.Output.prettyErrorln("error: Route parameter cannot be a numeric value in '{s}'", .{full_rel_path});
|
||||
bun.Output.flush();
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
@@ -1147,6 +1277,26 @@ fn scanInner(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this is shitty
|
||||
pub fn extractPathnameFromUrl(url: []const u8) []const u8 {
|
||||
// Extract pathname from URL (remove protocol, host, query, hash)
|
||||
var pathname = if (std.mem.indexOf(u8, url, "://")) |proto_end| blk: {
|
||||
const after_proto = url[proto_end + 3 ..];
|
||||
break :blk after_proto;
|
||||
} else url;
|
||||
|
||||
if (std.mem.indexOfScalar(u8, pathname, '/')) |path_start| {
|
||||
const path_with_query = pathname[path_start..];
|
||||
// Remove query string and hash
|
||||
const query_index = std.mem.indexOfScalar(u8, path_with_query, '?') orelse path_with_query.len;
|
||||
const hash_index = std.mem.indexOfScalar(u8, path_with_query, '#') orelse path_with_query.len;
|
||||
const end = @min(query_index, hash_index);
|
||||
pathname = path_with_query[0..end];
|
||||
}
|
||||
|
||||
return pathname;
|
||||
}
|
||||
|
||||
/// This binding is currently only intended for testing FrameworkRouter, and not
|
||||
/// production usage. It uses a slower but easier to use pattern for object
|
||||
/// creation. A production-grade JS api would be able to re-use objects.
|
||||
|
||||
3
src/bake/bake.d.ts
vendored
3
src/bake/bake.d.ts
vendored
@@ -369,7 +369,7 @@ declare module "bun" {
|
||||
* A common pattern would be to enforce the object is
|
||||
* `{ default: ReactComponent }`
|
||||
*/
|
||||
render: (request: Request, routeMetadata: RouteMetadata) => Awaitable<Response>;
|
||||
render: (request: Request, routeMetadata: RouteMetadata, als: AsyncLocalStorage<unknown>) => Awaitable<Response>;
|
||||
/**
|
||||
* Prerendering does not use a request, and is allowed to generate
|
||||
* multiple responses. This is used for static site generation, but not
|
||||
@@ -493,6 +493,7 @@ declare module "bun" {
|
||||
* A list of css files that the route will need to be styled.
|
||||
*/
|
||||
readonly styles: ReadonlyArray<string>;
|
||||
readonly request: Request | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -163,6 +163,7 @@ export async function render(
|
||||
export async function prerender(meta: Bake.RouteMetadata) {
|
||||
const page = getPage(meta, meta.styles);
|
||||
|
||||
console.log("SERVER MANIFEST", serverManifest);
|
||||
const rscPayload = renderToPipeableStream(page, serverManifest)
|
||||
// TODO: write a lightweight version of PassThrough
|
||||
.pipe(new PassThrough());
|
||||
|
||||
868
src/bake/prod/Manifest.zig
Normal file
868
src/bake/prod/Manifest.zig
Normal file
@@ -0,0 +1,868 @@
|
||||
const Manifest = @This();
|
||||
|
||||
pub const CURRENT_VERSION = bun.Semver.Version{
|
||||
.major = 0,
|
||||
.minor = 0,
|
||||
.patch = 1,
|
||||
};
|
||||
|
||||
version: bun.Semver.Version = CURRENT_VERSION,
|
||||
|
||||
/// All allocations except for the router are handled with this arena
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
/// Routes which are encoded in the manifest.
|
||||
///
|
||||
/// The indices correspond to the `route_index` in the manifest.
|
||||
///
|
||||
/// Also: `manifest.routes[i] ≅ server.framework_router.routes[i]`
|
||||
routes: []Route = &[_]Route{},
|
||||
/// Build output directory, defaults to "dist"
|
||||
build_output_dir: []const u8 = "dist",
|
||||
/// Router types with their server entrypoints
|
||||
router_types: []RouterType = &[_]RouterType{},
|
||||
/// Static assets
|
||||
assets: [][]const u8 = &[_][]const u8{},
|
||||
|
||||
/// All memory allocated with bun.default_allocator here
|
||||
router: bun.ptr.Owned(*FrameworkRouter),
|
||||
|
||||
pub const RouterType = struct {
|
||||
/// Path to the server entrypoint module for this router type
|
||||
server_entrypoint: []const u8,
|
||||
};
|
||||
|
||||
pub fn allocate(_self: Manifest) !*Manifest {
|
||||
var self = _self;
|
||||
const ptr = try self.arena.allocator().create(Manifest);
|
||||
ptr.* = self;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Manifest) void {
|
||||
self.router.get().deinit(bun.default_allocator);
|
||||
self.router.deinitShallow();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn fromFD(self: *Manifest, fd: bun.FileDescriptor, log: *logger.Log) !void {
|
||||
const source = fd.stdFile().readToEndAlloc(self.arena.allocator(), std.math.maxInt(usize)) catch |e| {
|
||||
try log.addErrorFmt(null, logger.Loc.Empty, log.msgs.allocator, "Failed to read manifest.json: {s}", .{@errorName(e)});
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
const json_source = logger.Source.initPathString("dist/manifest.json", source);
|
||||
try self.initFromJSON(&json_source, log);
|
||||
|
||||
bun.assertf(self.routes.len == self.router.get().routes.items.len, "Routes length mismatch, self.routes.len: {d}, self.router.get().routes.items.len: {d}", .{ self.routes.len, self.router.get().routes.items.len });
|
||||
}
|
||||
|
||||
fn initFromJSON(self: *Manifest, source: *const logger.Source, log: *logger.Log) !void {
|
||||
const router: *FrameworkRouter = self.router.get();
|
||||
const allocator = self.arena.allocator();
|
||||
|
||||
var temp_arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
defer temp_arena.deinit();
|
||||
const temp_allocator = temp_arena.allocator();
|
||||
|
||||
const json_source = logger.Source.initPathString(source.path.text, source.contents);
|
||||
const json_expr = bun.json.parseUTF8(&json_source, log, allocator) catch {
|
||||
try log.addError(&json_source, logger.Loc.Empty, "Failed to parse manifest.json");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
if (json_expr.data != .e_object) {
|
||||
try log.addError(&json_source, json_expr.loc, "manifest.json must be an object");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const json_obj = json_expr.data.e_object;
|
||||
|
||||
// Parse version
|
||||
const version_prop = json_obj.get("version") orelse {
|
||||
try log.addError(&json_source, json_expr.loc, "manifest.json must have a 'version' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
if (version_prop.data != .e_string) {
|
||||
try log.addError(&json_source, version_prop.loc, "manifest.json version must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const version_str = version_prop.data.e_string.string(temp_allocator) catch {
|
||||
try log.addError(&json_source, version_prop.loc, "failed to parse version string");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
// Parse semantic version
|
||||
const parse_result = bun.Semver.Version.parseUTF8(version_str);
|
||||
if (!parse_result.valid) {
|
||||
try log.addErrorFmt(&json_source, json_expr.loc, log.msgs.allocator, "Invalid semantic version: '{s}'. Expected format: 'major.minor.patch' (e.g., '1.0.0')", .{version_str});
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const manifest_version = parse_result.version.min();
|
||||
|
||||
// Check compatibility - major version must match
|
||||
const manifest_major = manifest_version.major;
|
||||
const manifest_minor = manifest_version.minor;
|
||||
const manifest_patch = manifest_version.patch;
|
||||
|
||||
if (manifest_major != CURRENT_VERSION.major) {
|
||||
const current_version_str = try std.fmt.allocPrint(temp_allocator, "{d}.{d}.{d}", .{ CURRENT_VERSION.major, CURRENT_VERSION.minor, CURRENT_VERSION.patch });
|
||||
const manifest_version_str = try std.fmt.allocPrint(temp_allocator, "{d}.{d}.{d}", .{ manifest_major, manifest_minor, manifest_patch });
|
||||
|
||||
if (manifest_major > CURRENT_VERSION.major) {
|
||||
try log.addErrorFmt(&json_source, json_expr.loc, log.msgs.allocator, "Manifest version {s} is not compatible with current version {s}. The manifest was created with a newer version of Bun. Please update Bun.", .{ manifest_version_str, current_version_str });
|
||||
} else {
|
||||
try log.addErrorFmt(&json_source, json_expr.loc, log.msgs.allocator, "Manifest version {s} is not compatible with current version {s}. The manifest needs to be regenerated with the current version of Bun.", .{ manifest_version_str, current_version_str });
|
||||
}
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
// Minor and patch versions can be different - we're backwards compatible within the same major version
|
||||
self.version = manifest_version;
|
||||
|
||||
// Parse build_output_dir (optional, defaults to "dist")
|
||||
if (json_obj.get("build_output_dir")) |build_output_dir_prop| {
|
||||
if (build_output_dir_prop.data == .e_string) {
|
||||
self.build_output_dir = try build_output_dir_prop.data.e_string.string(allocator);
|
||||
} else if (build_output_dir_prop.data != .e_null) {
|
||||
try log.addError(&json_source, build_output_dir_prop.loc, "manifest.json build_output_dir must be a string or null");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
}
|
||||
|
||||
if (json_obj.get("assets")) |assets_prop| {
|
||||
if (assets_prop.data == .e_array) {
|
||||
const items = assets_prop.data.e_array.items.slice();
|
||||
self.assets = try allocator.alloc([]const u8, items.len);
|
||||
|
||||
for (items, self.assets) |*in, *out| {
|
||||
if (in.data != .e_string) {
|
||||
// All style array elements must be strings
|
||||
try log.addError(&json_source, Loc.Empty, "\"assets\" must be an array of strings");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const style_str = try in.data.e_string.string(allocator);
|
||||
out.* = style_str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse router_types array (optional)
|
||||
if (json_obj.get("router_types")) |router_types_prop| {
|
||||
if (router_types_prop.data == .e_array) {
|
||||
const router_types_array = router_types_prop.data.e_array.items.slice();
|
||||
self.router_types = try allocator.alloc(RouterType, router_types_array.len);
|
||||
|
||||
for (router_types_array, 0..) |router_type_expr, i| {
|
||||
if (router_type_expr.data != .e_object) {
|
||||
try log.addError(&json_source, router_type_expr.loc, "manifest.json router_types array must contain objects");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const router_type_obj = router_type_expr.data.e_object;
|
||||
|
||||
// Parse server_entrypoint
|
||||
const server_entrypoint_prop = router_type_obj.get("server_entrypoint") orelse {
|
||||
try log.addError(&json_source, router_type_expr.loc, "router_type entry missing required 'server_entrypoint' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
if (server_entrypoint_prop.data != .e_string) {
|
||||
try log.addError(&json_source, server_entrypoint_prop.loc, "router_type 'server_entrypoint' must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
self.router_types[i] = .{
|
||||
.server_entrypoint = try server_entrypoint_prop.data.e_string.string(allocator),
|
||||
};
|
||||
}
|
||||
} else if (router_types_prop.data != .e_null) {
|
||||
try log.addError(&json_source, router_types_prop.loc, "manifest.json router_types must be an array or null");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse routes array
|
||||
const routes_prop = json_obj.get("routes") orelse {
|
||||
try log.addError(&json_source, json_expr.loc, "manifest.json must have an 'routes' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
if (routes_prop.data != .e_array) {
|
||||
try log.addError(&json_source, routes_prop.loc, "manifest.json routes must be an array");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const entries = routes_prop.data.e_array.items.slice();
|
||||
|
||||
// Group entries by route_index
|
||||
var route_map = std.AutoHashMap(u32, std.ArrayList(RawManifestEntry)).init(temp_allocator);
|
||||
defer {
|
||||
var it = route_map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
entry.value_ptr.deinit();
|
||||
}
|
||||
route_map.deinit();
|
||||
}
|
||||
|
||||
var max_route_index: u32 = 0;
|
||||
|
||||
// First pass: collect all entries and insert routes into router
|
||||
for (entries) |entry_expr| {
|
||||
if (entry_expr.data != .e_object) continue;
|
||||
|
||||
const entry_obj = entry_expr.data.e_object;
|
||||
|
||||
// Parse the new "route" field
|
||||
const route_prop = entry_obj.get("route") orelse {
|
||||
try log.addError(&json_source, entry_expr.loc, "manifest entry missing required 'route' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
if (route_prop.data != .e_string) {
|
||||
try log.addError(&json_source, route_prop.loc, "manifest entry 'route' field must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const route_str = try route_prop.data.e_string.string(temp_allocator);
|
||||
|
||||
// Parse the "route_type" field
|
||||
const route_type_prop = entry_obj.get("route_type") orelse {
|
||||
try log.addError(&json_source, entry_expr.loc, "manifest entry missing required 'route_type' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
if (route_type_prop.data != .e_number) {
|
||||
try log.addError(&json_source, route_type_prop.loc, "manifest entry 'route_type' field must be a number");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const route_type_index = route_type_prop.data.e_number.toU32();
|
||||
|
||||
// Get the type from the router
|
||||
if (route_type_index >= router.types.len) {
|
||||
try log.addErrorFmt(&json_source, route_type_prop.loc, log.msgs.allocator, "Invalid route_type index {d} (max: {d})", .{ route_type_index, router.types.len - 1 });
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const route_type = &router.types[route_type_index];
|
||||
|
||||
const rel_path = route_str;
|
||||
|
||||
// Use a temporary log for parsing
|
||||
var parse_log = FrameworkRouter.TinyLog.empty;
|
||||
const parsed = (route_type.style.parse(rel_path, null, &parse_log, route_type.allow_layouts, temp_allocator) catch {
|
||||
parse_log.cursor_at += @intCast(route_type.abs_root.len - router.root.len);
|
||||
try log.addErrorFmt(&json_source, route_prop.loc, log.msgs.allocator, "Failed to parse route pattern '{s}': {s}", .{ route_str, parse_log.msg.slice() });
|
||||
return error.InvalidManifest;
|
||||
}) orelse {
|
||||
// Route pattern not recognized by the style
|
||||
continue;
|
||||
};
|
||||
|
||||
// Create encoded pattern for insertion
|
||||
const encoded_pattern = try FrameworkRouter.EncodedPattern.initFromParts(parsed.parts, router.pattern_string_arena.allocator());
|
||||
|
||||
// Create a dummy insertion context for manifest loading
|
||||
// We don't need actual file operations since we're loading from manifest
|
||||
const DummyContext = struct {
|
||||
fn getFileIdForRouter(_: *anyopaque, _: []const u8, _: FrameworkRouter.Route.Index, _: FrameworkRouter.Route.FileKind) bun.OOM!FrameworkRouter.OpaqueFileId {
|
||||
// Return a dummy file ID
|
||||
return FrameworkRouter.OpaqueFileId.init(0);
|
||||
}
|
||||
fn onRouterSyntaxError(_: *anyopaque, _: []const u8, _: FrameworkRouter.TinyLog) bun.OOM!void {
|
||||
// Ignore syntax errors during manifest load
|
||||
}
|
||||
fn onRouterCollisionError(_: *anyopaque, _: []const u8, _: FrameworkRouter.OpaqueFileId, _: FrameworkRouter.Route.FileKind) bun.OOM!void {
|
||||
// Ignore collision errors during manifest load - they're expected for SSG
|
||||
}
|
||||
};
|
||||
|
||||
var dummy_ctx: u8 = 0; // Just a dummy value to get a pointer
|
||||
const vtable = struct {
|
||||
const getFileId = DummyContext.getFileIdForRouter;
|
||||
const onSyntaxError = DummyContext.onRouterSyntaxError;
|
||||
const onCollisionError = DummyContext.onRouterCollisionError;
|
||||
};
|
||||
const insertion_ctx = FrameworkRouter.InsertionContext{
|
||||
.opaque_ctx = @ptrCast(&dummy_ctx),
|
||||
.vtable = &.{
|
||||
.getFileIdForRouter = vtable.getFileId,
|
||||
.onRouterSyntaxError = vtable.onSyntaxError,
|
||||
.onRouterCollisionError = vtable.onCollisionError,
|
||||
},
|
||||
};
|
||||
|
||||
// Insert route into router to get route index
|
||||
var dummy_file_id: FrameworkRouter.OpaqueFileId = undefined;
|
||||
const route_index = blk: {
|
||||
const file_kind: FrameworkRouter.Route.FileKind = switch (parsed.kind) {
|
||||
.page => .page,
|
||||
.layout => .layout,
|
||||
else => .page,
|
||||
};
|
||||
|
||||
// Handle static vs dynamic routes separately since insertion_kind must be comptime
|
||||
// Check if this is truly a static route (no dynamic parts)
|
||||
const is_static = isStaticCheck: {
|
||||
for (parsed.parts) |part| {
|
||||
switch (part) {
|
||||
.text, .group => {},
|
||||
.param, .catch_all, .catch_all_optional => break :isStaticCheck false,
|
||||
}
|
||||
}
|
||||
break :isStaticCheck true;
|
||||
};
|
||||
|
||||
if (is_static) {
|
||||
// Static route - build the route path similarly to how scan() does it
|
||||
// Calculate total length needed for the static route path
|
||||
var static_total_len: usize = 0;
|
||||
for (parsed.parts) |part| {
|
||||
switch (part) {
|
||||
.text => |data| static_total_len += 1 + data.len, // "/" + text
|
||||
.group => {},
|
||||
.param, .catch_all, .catch_all_optional => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate and build the static route path
|
||||
const allocation = try router.pattern_string_arena.allocator().alloc(u8, static_total_len);
|
||||
var s = std.io.fixedBufferStream(allocation);
|
||||
for (parsed.parts) |part| {
|
||||
switch (part) {
|
||||
.text => |data| {
|
||||
_ = s.write("/") catch unreachable;
|
||||
_ = s.write(data) catch unreachable;
|
||||
},
|
||||
.group => {},
|
||||
.param, .catch_all, .catch_all_optional => unreachable,
|
||||
}
|
||||
}
|
||||
bun.assert(s.getWritten().len == allocation.len);
|
||||
|
||||
// Check if route already exists
|
||||
const lookup_path = if (allocation.len == 0) "/" else allocation;
|
||||
if (router.static_routes.get(lookup_path)) |existing_route_index| {
|
||||
// Route already exists, use existing index
|
||||
break :blk existing_route_index;
|
||||
}
|
||||
|
||||
// Insert the static route properly
|
||||
break :blk router.insert(
|
||||
bun.default_allocator,
|
||||
FrameworkRouter.Type.Index.init(@intCast(route_type_index)),
|
||||
.static,
|
||||
.{ .route_path = lookup_path },
|
||||
file_kind,
|
||||
route_str,
|
||||
insertion_ctx,
|
||||
&dummy_file_id,
|
||||
) catch |err| switch (err) {
|
||||
error.RouteCollision => {
|
||||
// For static routes that collide, try to find the existing route
|
||||
if (router.static_routes.get(lookup_path)) |existing_route_index| {
|
||||
break :blk existing_route_index;
|
||||
}
|
||||
// If we still can't find it, use root as fallback
|
||||
break :blk FrameworkRouter.Type.rootRouteIndex(FrameworkRouter.Type.Index.init(@intCast(route_type_index)));
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
} else {
|
||||
// Dynamic route
|
||||
break :blk router.insert(
|
||||
bun.default_allocator,
|
||||
FrameworkRouter.Type.Index.init(@intCast(route_type_index)),
|
||||
.dynamic,
|
||||
encoded_pattern,
|
||||
file_kind,
|
||||
route_str,
|
||||
insertion_ctx,
|
||||
&dummy_file_id,
|
||||
) catch |err| switch (err) {
|
||||
error.RouteCollision => {
|
||||
// For dynamic routes that collide, we need to find the existing route
|
||||
// This is expected for SSG routes with multiple param combinations
|
||||
// The collision means the route pattern already exists
|
||||
// We'll search for it in the dynamic routes
|
||||
for (router.dynamic_routes.keys()) |existing_pattern| {
|
||||
if (existing_pattern.effectiveURLHash() == encoded_pattern.effectiveURLHash()) {
|
||||
break :blk router.dynamic_routes.get(existing_pattern).?;
|
||||
}
|
||||
}
|
||||
// If we couldn't find it, something is wrong
|
||||
return err;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Parse mode
|
||||
const mode_prop = entry_obj.get("mode") orelse {
|
||||
try log.addError(&json_source, entry_expr.loc, "manifest entry missing required 'mode' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
if (mode_prop.data != .e_string) {
|
||||
try log.addError(&json_source, mode_prop.loc, "manifest entry 'mode' field must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const mode: RawManifestEntry.Mode = if (mode_prop.data.e_string.eqlComptime("ssr"))
|
||||
.ssr
|
||||
else if (mode_prop.data.e_string.eqlComptime("ssg"))
|
||||
.ssg
|
||||
else {
|
||||
try log.addError(&json_source, mode_prop.loc, "manifest entry 'mode' must be 'ssr' or 'ssg'");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
const manifest_entry = RawManifestEntry{
|
||||
.json_obj = entry_obj,
|
||||
.mode = mode,
|
||||
.loc = entry_expr.loc,
|
||||
};
|
||||
|
||||
const route_index_u32 = route_index.get();
|
||||
max_route_index = @max(max_route_index, route_index_u32);
|
||||
|
||||
const gop = try route_map.getOrPut(route_index_u32);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = std.ArrayList(RawManifestEntry).init(temp_allocator);
|
||||
}
|
||||
try gop.value_ptr.append(manifest_entry);
|
||||
}
|
||||
|
||||
// Return early if no routes
|
||||
if (route_map.count() == 0) {
|
||||
self.routes = &[_]Route{};
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate routes array
|
||||
var routes = try allocator.alloc(Route, max_route_index + 1);
|
||||
@memset(routes, Route{ .empty = {} });
|
||||
|
||||
// Second pass: build Route structs (continues from original code...)
|
||||
var it = route_map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const route_index = entry.key_ptr.*;
|
||||
const route_entries = entry.value_ptr.items;
|
||||
|
||||
if (route_entries.len == 0) continue;
|
||||
|
||||
const first_mode = route_entries[0].mode;
|
||||
|
||||
// Check if all entries have the same mode
|
||||
for (route_entries) |route_entry| {
|
||||
if (route_entry.mode != first_mode) {
|
||||
try log.addError(&json_source, route_entry.loc, "All entries for a route must have the same mode");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
}
|
||||
|
||||
switch (first_mode) {
|
||||
.ssr => {
|
||||
// SSR route - should only have one entry
|
||||
if (route_entries.len != 1) {
|
||||
try log.addErrorFmt(&json_source, Loc.Empty, log.msgs.allocator, "Found multiple entries for SSR route index {d}, this indicates a bug in the manifest.", .{route_index});
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const entry_obj = route_entries[0].json_obj;
|
||||
|
||||
// Parse client entrypoint
|
||||
const ep_prop = entry_obj.get("client_entrypoint") orelse entry_obj.get("entrypoint") orelse {
|
||||
try log.addError(&json_source, Loc.Empty, "SSR entry missing required 'client_entrypoint' or 'entrypoint' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
if (ep_prop.data != .e_string) {
|
||||
try log.addError(&json_source, Loc.Empty, "SSR entry 'client_entrypoint' must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const entrypoint = try ep_prop.data.e_string.string(allocator);
|
||||
|
||||
// Parse all modules (optional for new format)
|
||||
var modules = bun.BabyList([]const u8){};
|
||||
if (entry_obj.get("modules")) |modules_prop| {
|
||||
if (modules_prop.data != .e_array) {
|
||||
try log.addError(&json_source, Loc.Empty, "SSR entry 'modules' must be an array");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const modules_array = modules_prop.data.e_array.items.slice();
|
||||
if (modules_array.len > 0) {
|
||||
try modules.ensureUnusedCapacity(allocator, modules_array.len);
|
||||
for (modules_array, 0..) |module_expr, i| {
|
||||
if (module_expr.data != .e_string) {
|
||||
try log.addErrorFmt(&json_source, Loc.Empty, log.msgs.allocator, "SSR entry modules[{}] must be a string", .{i});
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const module_str = try module_expr.data.e_string.string(allocator);
|
||||
try modules.append(allocator, module_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const styles = try parseStyles(allocator, log, &json_source, entry_obj);
|
||||
|
||||
routes[route_index] = .{
|
||||
.ssr = .{
|
||||
.entrypoint = entrypoint,
|
||||
.modules = modules,
|
||||
.styles = styles,
|
||||
},
|
||||
};
|
||||
},
|
||||
.ssg => {
|
||||
if (route_entries.len == 1) {
|
||||
// Single SSG entry (no params or single param combo)
|
||||
const entry_obj = route_entries[0].json_obj;
|
||||
const ep_prop = entry_obj.get("entrypoint") orelse {
|
||||
try log.addError(&json_source, Loc.Empty, "SSG entry missing required 'entrypoint' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
if (ep_prop.data != .e_string) {
|
||||
try log.addError(&json_source, Loc.Empty, "SSG entry 'entrypoint' must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const entrypoint = try ep_prop.data.e_string.string(allocator);
|
||||
|
||||
const params = try parseParams(allocator, log, &json_source, entry_obj);
|
||||
const styles = try parseStyles(allocator, log, &json_source, entry_obj);
|
||||
|
||||
routes[route_index] = .{
|
||||
.ssg = Route.SSG{
|
||||
.entrypoint = entrypoint,
|
||||
.params = params,
|
||||
.styles = styles,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Multiple SSG entries for the same route (different params)
|
||||
var ssg_map = Route.SSGMany{};
|
||||
|
||||
for (route_entries) |route_entry| {
|
||||
const entry_obj = route_entry.json_obj;
|
||||
const ep_prop = entry_obj.get("entrypoint") orelse {
|
||||
try log.addError(&json_source, Loc.Empty, "SSG entry missing required 'entrypoint' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
if (ep_prop.data != .e_string) {
|
||||
try log.addError(&json_source, Loc.Empty, "SSG entry 'entrypoint' must be a string");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const entrypoint = try ep_prop.data.e_string.string(allocator);
|
||||
|
||||
const params = try parseParams(allocator, log, &json_source, entry_obj);
|
||||
const styles = try parseStyles(allocator, log, &json_source, entry_obj);
|
||||
|
||||
const ssg = Route.SSG{
|
||||
.entrypoint = entrypoint,
|
||||
.params = params,
|
||||
.styles = styles,
|
||||
};
|
||||
|
||||
try ssg_map.put(allocator, ssg, {});
|
||||
}
|
||||
|
||||
routes[route_index] = .{
|
||||
.ssg_many = ssg_map,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
self.routes = routes;
|
||||
}
|
||||
|
||||
fn parseParams(allocator: Allocator, log: *logger.Log, json_source: *const logger.Source, entry_obj: *const E.Object) !bun.BabyList(ParamEntry) {
|
||||
var params = bun.BabyList(ParamEntry){};
|
||||
|
||||
// Params is optional for SSG entries - it's only present for dynamic routes
|
||||
if (entry_obj.get("params")) |params_prop| {
|
||||
if (params_prop.data != .e_object) {
|
||||
// If params is present, it must be an object
|
||||
try log.addError(json_source, params_prop.loc, "SSG entry 'params' must be an object");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const params_obj = params_prop.data.e_object;
|
||||
|
||||
for (params_obj.properties.slice()) |prop| {
|
||||
if (prop.value) |value_expr| {
|
||||
const key = prop.key.?.asString(allocator) orelse "";
|
||||
|
||||
switch (value_expr.data) {
|
||||
.e_string => {
|
||||
// Single string value
|
||||
const param_entry = ParamEntry{
|
||||
.key = key,
|
||||
.value = .{ .single = try value_expr.data.e_string.string(allocator) },
|
||||
};
|
||||
try params.append(allocator, param_entry);
|
||||
},
|
||||
.e_array => |arr| {
|
||||
// Array of strings - for catch-all routes
|
||||
const array_items = arr.items.slice();
|
||||
if (array_items.len == 0) {
|
||||
try log.addError(json_source, value_expr.loc, "SSG entry 'params' array cannot be empty");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
// Validate all items are strings and collect them
|
||||
var values = bun.BabyList([]const u8){};
|
||||
for (array_items) |item| {
|
||||
if (item.data != .e_string) {
|
||||
try log.addError(json_source, item.loc, "SSG entry 'params' array must contain only strings");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
try values.append(allocator, try item.data.e_string.string(allocator));
|
||||
}
|
||||
|
||||
const param_entry = ParamEntry{
|
||||
.key = key,
|
||||
.value = .{ .multiple = values },
|
||||
};
|
||||
try params.append(allocator, param_entry);
|
||||
},
|
||||
else => {
|
||||
try log.addError(json_source, value_expr.loc, "SSG entry 'params' values must be strings or arrays of strings");
|
||||
return error.InvalidManifest;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
fn parseStyles(allocator: Allocator, log: *logger.Log, json_source: *const logger.Source, entry_obj: *const E.Object) !Styles {
|
||||
var styles = Styles{};
|
||||
|
||||
// Styles field is required and must be an array (can be empty)
|
||||
const styles_prop = entry_obj.get("styles") orelse {
|
||||
try log.addError(json_source, Loc.Empty, "SSG entry missing required 'styles' field");
|
||||
return error.InvalidManifest;
|
||||
};
|
||||
|
||||
if (styles_prop.data != .e_array) {
|
||||
try log.addError(json_source, styles_prop.loc, "SSG entry 'styles' must be an array");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
|
||||
const styles_array = styles_prop.data.e_array.items.slice();
|
||||
|
||||
// Preallocate capacity based on array length
|
||||
if (styles_array.len > 0) {
|
||||
try styles.ensureUnusedCapacity(allocator, styles_array.len);
|
||||
}
|
||||
|
||||
// Validate all elements are strings and add them
|
||||
for (styles_array) |style_expr| {
|
||||
if (style_expr.data != .e_string) {
|
||||
// All style array elements must be strings
|
||||
try log.addError(json_source, Loc.Empty, "SSG entry 'styles' must be an array");
|
||||
return error.InvalidManifest;
|
||||
}
|
||||
const style_str = try style_expr.data.e_string.string(allocator);
|
||||
try styles.append(allocator, style_str);
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
pub const Route = union(enum) {
|
||||
empty,
|
||||
ssr: SSR,
|
||||
ssg: SSG,
|
||||
ssg_many: SSGMany,
|
||||
|
||||
/// A route which has been server-side rendered
|
||||
pub const SSR = struct {
|
||||
entrypoint: []const u8,
|
||||
modules: bun.BabyList([]const u8),
|
||||
styles: Styles,
|
||||
};
|
||||
|
||||
/// A route which has been statically generated
|
||||
pub const SSG = struct {
|
||||
entrypoint: []const u8,
|
||||
params: bun.BabyList(ParamEntry),
|
||||
styles: Styles,
|
||||
store: ?*jsc.WebCore.Blob.Store = null,
|
||||
|
||||
pub fn fromMatchedParams(allocator: Allocator, params: *const bun.bake.FrameworkRouter.MatchedParams) !@This() {
|
||||
const matched_params = params.params.slice();
|
||||
if (matched_params.len == 0) {
|
||||
return .{
|
||||
.entrypoint = "",
|
||||
.params = bun.BabyList(ParamEntry){},
|
||||
.styles = .{},
|
||||
.store = null,
|
||||
};
|
||||
}
|
||||
|
||||
// Count unique keys to pre-allocate the exact capacity
|
||||
var unique_keys: usize = 0;
|
||||
{
|
||||
var i: usize = 0;
|
||||
while (i < matched_params.len) {
|
||||
const key = matched_params[i].key;
|
||||
unique_keys += 1;
|
||||
|
||||
// Skip over any consecutive entries with the same key
|
||||
i += 1;
|
||||
while (i < matched_params.len and bun.strings.eql(matched_params[i].key, key)) {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var params_list = try bun.BabyList(ParamEntry).initCapacity(allocator, unique_keys);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < matched_params.len) {
|
||||
const entry = matched_params[i];
|
||||
const key = entry.key;
|
||||
|
||||
// Check if the next entries have the same key (catch-all param case)
|
||||
var j = i + 1;
|
||||
while (j < matched_params.len and bun.strings.eql(matched_params[j].key, key)) {
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if (j - i == 1) {
|
||||
// Single value for this key
|
||||
try params_list.append(allocator, .{
|
||||
.key = key,
|
||||
.value = .{ .single = entry.value },
|
||||
});
|
||||
} else {
|
||||
// Multiple values for the same key (catch-all param)
|
||||
var multiple_values = try bun.BabyList([]const u8).initCapacity(allocator, j - i);
|
||||
for (matched_params[i..j]) |param_entry| {
|
||||
try multiple_values.append(allocator, param_entry.value);
|
||||
}
|
||||
try params_list.append(allocator, .{
|
||||
.key = key,
|
||||
.value = .{ .multiple = multiple_values },
|
||||
});
|
||||
}
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
return .{
|
||||
.entrypoint = "",
|
||||
.params = params_list,
|
||||
.styles = .{},
|
||||
.store = null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// A route which has been statically generated and has multiple pages
|
||||
/// associated with it.
|
||||
///
|
||||
/// Example:
|
||||
/// - Pattern => "/blog/[slug].tsx"
|
||||
/// - Pages => ["/blog/foo.tsx", "/blog/bar.tsx"]
|
||||
///
|
||||
/// The routes are stored in hashmap and they are hashed by the params as
|
||||
/// those are unique for a given route
|
||||
///
|
||||
/// We do this so we can quickly disambiguate based on the params
|
||||
pub const SSGMany = std.ArrayHashMapUnmanaged(
|
||||
SSG,
|
||||
void,
|
||||
struct {
|
||||
pub fn hash(_: @This(), key: SSG) u32 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
for (key.params.slice()) |param| {
|
||||
hasher.update(param.key);
|
||||
switch (param.value) {
|
||||
.single => |val| hasher.update(val),
|
||||
.multiple => |vals| {
|
||||
for (vals.slice()) |val| {
|
||||
hasher.update(val);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return @truncate(hasher.final());
|
||||
}
|
||||
|
||||
pub fn eql(
|
||||
_: @This(),
|
||||
a: SSG,
|
||||
b: SSG,
|
||||
_: usize,
|
||||
) bool {
|
||||
return a.params.eql(&b.params);
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const Index = bun.GenericIndex(u32, Route);
|
||||
};
|
||||
|
||||
const RawManifestEntry = struct {
|
||||
json_obj: *const E.Object,
|
||||
loc: logger.Loc,
|
||||
mode: Mode,
|
||||
|
||||
const Mode = enum { ssr, ssg };
|
||||
};
|
||||
|
||||
const ParamEntriesHash = u32;
|
||||
|
||||
pub const ParamEntry = struct {
|
||||
key: []const u8,
|
||||
value: Value,
|
||||
|
||||
pub const Value = union(enum) {
|
||||
single: []const u8,
|
||||
multiple: bun.BabyList([]const u8),
|
||||
|
||||
pub fn eql(a: *const Value, b: *const Value) bool {
|
||||
if (@as(std.meta.Tag(Value), a.*) != @as(std.meta.Tag(Value), b.*)) return false;
|
||||
return switch (a.*) {
|
||||
.single => |a_val| bun.strings.eql(a_val, b.single),
|
||||
.multiple => |a_list| blk: {
|
||||
const b_list = b.multiple;
|
||||
if (a_list.len != b_list.len) break :blk false;
|
||||
for (a_list.slice(), b_list.slice()) |a_item, b_item| {
|
||||
if (!bun.strings.eql(a_item, b_item)) break :blk false;
|
||||
}
|
||||
break :blk true;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn eql(a: *const ParamEntry, b: *const ParamEntry) bool {
|
||||
return bun.strings.eql(a.key, b.key) and a.value.eql(&b.value);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Styles = bun.BabyList([]const u8);
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const strings = bun.strings;
|
||||
const logger = bun.logger;
|
||||
const Loc = logger.Loc;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const CallFrame = jsc.CallFrame;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
const E = bun.ast.E;
|
||||
|
||||
const DirInfo = bun.resolver.DirInfo;
|
||||
const Resolver = bun.resolver.Resolver;
|
||||
|
||||
const mem = std.mem;
|
||||
const Allocator = mem.Allocator;
|
||||
|
||||
const FrameworkRouter = bun.bake.FrameworkRouter;
|
||||
89
src/bake/prod/ProductionFrameworkRouter.zig
Normal file
89
src/bake/prod/ProductionFrameworkRouter.zig
Normal file
@@ -0,0 +1,89 @@
|
||||
/// Context type for FrameworkRouter in production mode
|
||||
/// Implements the required methods for route scanning
|
||||
const ProductionFrameworkRouter = @This();
|
||||
|
||||
file_id_counter: u32 = 0,
|
||||
|
||||
pub fn init() ProductionFrameworkRouter {
|
||||
return .{};
|
||||
}
|
||||
|
||||
/// Generate a file ID for a route file
|
||||
/// In production, we don't need to track actual files since they're bundled
|
||||
pub fn getFileIdForRouter(
|
||||
this: *ProductionFrameworkRouter,
|
||||
abs_path: []const u8,
|
||||
associated_route: bun.bake.FrameworkRouter.Route.Index,
|
||||
file_kind: bun.bake.FrameworkRouter.Route.FileKind,
|
||||
) !bun.bake.FrameworkRouter.OpaqueFileId {
|
||||
_ = abs_path;
|
||||
_ = associated_route;
|
||||
_ = file_kind;
|
||||
// In production, we just need unique IDs for the route structure
|
||||
// The actual files are already bundled
|
||||
const id = this.file_id_counter;
|
||||
this.file_id_counter += 1;
|
||||
return bun.bake.FrameworkRouter.OpaqueFileId.init(id);
|
||||
}
|
||||
|
||||
/// Handle route syntax errors
|
||||
pub fn onRouterSyntaxError(
|
||||
this: *ProductionFrameworkRouter,
|
||||
rel_path: []const u8,
|
||||
log: bun.bake.FrameworkRouter.TinyLog,
|
||||
) !void {
|
||||
_ = this;
|
||||
// In production, log syntax errors to console
|
||||
// These shouldn't happen in production as routes are pre-validated during build
|
||||
bun.Output.prettyErrorln("<r><red>error<r>: route syntax error in {s}", .{rel_path});
|
||||
log.print(rel_path);
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
/// Handle route collision errors
|
||||
pub fn onRouterCollisionError(
|
||||
this: *ProductionFrameworkRouter,
|
||||
rel_path: []const u8,
|
||||
other_id: bun.bake.FrameworkRouter.OpaqueFileId,
|
||||
file_kind: bun.bake.FrameworkRouter.Route.FileKind,
|
||||
) !void {
|
||||
_ = this;
|
||||
_ = other_id;
|
||||
// In production, log collision errors
|
||||
// These shouldn't happen in production as routes are pre-validated during build
|
||||
Output.errGeneric("Multiple {s} matching the same route pattern is ambiguous", .{
|
||||
switch (file_kind) {
|
||||
.page => "pages",
|
||||
.layout => "layout",
|
||||
},
|
||||
});
|
||||
Output.prettyErrorln(" - <blue>{s}<r>", .{rel_path});
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const std = @import("std");
|
||||
const bake = bun.bake;
|
||||
const strings = bun.strings;
|
||||
const logger = bun.logger;
|
||||
const Loc = logger.Loc;
|
||||
|
||||
const Route = bun.bake.FrameworkRouter.Route;
|
||||
const SSRRouteList = bun.bake.SSRRouteList;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSError = bun.JSError;
|
||||
const CallFrame = jsc.CallFrame;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
const E = bun.ast.E;
|
||||
|
||||
const DirInfo = bun.resolver.DirInfo;
|
||||
const Resolver = bun.resolver.Resolver;
|
||||
|
||||
const mem = std.mem;
|
||||
const Allocator = mem.Allocator;
|
||||
const Output = bun.Output;
|
||||
const Manifest = bun.bake.Manifest;
|
||||
|
||||
const FrameworkRouter = bun.bake.FrameworkRouter;
|
||||
446
src/bake/prod/ProductionServerMethods.zig
Normal file
446
src/bake/prod/ProductionServerMethods.zig
Normal file
@@ -0,0 +1,446 @@
|
||||
const log = Output.scoped(.bake_prod, .visible);
|
||||
const httplog = log;
|
||||
|
||||
pub fn ProductionServerMethods(protocol_enum: bun.api.server.Protocol, development_kind: bun.api.server.DevelopmentKind) type {
|
||||
return struct {
|
||||
const Server = bun.api.server.NewServer(protocol_enum, development_kind);
|
||||
const ThisServer = Server;
|
||||
const App = Server.App;
|
||||
const ssl_enabled = Server.ssl_enabled;
|
||||
|
||||
pub fn bakeProductionSSRRouteHandler(server: *ThisServer, req: *uws.Request, resp: *App.Response) void {
|
||||
bakeProductionSSRRouteHandlerWithURL(server, req, resp, req.url());
|
||||
}
|
||||
|
||||
pub fn bakeProductionSSRRouteHandlerWithURL(server: *ThisServer, req: *uws.Request, resp: *App.Response, url: []const u8) void {
|
||||
// We can assume manifest and router exist since this handler is only registered when they do
|
||||
const manifest = server.bake_prod.get().?.manifest;
|
||||
const router = server.bake_prod.get().?.getRouter();
|
||||
|
||||
// Try to match the request URL against the router
|
||||
var params: bun.bake.FrameworkRouter.MatchedParams = undefined;
|
||||
if (router.matchSlow(url, ¶ms)) |route_index| {
|
||||
// Found a route - check if it's an SSR route
|
||||
if (route_index.get() < manifest.routes.len) {
|
||||
const route = &manifest.routes[route_index.get()];
|
||||
switch (route.*) {
|
||||
.ssr => |*ssr| {
|
||||
_ = ssr;
|
||||
// Call the SSR request handler
|
||||
onBakeFrameworkSSRRequest(server, req, resp, route_index, ¶ms);
|
||||
return;
|
||||
},
|
||||
.ssg, .ssg_many => {
|
||||
// This is an SSG route, which should have been handled by static routes
|
||||
// Fall through to the original handler
|
||||
},
|
||||
.empty => {
|
||||
// Empty route, fall through
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No SSR route matched, call the original handler based on config
|
||||
switch (server.config.onNodeHTTPRequest) {
|
||||
.zero => switch (server.config.onRequest) {
|
||||
.zero => server.on404(req, resp),
|
||||
else => server.onRequest(req, resp),
|
||||
},
|
||||
else => server.onNodeHTTPRequest(req, resp),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn onBakeFrameworkSSRRequest(
|
||||
server: *ThisServer,
|
||||
req: *uws.Request,
|
||||
resp: *App.Response,
|
||||
route_index: bun.bake.FrameworkRouter.Route.Index,
|
||||
params: *const bun.bake.FrameworkRouter.MatchedParams,
|
||||
) void {
|
||||
onBakeFrameworkSSRRequestImpl(server, req, resp, route_index, params) catch |err| switch (err) {
|
||||
error.JSError => server.vm.global.reportActiveExceptionAsUnhandled(err),
|
||||
error.OutOfMemory => bun.outOfMemory(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onBakeFrameworkSSRRequestImpl(
|
||||
server: *ThisServer,
|
||||
req: *uws.Request,
|
||||
resp: *App.Response,
|
||||
route_index: bun.bake.FrameworkRouter.Route.Index,
|
||||
params: *const bun.bake.FrameworkRouter.MatchedParams,
|
||||
) bun.JSError!void {
|
||||
if (comptime Environment.enable_logs)
|
||||
httplog("[Bake SSR] {s} - {s}", .{ req.method(), req.url() });
|
||||
|
||||
const bake_prod = server.bake_prod.get().?;
|
||||
const server_request_callback = bake_prod.bake_server_runtime_handler.get();
|
||||
const global = server.globalThis;
|
||||
const args = try bake_prod.newRouteParams(global, route_index, params);
|
||||
|
||||
// Call the server runtime's handleRequest function using onSavedRequest
|
||||
server.onSavedRequest(
|
||||
.{ .stack = req },
|
||||
resp,
|
||||
server_request_callback,
|
||||
6,
|
||||
.{
|
||||
args.route_index,
|
||||
args.router_type_index,
|
||||
args.route_info,
|
||||
args.params,
|
||||
args.newRouteParams,
|
||||
args.setAsyncLocalStorage,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn handleSingleSSGRoute(
|
||||
server: *ThisServer,
|
||||
app: anytype,
|
||||
ssg: *bun.bake.Manifest.Route.SSG,
|
||||
route_index: usize,
|
||||
client_entrypoints_seen: *std.hash_map.HashMap([]const u8, void, bun.StringHashMapContext, 80),
|
||||
) void {
|
||||
const global = server.globalThis;
|
||||
// const bake_prod = this.bake_prod.get().?;
|
||||
const any_server = AnyServer.from(server);
|
||||
|
||||
// For SSG routes with params, we need to build the actual URL path
|
||||
// Use the route index to look up the pattern from the framework router
|
||||
const url_path =
|
||||
server.bake_prod.get().?.reconstructPathFromParams(bun.default_allocator, @intCast(route_index), &ssg.params) catch "/";
|
||||
|
||||
const url_path_without_leading_slash = std.mem.trimLeft(u8, url_path, "/");
|
||||
|
||||
log("Setting URL path: {s}\n", .{url_path});
|
||||
|
||||
// Build the filesystem path to the pre-rendered files
|
||||
// SSG files are stored in dist/{route}/index.html and dist/{route}/index.rsc
|
||||
const pathbuf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(pathbuf);
|
||||
|
||||
// Create file routes for the HTML and RSC files
|
||||
// Serve index.html for the main route
|
||||
const html_path = bun.handleOom(bun.default_allocator.dupe(u8, bun.path.joinStringBuf(
|
||||
pathbuf,
|
||||
&[_][]const u8{ "dist", url_path_without_leading_slash, "index.html" },
|
||||
.auto,
|
||||
)));
|
||||
|
||||
// Create a file blob for the HTML file
|
||||
const html_store = jsc.WebCore.Blob.Store.initFile(
|
||||
.{ .path = .{ .string = bun.PathString.init(html_path) } },
|
||||
bun.http.MimeType.html,
|
||||
bun.default_allocator,
|
||||
) catch bun.outOfMemory();
|
||||
|
||||
html_store.ref();
|
||||
ssg.store = html_store;
|
||||
|
||||
const html_blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = html_store,
|
||||
.content_type = bun.http.MimeType.html.value,
|
||||
.globalThis = global,
|
||||
};
|
||||
|
||||
const html_route = FileRoute.initFromBlob(html_blob, .{
|
||||
.server = any_server,
|
||||
.status_code = 200,
|
||||
});
|
||||
|
||||
// Apply the HTML route
|
||||
ServerConfig.applyStaticRoute(any_server, Server.ssl_enabled, app, *FileRoute, html_route, url_path, .{ .method = bun.http.Method.Set.init(.{ .GET = true }) });
|
||||
|
||||
// Also serve the .rsc file at the same path with .rsc extension
|
||||
const rsc_url_path = bun.strings.concat(bun.default_allocator, &.{ url_path_without_leading_slash, ".rsc" }) catch |e| bun.handleOom(e);
|
||||
|
||||
const rsc_path = bun.handleOom(
|
||||
bun.default_allocator.dupe(
|
||||
u8,
|
||||
bun.path.joinStringBuf(
|
||||
pathbuf,
|
||||
&[_][]const u8{ "dist", url_path_without_leading_slash, "index.rsc" },
|
||||
.auto,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Create a file blob for the RSC file
|
||||
const rsc_store = jsc.WebCore.Blob.Store.initFile(
|
||||
.{ .path = .{ .string = bun.PathString.init(rsc_path) } },
|
||||
bun.http.MimeType.javascript,
|
||||
bun.default_allocator,
|
||||
) catch bun.outOfMemory();
|
||||
|
||||
const rsc_blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = rsc_store,
|
||||
.content_type = bun.http.MimeType.javascript.value,
|
||||
.globalThis = global,
|
||||
};
|
||||
|
||||
const rsc_route = FileRoute.initFromBlob(rsc_blob, .{
|
||||
.server = any_server,
|
||||
.status_code = 200,
|
||||
});
|
||||
|
||||
// Apply the RSC route
|
||||
ServerConfig.applyStaticRoute(any_server, Server.ssl_enabled, app, *FileRoute, rsc_route, rsc_url_path, .{ .method = bun.http.Method.Set.init(.{ .GET = true }) });
|
||||
|
||||
// Register the client entrypoint if we haven't already
|
||||
if (ssg.entrypoint.len > 0) {
|
||||
const result = client_entrypoints_seen.getOrPut(ssg.entrypoint) catch bun.outOfMemory();
|
||||
if (!result.found_existing) {
|
||||
// Serve the client JS file (e.g., /_bun/2eeb5qyr.js)
|
||||
// The file is in dist/_bun/xxx.js
|
||||
const client_path = bun.handleOom(
|
||||
bun.default_allocator.dupe(u8, bun.path.joinStringBuf(
|
||||
pathbuf,
|
||||
&[_][]const u8{ "dist", std.mem.trimLeft(u8, ssg.entrypoint, "/") },
|
||||
.auto,
|
||||
)),
|
||||
);
|
||||
|
||||
// Create a file blob for the client JS file
|
||||
const client_store = jsc.WebCore.Blob.Store.initFile(
|
||||
.{ .path = .{ .string = bun.PathString.init(client_path) } },
|
||||
bun.http.MimeType.javascript,
|
||||
bun.default_allocator,
|
||||
) catch bun.outOfMemory();
|
||||
|
||||
const client_blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = client_store,
|
||||
.content_type = bun.http.MimeType.javascript.value,
|
||||
.globalThis = global,
|
||||
};
|
||||
|
||||
const client_route = FileRoute.initFromBlob(client_blob, .{
|
||||
.server = any_server,
|
||||
.status_code = 200,
|
||||
});
|
||||
|
||||
const client_url = bun.default_allocator.dupe(u8, ssg.entrypoint) catch bun.outOfMemory();
|
||||
ServerConfig.applyStaticRoute(any_server, Server.ssl_enabled, app, *FileRoute, client_route, client_url, .{ .method = bun.http.Method.Set.init(.{ .GET = true }) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setBakeManifestRoutes(server: *Server, app: *Server.App, manifest: *bun.bake.Manifest) void {
|
||||
// Add route handler for /_bun/* static chunk files
|
||||
setStaticRoutes(server, app, manifest);
|
||||
|
||||
// First, we need to serve the client entrypoint files
|
||||
// These are shared across all SSG routes of the same type
|
||||
var client_entrypoints_seen = std.hash_map.HashMap([]const u8, void, bun.StringHashMapContext, 80).init(bun.default_allocator);
|
||||
defer client_entrypoints_seen.deinit();
|
||||
|
||||
for (manifest.routes, 0..) |*route, route_index| {
|
||||
switch (route.*) {
|
||||
.empty => {},
|
||||
.ssr => {
|
||||
// SSR routes are handled dynamically via bakeProductionSSRRouteHandler
|
||||
// We don't need to set up static routes for SSR
|
||||
},
|
||||
.ssg => |*ssg| {
|
||||
handleSingleSSGRoute(
|
||||
server,
|
||||
app,
|
||||
ssg,
|
||||
route_index,
|
||||
&client_entrypoints_seen,
|
||||
);
|
||||
},
|
||||
.ssg_many => |*ssg_many| {
|
||||
// Handle multiple SSG entries for the same route
|
||||
var iter = ssg_many.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
const ssg = &entry.key_ptr.*;
|
||||
handleSingleSSGRoute(
|
||||
server,
|
||||
app,
|
||||
ssg,
|
||||
route_index,
|
||||
&client_entrypoints_seen,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setStaticRoutes(server: *Server, app: *Server.App, manifest: *bun.bake.Manifest) void {
|
||||
const assets = manifest.assets;
|
||||
|
||||
const pathbuf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(pathbuf);
|
||||
|
||||
for (assets) |asset_path| {
|
||||
bun.assert(bun.strings.hasPrefixComptime(asset_path, "/_bun/"));
|
||||
const file = bun.strings.trimPrefixComptime(u8, asset_path, "/_bun/");
|
||||
|
||||
const file_path_copy = bun.default_allocator.dupe(u8, bun.path.joinStringBuf(
|
||||
pathbuf,
|
||||
&[_][]const u8{ manifest.build_output_dir, file },
|
||||
.auto,
|
||||
)) catch |e| bun.handleOom(e);
|
||||
|
||||
// Determine MIME type based on file extension
|
||||
const mime_type = if (std.mem.endsWith(u8, asset_path, ".js"))
|
||||
bun.http.MimeType.javascript
|
||||
else if (std.mem.endsWith(u8, asset_path, ".css"))
|
||||
bun.http.MimeType.css
|
||||
else if (std.mem.endsWith(u8, asset_path, ".map"))
|
||||
bun.http.MimeType.json
|
||||
else
|
||||
bun.http.MimeType.other;
|
||||
|
||||
// Create a file blob for the static chunk
|
||||
const store = jsc.WebCore.Blob.Store.initFile(
|
||||
.{ .path = .{ .string = bun.PathString.init(file_path_copy) } },
|
||||
mime_type,
|
||||
bun.default_allocator,
|
||||
) catch |e| bun.handleOom(e);
|
||||
|
||||
const blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = store,
|
||||
.content_type = mime_type.value,
|
||||
.globalThis = server.globalThis,
|
||||
};
|
||||
|
||||
// Create a file route and serve it
|
||||
const any_server = AnyServer.from(server);
|
||||
const file_route = FileRoute.initFromBlob(blob, .{
|
||||
.server = any_server,
|
||||
.status_code = 200,
|
||||
});
|
||||
ServerConfig.applyStaticRoute(
|
||||
any_server,
|
||||
Server.ssl_enabled,
|
||||
app,
|
||||
*FileRoute,
|
||||
file_route,
|
||||
asset_path,
|
||||
.{ .method = bun.http.Method.Set.init(.{ .GET = true }) },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bakeStaticChunkRequestHandler(server: *ThisServer, req: *uws.Request, resp: *App.Response) void {
|
||||
const manifest = server.bake_prod.get().?.manifest;
|
||||
|
||||
// Get the asset path from the URL (everything after /_bun/)
|
||||
const url = req.url();
|
||||
const prefix = "/_bun/";
|
||||
if (!std.mem.startsWith(u8, url, prefix)) {
|
||||
resp.writeStatus("404 Not Found");
|
||||
resp.end("", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const asset_path = url[prefix.len..];
|
||||
if (asset_path.len == 0) {
|
||||
resp.writeStatus("404 Not Found");
|
||||
resp.end("", false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the full file path: manifest.build_output_dir + "/_bun/" + asset_path
|
||||
var file_path_buf: [4096]u8 = undefined;
|
||||
const file_path = std.fmt.bufPrint(&file_path_buf, "{s}/_bun/{s}", .{ manifest.build_output_dir, asset_path }) catch {
|
||||
resp.writeStatus("500 Internal Server Error");
|
||||
resp.end("", false);
|
||||
return;
|
||||
};
|
||||
|
||||
// Make a copy of the path for the blob to own
|
||||
const file_path_copy = bun.default_allocator.dupe(u8, file_path) catch {
|
||||
resp.writeStatus("500 Internal Server Error");
|
||||
resp.end("", false);
|
||||
return;
|
||||
};
|
||||
|
||||
// Determine MIME type based on file extension
|
||||
const mime_type = if (std.mem.endsWith(u8, asset_path, ".js"))
|
||||
bun.http.MimeType.javascript
|
||||
else if (std.mem.endsWith(u8, asset_path, ".css"))
|
||||
bun.http.MimeType.css
|
||||
else if (std.mem.endsWith(u8, asset_path, ".map"))
|
||||
bun.http.MimeType.json
|
||||
else
|
||||
bun.http.MimeType.other;
|
||||
|
||||
// Create a file blob for the static chunk
|
||||
const store = jsc.WebCore.Blob.Store.initFile(
|
||||
.{ .path = .{ .string = bun.PathString.init(file_path_copy) } },
|
||||
mime_type,
|
||||
bun.default_allocator,
|
||||
) catch {
|
||||
resp.writeStatus("404 Not Found");
|
||||
resp.end("", false);
|
||||
return;
|
||||
};
|
||||
|
||||
const blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = store,
|
||||
.content_type = mime_type.value,
|
||||
.globalThis = server.globalThis,
|
||||
};
|
||||
|
||||
// Create a file route and serve it
|
||||
const any_server = AnyServer.from(server);
|
||||
const file_route = FileRoute.initFromBlob(blob, .{
|
||||
.server = any_server,
|
||||
.status_code = 200,
|
||||
});
|
||||
|
||||
// Serve the file using the file route handler
|
||||
const any_resp = if (ssl_enabled)
|
||||
uws.AnyResponse{ .SSL = resp }
|
||||
else
|
||||
uws.AnyResponse{ .TCP = resp };
|
||||
file_route.onRequest(req, any_resp);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const bake = bun.bake;
|
||||
const strings = bun.strings;
|
||||
const logger = bun.logger;
|
||||
const Loc = logger.Loc;
|
||||
|
||||
const Route = bun.bake.FrameworkRouter.Route;
|
||||
const SSRRouteList = bun.bake.SSRRouteList;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSError = bun.JSError;
|
||||
const CallFrame = jsc.CallFrame;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
const E = bun.ast.E;
|
||||
|
||||
const DirInfo = bun.resolver.DirInfo;
|
||||
const Resolver = bun.resolver.Resolver;
|
||||
|
||||
const mem = std.mem;
|
||||
const Allocator = mem.Allocator;
|
||||
const Manifest = bun.bake.Manifest;
|
||||
|
||||
const ServerConfig = bun.api.server.ServerConfig;
|
||||
const AnyServer = bun.api.server.AnyServer;
|
||||
|
||||
const Output = bun.Output;
|
||||
const FileRoute = bun.api.server.FileRoute;
|
||||
const StaticRoute = bun.api.server.StaticRoute;
|
||||
|
||||
const Environment = bun.Environment;
|
||||
|
||||
const FrameworkRouter = bun.bake.FrameworkRouter;
|
||||
const std = @import("std");
|
||||
const uws = bun.uws;
|
||||
447
src/bake/prod/ProductionServerState.zig
Normal file
447
src/bake/prod/ProductionServerState.zig
Normal file
@@ -0,0 +1,447 @@
|
||||
const log = Output.scoped(.bake_prod, .visible);
|
||||
const httplog = log;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
route_list: jsc.Strong,
|
||||
bake_server_runtime_handler: jsc.Strong,
|
||||
/// Pointer is owned by the arena inside Manifest
|
||||
manifest: *bun.bake.Manifest,
|
||||
|
||||
pub const ProductionFrameworkRouter = @import("./ProductionFrameworkRouter.zig");
|
||||
|
||||
pub fn getRouter(this: *Self) *bun.bake.FrameworkRouter {
|
||||
return this.manifest.router.get();
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Self) void {
|
||||
this.route_list.deinit();
|
||||
this.bake_server_runtime_handler.deinit();
|
||||
this.manifest.deinit();
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: *bun.transpiler.Transpiler,
|
||||
config: *bun.api.ServerConfig,
|
||||
bake_opts: *const bun.bake.UserOptions,
|
||||
) JSError!bun.ptr.Owned(*Self) {
|
||||
// const allocator = bun.default_allocator;
|
||||
|
||||
const manifest: *Manifest = bun.take(&config.bake_manifest) orelse {
|
||||
return globalObject.throw("Manifest not configured", .{});
|
||||
};
|
||||
|
||||
const route_list = try SSRRouteList.create(globalObject, manifest.routes.len);
|
||||
|
||||
const build_output_dir = manifest.build_output_dir;
|
||||
|
||||
// Create absolute path for build output dir
|
||||
const server_runtime_path = bun.path.joinAbsString(
|
||||
bake_opts.root,
|
||||
&.{ build_output_dir, "_bun", "server-runtime.js" },
|
||||
.auto,
|
||||
);
|
||||
|
||||
const bake_server_runtime_handler = try initBakeServerRuntime(globalObject, server_runtime_path);
|
||||
|
||||
const self: Self = .{
|
||||
.route_list = jsc.Strong.create(route_list, globalObject),
|
||||
.bake_server_runtime_handler = bake_server_runtime_handler,
|
||||
.manifest = manifest,
|
||||
};
|
||||
|
||||
return bun.ptr.Owned(*Self).new(self);
|
||||
}
|
||||
|
||||
pub fn initBakeServerRuntime(global: *JSGlobalObject, server_runtime_path: []const u8) !jsc.Strong {
|
||||
// Get the production server runtime code
|
||||
const runtime_code = bun.String.static(bun.bake.getProductionRuntime(.server).code);
|
||||
|
||||
// Convert path to bun.String for passing to C++
|
||||
const path_str = bun.String.cloneUTF8(server_runtime_path);
|
||||
defer path_str.deref();
|
||||
|
||||
// Load and execute the production server runtime IIFE
|
||||
const exports_object = BakeLoadProductionServerCode(global, runtime_code, path_str) catch {
|
||||
return global.throw("Server runtime failed to start", .{});
|
||||
};
|
||||
|
||||
if (!exports_object.isObject()) {
|
||||
return global.throw("Server runtime failed to load - expected an object", .{});
|
||||
}
|
||||
|
||||
// Extract and store the handleRequest function from the exports object
|
||||
const handle_request_fn = exports_object.get(global, "handleRequest") catch null orelse {
|
||||
return global.throw("Server runtime module is missing 'handleRequest' export", .{});
|
||||
};
|
||||
|
||||
if (!handle_request_fn.isCallable()) {
|
||||
return global.throw("Server runtime module's 'handleRequest' export is not a function", .{});
|
||||
}
|
||||
|
||||
handle_request_fn.ensureStillAlive();
|
||||
|
||||
return jsc.Strong.create(handle_request_fn, global);
|
||||
}
|
||||
|
||||
pub fn getRouteInfo(this: *Self, global: *JSGlobalObject, index: Route.Index) JSError!JSValue {
|
||||
return SSRRouteList.getRouteInfo(global, this.route_list.get(), index.get());
|
||||
}
|
||||
|
||||
fn BakeLoadProductionServerCode(global: *jsc.JSGlobalObject, code: bun.String, path: bun.String) bun.JSError!jsc.JSValue {
|
||||
const f = @extern(*const fn (*jsc.JSGlobalObject, bun.String, bun.String) callconv(.c) jsc.JSValue, .{ .name = "BakeLoadProductionServerCode" }).*;
|
||||
return bun.jsc.fromJSHostCall(global, @src(), f, .{ global, code, path });
|
||||
}
|
||||
|
||||
pub fn routeDataForInitialization(
|
||||
globalObject: *JSGlobalObject,
|
||||
request: *bun.webcore.Request,
|
||||
router_index: usize,
|
||||
router_type_index: usize,
|
||||
out_router_type_main: *JSValue,
|
||||
out_route_modules: *JSValue,
|
||||
out_client_entry_url: *JSValue,
|
||||
out_styles: *JSValue,
|
||||
) JSError!void {
|
||||
const server = request.request_context.getBakeProdState() orelse {
|
||||
return globalObject.throw("Request context is not a production server state", .{});
|
||||
};
|
||||
|
||||
const rtr = server.getRouter();
|
||||
|
||||
if (router_index >= rtr.routes.items.len) {
|
||||
return globalObject.throw("Router index out of bounds", .{});
|
||||
}
|
||||
if (router_type_index >= rtr.types.len) {
|
||||
return globalObject.throw("Router type index out of bounds", .{});
|
||||
}
|
||||
|
||||
const route = switch (server.manifest.routes[router_index]) {
|
||||
.ssr => |*ssr| ssr,
|
||||
else => {
|
||||
return globalObject.throw("Route is not an SSR route", .{});
|
||||
},
|
||||
};
|
||||
|
||||
const router_type_main = bun.String.init(server.manifest.router_types[router_type_index].server_entrypoint);
|
||||
out_router_type_main.* = router_type_main.toJS(globalObject);
|
||||
|
||||
const route_modules = try jsc.JSValue.createEmptyArray(globalObject, route.modules.len);
|
||||
for (route.modules.slice(), 0..) |module_path, i| {
|
||||
const module_str = bun.String.init(module_path);
|
||||
try route_modules.putIndex(globalObject, @intCast(i), module_str.toJS(globalObject));
|
||||
}
|
||||
out_route_modules.* = route_modules;
|
||||
|
||||
const client_entry_url = bun.String.init(route.entrypoint).toJS(globalObject);
|
||||
out_client_entry_url.* = client_entry_url;
|
||||
|
||||
const styles = try jsc.JSValue.createEmptyArray(globalObject, route.styles.len);
|
||||
for (route.styles.slice(), 0..) |style_path, i| {
|
||||
const style_str = bun.String.init(style_path);
|
||||
try styles.putIndex(globalObject, @intCast(i), style_str.toJS(globalObject));
|
||||
}
|
||||
out_styles.* = styles;
|
||||
}
|
||||
|
||||
export fn Bun__BakeProductionSSRRouteInfo__dataForInitialization(
|
||||
globalObject: *JSGlobalObject,
|
||||
zigRequestPtr: *anyopaque,
|
||||
routerIndex: usize,
|
||||
routerTypeIndex: usize,
|
||||
routerTypeMain: *JSValue,
|
||||
routeModules: *JSValue,
|
||||
clientEntryUrl: *JSValue,
|
||||
styles: *JSValue,
|
||||
) callconv(jsc.conv) c_int {
|
||||
const request: *bun.webcore.Request = @ptrCast(@alignCast(zigRequestPtr));
|
||||
routeDataForInitialization(globalObject, request, routerIndex, routerTypeIndex, routerTypeMain, routeModules, clientEntryUrl, styles) catch |err| {
|
||||
if (err == error.OutOfMemory) bun.outOfMemory();
|
||||
return 0;
|
||||
};
|
||||
return 1;
|
||||
}
|
||||
|
||||
export fn Bake__getProdNewRouteParamsJSFunctionImpl(global: *bun.jsc.JSGlobalObject, callframe: *jsc.CallFrame) callconv(jsc.conv) bun.jsc.JSValue {
|
||||
return jsc.toJSHostCall(global, @src(), newRouteParamsJS, .{ global, callframe });
|
||||
}
|
||||
|
||||
pub fn newRouteParamsJS(global: *bun.jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!bun.jsc.JSValue {
|
||||
if (callframe.argumentsCount() != 2) {
|
||||
return global.throw("Expected 3 arguments", .{});
|
||||
}
|
||||
|
||||
const request_js = callframe.argument(0);
|
||||
const url_js = callframe.argument(1);
|
||||
|
||||
if (!request_js.isObject()) return global.throw("Request must be an object", .{});
|
||||
if (!url_js.isString()) return global.throw("URL must be a string", .{});
|
||||
|
||||
const request = request_js.as(bun.webcore.Request) orelse return global.throw("Request must be a Request object", .{});
|
||||
const self = request.request_context.getBakeProdState() orelse return global.throw("Request context is not a production server state", .{});
|
||||
|
||||
const url = try url_js.toBunString(global);
|
||||
const url_utf8 = url.toUTF8(bun.default_allocator);
|
||||
defer url_utf8.deinit();
|
||||
|
||||
const pathname = FrameworkRouter.extractPathnameFromUrl(url_utf8.byteSlice());
|
||||
var params: bun.bake.FrameworkRouter.MatchedParams = undefined;
|
||||
const route_index = self.getRouter().matchSlow(pathname, ¶ms) orelse return global.throw("No route found for path: {s}", .{url_utf8.byteSlice()});
|
||||
|
||||
const route = self.manifest.routes[route_index.get()];
|
||||
switch (route) {
|
||||
.ssr => {},
|
||||
.ssg => |*ssg| {
|
||||
const html_store = ssg.store orelse return global.throw("No HML blob found for path: {s}", .{url_utf8.byteSlice()});
|
||||
html_store.ref();
|
||||
const blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = html_store,
|
||||
.content_type = bun.http.MimeType.html.value,
|
||||
.globalThis = global,
|
||||
};
|
||||
return jsc.WebCore.Blob.new(blob).toJS(global);
|
||||
},
|
||||
.ssg_many => {
|
||||
// FIXME: i don't like allocating just to make the key. We only use the `params` field of SSG when reconstructing the URL path when we setup the routess
|
||||
var lookup_key = try Manifest.Route.SSG.fromMatchedParams(bun.default_allocator, ¶ms);
|
||||
defer lookup_key.params.deinit(bun.default_allocator);
|
||||
|
||||
const ssg = route.ssg_many.getKeyPtr(lookup_key) orelse
|
||||
return global.throw("No pre-rendered page found for this parameter combination: {s}", .{url_utf8.byteSlice()});
|
||||
|
||||
const html_store = ssg.store orelse return global.throw("No HTML blob found for path: {s}", .{url_utf8.byteSlice()});
|
||||
html_store.ref();
|
||||
const blob = jsc.WebCore.Blob{
|
||||
.size = jsc.WebCore.Blob.max_size,
|
||||
.store = html_store,
|
||||
.content_type = bun.http.MimeType.html.value,
|
||||
.globalThis = global,
|
||||
};
|
||||
return jsc.WebCore.Blob.new(blob).toJS(global);
|
||||
},
|
||||
.empty => return global.throw("Path points to an invalid route: {s}", .{url_utf8.byteSlice()}),
|
||||
}
|
||||
|
||||
const route_info = try self.getRouteInfo(global, route_index);
|
||||
const framework_route = self.getRouter().routes.items[route_index.get()];
|
||||
const router_type_index = framework_route.type.get();
|
||||
|
||||
var result = try JSValue.createEmptyArray(global, 4);
|
||||
result.putIndex(global, 0, JSValue.jsNumberFromUint64(route_index.get())) catch unreachable;
|
||||
result.putIndex(global, 1, JSValue.jsNumberFromUint64(router_type_index)) catch unreachable;
|
||||
result.putIndex(global, 2, route_info) catch unreachable;
|
||||
result.putIndex(global, 3, params.toJS(global)) catch unreachable;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C" fn Bake__getProdNewRouteParamsJSFunction(global: *bun.jsc.JSGlobalObject) callconv(jsc.conv) bun.jsc.JSValue;
|
||||
|
||||
pub fn createParamsObject(
|
||||
self: *Self,
|
||||
global: *bun.jsc.JSGlobalObject,
|
||||
route_index: bun.bake.FrameworkRouter.Route.Index,
|
||||
params: *const bun.bake.FrameworkRouter.MatchedParams,
|
||||
) bun.JSError!bun.jsc.JSValue {
|
||||
const params_structure = try SSRRouteList.getRouteParamsStructure(
|
||||
global,
|
||||
self.route_list.get(),
|
||||
route_index.get(),
|
||||
) orelse params_structure: {
|
||||
// MatchedParams enforces a limit of 64 parameters
|
||||
var js_params: [64]bun.String = undefined;
|
||||
var it = params.keyIterator();
|
||||
var i: usize = 0;
|
||||
while (it.next()) |key| {
|
||||
js_params[i] = bun.String.init(key);
|
||||
i += 1;
|
||||
}
|
||||
const params_structure = try SSRRouteList.createRouteParamsStructure(
|
||||
global,
|
||||
self.route_list.get(),
|
||||
route_index.get(),
|
||||
js_params[0..i],
|
||||
);
|
||||
break :params_structure params_structure;
|
||||
};
|
||||
return try params.toJSWithStructure(global, params_structure);
|
||||
}
|
||||
|
||||
pub fn newRouteParams(
|
||||
self: *Self,
|
||||
global: *bun.jsc.JSGlobalObject,
|
||||
route_index: bun.bake.FrameworkRouter.Route.Index,
|
||||
params: *const bun.bake.FrameworkRouter.MatchedParams,
|
||||
) bun.JSError!struct {
|
||||
route_index: JSValue,
|
||||
router_type_index: JSValue,
|
||||
route_info: JSValue,
|
||||
params: JSValue,
|
||||
newRouteParams: JSValue,
|
||||
setAsyncLocalStorage: JSValue,
|
||||
} {
|
||||
const r = self.getRouter();
|
||||
|
||||
// Look up the route to get its router type
|
||||
const framework_route = &r.routes.items[route_index.get()];
|
||||
const router_type_index = framework_route.type.get();
|
||||
|
||||
// Convert params to JSValue
|
||||
const params_js = try self.createParamsObject(global, route_index, params);
|
||||
|
||||
// Get the setAsyncLocalStorage function that properly sets up the AsyncLocalStorage instance
|
||||
const setAsyncLocalStorage = Bake__getEnsureAsyncLocalStorageInstanceJSFunction(global);
|
||||
|
||||
const route_info = try self.getRouteInfo(global, route_index);
|
||||
|
||||
return .{
|
||||
.route_index = JSValue.jsNumberFromUint64(route_index.get()),
|
||||
.router_type_index = JSValue.jsNumberFromUint64(router_type_index),
|
||||
.route_info = route_info,
|
||||
.params = params_js,
|
||||
.newRouteParams = Bake__getProdNewRouteParamsJSFunction(global),
|
||||
.setAsyncLocalStorage = setAsyncLocalStorage,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Bake__getEnsureAsyncLocalStorageInstanceJSFunction(global: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
const f = @extern(*const fn (*jsc.JSGlobalObject) callconv(.c) jsc.JSValue, .{ .name = "Bake__getEnsureAsyncLocalStorageInstanceJSFunction" }).*;
|
||||
return f(global);
|
||||
}
|
||||
|
||||
pub fn reconstructPathFromParams(
|
||||
this: *Self,
|
||||
allocator: std.mem.Allocator,
|
||||
route_index: u32,
|
||||
params: *const bun.BabyList(bun.bake.Manifest.ParamEntry),
|
||||
) ![]const u8 {
|
||||
const router = this.getRouter();
|
||||
if (route_index >= router.routes.items.len) return error.InvalidRouteIndex;
|
||||
|
||||
const target_route = &router.routes.items[route_index];
|
||||
var parts = std.ArrayList(u8).init(allocator);
|
||||
defer parts.deinit();
|
||||
|
||||
// Reconstruct the URL path from the route parts and params
|
||||
var current_route: ?*const bun.bake.FrameworkRouter.Route = target_route;
|
||||
var path_parts = std.ArrayList(bun.bake.FrameworkRouter.Part).init(allocator);
|
||||
defer path_parts.deinit();
|
||||
|
||||
// Collect all parts from parent routes to build the full path
|
||||
while (current_route) |r| {
|
||||
try path_parts.append(r.part);
|
||||
if (r.parent.unwrap()) |parent_idx| {
|
||||
current_route = &router.routes.items[parent_idx.get()];
|
||||
} else {
|
||||
current_route = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse the parts array since we collected from child to parent
|
||||
std.mem.reverse(bun.bake.FrameworkRouter.Part, path_parts.items);
|
||||
|
||||
// Build the URL path
|
||||
for (path_parts.items) |part| {
|
||||
if (part == .text and part.text.len == 0) continue;
|
||||
try parts.append('/');
|
||||
switch (part) {
|
||||
.text => |text| try parts.appendSlice(text),
|
||||
.param => |param_name| {
|
||||
// Find the param value from the params list
|
||||
var found = false;
|
||||
for (params.slice()) |param| {
|
||||
if (strings.eql(param.key, param_name)) {
|
||||
switch (param.value) {
|
||||
.single => |val| try parts.appendSlice(val),
|
||||
.multiple => |vals| {
|
||||
// For regular params, just use the first value
|
||||
if (vals.len > 0) {
|
||||
try parts.appendSlice(vals.slice()[0]);
|
||||
}
|
||||
},
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// If param not found, use the param name as placeholder
|
||||
try parts.append('[');
|
||||
try parts.appendSlice(param_name);
|
||||
try parts.append(']');
|
||||
}
|
||||
},
|
||||
.catch_all, .catch_all_optional => |name| {
|
||||
// For catch-all routes, look for the param with multiple values
|
||||
var found = false;
|
||||
for (params.slice()) |param| {
|
||||
if (strings.eql(param.key, name)) {
|
||||
switch (param.value) {
|
||||
.single => |val| try parts.appendSlice(val),
|
||||
.multiple => |vals| {
|
||||
// Join all values with slashes for catch-all
|
||||
for (vals.slice(), 0..) |val, i| {
|
||||
if (i > 0) try parts.append('/');
|
||||
try parts.appendSlice(val);
|
||||
}
|
||||
},
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// If param not found, use placeholder
|
||||
try parts.appendSlice("[...");
|
||||
try parts.appendSlice(name);
|
||||
try parts.append(']');
|
||||
}
|
||||
},
|
||||
.group => {}, // Groups don't affect URL
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.items.len == 0) {
|
||||
try parts.append('/');
|
||||
}
|
||||
|
||||
return try parts.toOwnedSlice();
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const bake = bun.bake;
|
||||
const strings = bun.strings;
|
||||
const logger = bun.logger;
|
||||
const Loc = logger.Loc;
|
||||
|
||||
const Route = bun.bake.FrameworkRouter.Route;
|
||||
const SSRRouteList = bun.bake.SSRRouteList;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSError = bun.JSError;
|
||||
const CallFrame = jsc.CallFrame;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
const E = bun.ast.E;
|
||||
|
||||
const DirInfo = bun.resolver.DirInfo;
|
||||
const Resolver = bun.resolver.Resolver;
|
||||
|
||||
const mem = std.mem;
|
||||
const Allocator = mem.Allocator;
|
||||
const Manifest = bun.bake.Manifest;
|
||||
|
||||
const ServerConfig = bun.api.server.ServerConfig;
|
||||
const AnyServer = bun.api.server.AnyServer;
|
||||
|
||||
const Output = bun.Output;
|
||||
const FileRoute = bun.api.server.FileRoute;
|
||||
const StaticRoute = bun.api.server.StaticRoute;
|
||||
|
||||
const Environment = bun.Environment;
|
||||
|
||||
const FrameworkRouter = bun.bake.FrameworkRouter;
|
||||
const std = @import("std");
|
||||
const uws = bun.uws;
|
||||
48
src/bake/prod/SSRRouteList.zig
Normal file
48
src/bake/prod/SSRRouteList.zig
Normal file
@@ -0,0 +1,48 @@
|
||||
extern "C" fn Bun__BakeProductionSSRRouteList__create(globalObject: *JSGlobalObject, route_count: usize) JSValue;
|
||||
extern "C" fn Bun__BakeProductionSSRRouteList__getRouteInfo(globalObject: *JSGlobalObject, route_list_object: JSValue, index: usize) JSValue;
|
||||
extern "C" fn Bun__BakeProductionSSRRouteList__createRouteParamsStructure(globalObject: *JSGlobalObject, route_list_object: JSValue, index: usize, params: [*]const bun.String, params_count: usize) JSValue;
|
||||
extern "C" fn Bun__BakeProductionSSRRouteList__getRouteParamsStructure(globalObject: *JSGlobalObject, route_list_object: JSValue, index: usize) JSValue;
|
||||
|
||||
pub fn create(globalObject: *JSGlobalObject, route_count: usize) JSError!JSValue {
|
||||
return jsc.fromJSHostCall(
|
||||
globalObject,
|
||||
@src(),
|
||||
Bun__BakeProductionSSRRouteList__create,
|
||||
.{ globalObject, route_count },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn createRouteParamsStructure(globalObject: *JSGlobalObject, route_list_object: JSValue, index: usize, params: []const bun.String) JSError!JSValue {
|
||||
return jsc.fromJSHostCall(
|
||||
globalObject,
|
||||
@src(),
|
||||
Bun__BakeProductionSSRRouteList__createRouteParamsStructure,
|
||||
.{ globalObject, route_list_object, index, params.ptr, params.len },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getRouteParamsStructure(globalObject: *JSGlobalObject, route_list_object: JSValue, index: usize) JSError!?JSValue {
|
||||
const value = try jsc.fromJSHostCall(
|
||||
globalObject,
|
||||
@src(),
|
||||
Bun__BakeProductionSSRRouteList__getRouteParamsStructure,
|
||||
.{ globalObject, route_list_object, index },
|
||||
);
|
||||
if (value.isEmptyOrUndefinedOrNull()) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
pub fn getRouteInfo(globalObject: *JSGlobalObject, route_list_object: JSValue, index: usize) JSError!JSValue {
|
||||
return jsc.fromJSHostCall(
|
||||
globalObject,
|
||||
@src(),
|
||||
Bun__BakeProductionSSRRouteList__getRouteInfo,
|
||||
.{ globalObject, route_list_object, index },
|
||||
);
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const JSGlobalObject = bun.jsc.JSGlobalObject;
|
||||
const JSError = bun.JSError;
|
||||
const JSValue = bun.jsc.JSValue;
|
||||
const jsc = bun.jsc;
|
||||
174
src/bake/production-runtime-server.ts
Normal file
174
src/bake/production-runtime-server.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
// This file is the entrypoint to the production SSR runtime.
|
||||
// It handles server-side rendering for production builds.
|
||||
import type { Bake } from "bun";
|
||||
const { AsyncLocalStorage } = require("node:async_hooks");
|
||||
|
||||
// In production, we don't need HMR-related imports
|
||||
// We'll load modules directly from the bundled output
|
||||
|
||||
export type RequestContext = {
|
||||
responseOptions: ResponseInit;
|
||||
streaming: boolean;
|
||||
streamingStarted?: boolean;
|
||||
renderAbort?: (path: string, params: Record<string, any> | null) => never;
|
||||
};
|
||||
|
||||
// Create the AsyncLocalStorage instance for propagating response options
|
||||
const responseOptionsALS = new AsyncLocalStorage();
|
||||
// let responseOptionsALS = {
|
||||
// run: async (storeValue, fn) => {
|
||||
// return await fn();
|
||||
// },
|
||||
// };
|
||||
let asyncLocalStorageWasSet = false;
|
||||
|
||||
type Module = unknown;
|
||||
|
||||
type RouteArgs = {
|
||||
serverEntrypoint: string;
|
||||
routeModules: string[];
|
||||
clientEntryUrl: string;
|
||||
styles: string[];
|
||||
};
|
||||
|
||||
type RouteInfo = {
|
||||
serverEntrypoint: Module & Bake.ServerEntryPoint;
|
||||
routeModules: Module[];
|
||||
clientEntryUrl: string;
|
||||
styles: string[];
|
||||
dataForInitialization(req: Request, routeIndex: number, routerTypeIndex: number): RouteArgs;
|
||||
initializing: Promise<unknown> | undefined;
|
||||
};
|
||||
|
||||
interface Exports {
|
||||
handleRequest: (
|
||||
req: Request,
|
||||
routeIndex: number,
|
||||
routerTypeIndex: number,
|
||||
routeInfo: RouteInfo,
|
||||
params: Record<string, string> | null,
|
||||
newRouteParams: (
|
||||
req: Request,
|
||||
url: string,
|
||||
) =>
|
||||
| [routeIndex: number, routerTypeIndex: number, routeInfo: RouteInfo, params: Record<string, string> | null]
|
||||
| Blob,
|
||||
setAsyncLocalStorage: Function,
|
||||
) => Promise<Response>;
|
||||
}
|
||||
|
||||
declare let server_exports: Exports;
|
||||
|
||||
server_exports = {
|
||||
async handleRequest(req, routeIndex, routerTypeIndex, routeInfo, params, newRouteParams, setAsyncLocalStorage) {
|
||||
// Set up AsyncLocalStorage if not already done
|
||||
if (!asyncLocalStorageWasSet) {
|
||||
asyncLocalStorageWasSet = true;
|
||||
setAsyncLocalStorage(responseOptionsALS);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (routeInfo.initializing) {
|
||||
await routeInfo.initializing;
|
||||
routeInfo.initializing = undefined;
|
||||
}
|
||||
|
||||
if (!routeInfo.serverEntrypoint) {
|
||||
const args = routeInfo.dataForInitialization(req, routeIndex, routerTypeIndex);
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
routeInfo.initializing = promise;
|
||||
|
||||
try {
|
||||
routeInfo.serverEntrypoint = await import(args.serverEntrypoint);
|
||||
routeInfo.routeModules = await Promise.all(args.routeModules.map(modulePath => import(modulePath)));
|
||||
routeInfo.clientEntryUrl = args.clientEntryUrl;
|
||||
routeInfo.styles = args.styles;
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
throw error;
|
||||
} finally {
|
||||
routeInfo.initializing = undefined;
|
||||
}
|
||||
|
||||
if (!routeInfo.serverEntrypoint.render) {
|
||||
throw new Error('Framework server entrypoint is missing a "render" export.');
|
||||
}
|
||||
if (typeof routeInfo.serverEntrypoint.render !== "function") {
|
||||
throw new Error('Framework server entrypoint\'s "render" export is not a function.');
|
||||
}
|
||||
}
|
||||
|
||||
const serverRenderer = routeInfo.serverEntrypoint.render;
|
||||
|
||||
// Load all route modules (page and layouts)
|
||||
const [pageModule, ...layouts] = routeInfo.routeModules;
|
||||
|
||||
// Set up the request context for AsyncLocalStorage
|
||||
let storeValue: RequestContext = {
|
||||
responseOptions: {},
|
||||
streaming: (pageModule as { streaming?: boolean }).streaming ?? false,
|
||||
};
|
||||
|
||||
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(
|
||||
req,
|
||||
{
|
||||
styles: routeInfo.styles,
|
||||
modules: [routeInfo.clientEntryUrl],
|
||||
layouts,
|
||||
pageModule,
|
||||
modulepreload: [],
|
||||
params,
|
||||
request: pageModule.mode === "ssr" ? req : undefined,
|
||||
},
|
||||
responseOptionsALS,
|
||||
);
|
||||
});
|
||||
|
||||
if (!(response instanceof Response)) {
|
||||
throw new Error("Server-side request handler was expected to return a Response object.");
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// For `Response.render(...)`/`Response.redirect(...)` we throw the
|
||||
// response to stop React from rendering
|
||||
if (error instanceof Response) {
|
||||
const resp = error;
|
||||
|
||||
// Handle `Response.render(...)`
|
||||
if (resp.status !== 302) {
|
||||
const newUrl = resp.headers.get("location");
|
||||
if (!newUrl) {
|
||||
throw new Error("Response.render(...) was expected to have a Location header");
|
||||
}
|
||||
|
||||
const result = newRouteParams(req, newUrl);
|
||||
if (result instanceof Blob) {
|
||||
console.log("Returning a blob", result);
|
||||
return new Response(result);
|
||||
}
|
||||
const [newRouteIndex, newRouterTypeIndex, newRouteInfo, newParams] = result;
|
||||
|
||||
routeIndex = newRouteIndex;
|
||||
routerTypeIndex = newRouterTypeIndex;
|
||||
routeInfo = newRouteInfo;
|
||||
params = newParams;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// `Response.redirect(...)` or others, just return it
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -109,18 +109,20 @@ pub fn buildCommand(ctx: bun.cli.Command.Context) !void {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeSourcemapToDisk(
|
||||
pub fn registerSourcemap(
|
||||
allocator: std.mem.Allocator,
|
||||
root_dir: std.fs.Dir,
|
||||
file: *const OutputFile,
|
||||
bundled_outputs: []const OutputFile,
|
||||
bundled_outputs: []OutputFile,
|
||||
source_maps: *bun.StringArrayHashMapUnmanaged(OutputFile.Index),
|
||||
write_to_dist: bool,
|
||||
) !void {
|
||||
// don't call this if the file does not have sourcemaps!
|
||||
bun.assert(file.source_map_index != std.math.maxInt(u32));
|
||||
|
||||
// TODO: should we just write the sourcemaps to disk?
|
||||
const source_map_index = file.source_map_index;
|
||||
const source_map_file: *const OutputFile = &bundled_outputs[source_map_index];
|
||||
const source_map_file: *OutputFile = &bundled_outputs[source_map_index];
|
||||
bun.assert(source_map_file.output_kind == .sourcemap);
|
||||
|
||||
const without_prefix = if (bun.strings.hasPrefixComptime(file.dest_path, "./") or
|
||||
@@ -134,6 +136,13 @@ pub fn writeSourcemapToDisk(
|
||||
try std.fmt.allocPrint(allocator, "bake:/{s}", .{without_prefix}),
|
||||
OutputFile.Index.init(@intCast(source_map_index)),
|
||||
);
|
||||
|
||||
if (write_to_dist) {
|
||||
source_map_file.writeToDisk(root_dir, ".") catch |err| {
|
||||
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
||||
Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(file.dest_path)});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMachine, pt: *PerThread) !void {
|
||||
@@ -256,6 +265,14 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
// these share pointers right now, so setting NODE_ENV == production on one should affect all
|
||||
bun.assert(server_transpiler.env == client_transpiler.env);
|
||||
|
||||
const original_roots = brk: {
|
||||
const roots = try allocator.alloc([]const u8, options.framework.file_system_router_types.len);
|
||||
for (options.framework.file_system_router_types, roots) |*in, *out| {
|
||||
out.* = in.root;
|
||||
}
|
||||
break :brk roots;
|
||||
};
|
||||
|
||||
framework.* = framework.resolve(&server_transpiler.resolver, &client_transpiler.resolver, allocator) catch {
|
||||
if (framework.is_built_in_react)
|
||||
try bake.Framework.addReactInstallCommandNote(server_transpiler.log);
|
||||
@@ -310,6 +327,14 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
FrameworkRouter.InsertionContext.wrap(EntryPointMap, &entry_points),
|
||||
);
|
||||
|
||||
// Add the production SSR runtime server as an entry point
|
||||
const production_ssr_runtime_path = bun.path.joinAbs(
|
||||
bun.Environment.base_path,
|
||||
.auto,
|
||||
"src/bake/production-runtime-server.ts",
|
||||
);
|
||||
const production_ssr_runtime_id = try entry_points.getOrPutEntryPoint(production_ssr_runtime_path, .server);
|
||||
|
||||
const bundled_outputs_list = try bun.BundleV2.generateFromBakeProductionCLI(
|
||||
entry_points,
|
||||
&server_transpiler,
|
||||
@@ -336,6 +361,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
defer root_dir.close();
|
||||
|
||||
var maybe_runtime_file_index: ?u32 = null;
|
||||
var maybe_ssr_runtime_file_index: ?u32 = null;
|
||||
|
||||
var css_chunks_count: usize = 0;
|
||||
var css_chunks_first: usize = 0;
|
||||
@@ -382,6 +408,13 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
maybe_runtime_file_index = @intCast(i);
|
||||
}
|
||||
|
||||
// Check if this is the SSR runtime server file
|
||||
if (file.entry_point_index) |ep| {
|
||||
if (ep == production_ssr_runtime_id.get()) {
|
||||
maybe_ssr_runtime_file_index = @intCast(i);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe not do all the disk-writing in 1 thread?
|
||||
switch (file.side orelse continue) {
|
||||
.client => {
|
||||
@@ -392,18 +425,18 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
};
|
||||
},
|
||||
.server => {
|
||||
if (ctx.bundler_options.bake_debug_dump_server) {
|
||||
_ = file.writeToDisk(root_dir, ".") catch |err| {
|
||||
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
||||
Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(file.dest_path)});
|
||||
};
|
||||
}
|
||||
// Always write server files to disk for SSR support
|
||||
// SSR pages need their server bundles available at runtime
|
||||
_ = file.writeToDisk(root_dir, ".") catch |err| {
|
||||
bun.handleErrorReturnTrace(err, @errorReturnTrace());
|
||||
Output.err(err, "Failed to write {} to output directory", .{bun.fmt.quote(file.dest_path)});
|
||||
};
|
||||
|
||||
// If the file has a sourcemap, store it so we can put it on
|
||||
// `PerThread` so we can provide sourcemapped stacktraces for
|
||||
// server components.
|
||||
if (file.source_map_index != std.math.maxInt(u32)) {
|
||||
try writeSourcemapToDisk(allocator, &file, bundled_outputs, &source_maps);
|
||||
try registerSourcemap(allocator, root_dir, &file, bundled_outputs, &source_maps, true);
|
||||
}
|
||||
|
||||
switch (file.output_kind) {
|
||||
@@ -439,7 +472,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
|
||||
// TODO: should we just write the sourcemaps to disk?
|
||||
if (file.source_map_index != std.math.maxInt(u32)) {
|
||||
try writeSourcemapToDisk(allocator, &file, bundled_outputs, &source_maps);
|
||||
try registerSourcemap(allocator, root_dir, &file, bundled_outputs, &source_maps, true);
|
||||
}
|
||||
}
|
||||
// Write the runtime file to disk if there are any client chunks
|
||||
@@ -482,8 +515,21 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
const server_render_funcs = try JSValue.createEmptyArray(global, router.types.len);
|
||||
const server_param_funcs = try JSValue.createEmptyArray(global, router.types.len);
|
||||
const client_entry_urls = try JSValue.createEmptyArray(global, router.types.len);
|
||||
const router_type_roots = try JSValue.createEmptyArray(global, router.types.len);
|
||||
const router_type_server_entrypoints = try JSValue.createEmptyArray(global, router.types.len);
|
||||
|
||||
for (router.types, original_roots, 0..) |router_type, root, i| {
|
||||
// Add the router type root path to the array (relative path)
|
||||
try router_type_roots.putIndex(global, @intCast(i), try bun.String.createUTF8ForJS(global, root));
|
||||
|
||||
// Add the server entrypoint path for this router type
|
||||
const server_module_key = module_keys[router_type.server_file.get()];
|
||||
const server_entrypoint_js = if (server_module_key.isEmpty())
|
||||
JSValue.null
|
||||
else
|
||||
server_module_key.toJS(global);
|
||||
try router_type_server_entrypoints.putIndex(global, @intCast(i), server_entrypoint_js);
|
||||
|
||||
for (router.types, 0..) |router_type, i| {
|
||||
if (router_type.client_file.unwrap()) |client_file| {
|
||||
const str = (try bun.String.createFormat("{s}{s}", .{
|
||||
public_path,
|
||||
@@ -589,7 +635,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
params_buf.append(ctx.allocator, route.part.catch_all) catch unreachable;
|
||||
},
|
||||
.catch_all_optional => {
|
||||
return global.throw("catch-all routes are not supported in static site generation", .{});
|
||||
return global.throw("catch-all optional routes are not supported in static site generation", .{});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -682,27 +728,89 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
|
||||
try route_style_references.putIndex(global, @intCast(nav_index), styles);
|
||||
}
|
||||
|
||||
// Get the server runtime path if it exists
|
||||
const server_runtime_js = if (maybe_ssr_runtime_file_index) |ssr_idx| blk: {
|
||||
_ = ssr_idx; // Will use the bundled file later if needed
|
||||
const module_key = module_keys[production_ssr_runtime_id.get()];
|
||||
break :blk if (module_key.isEmpty()) JSValue.null else module_key.toJS(global);
|
||||
} else JSValue.null;
|
||||
|
||||
const render_promise = BakeRenderRoutesForProdStatic(
|
||||
global,
|
||||
// outBase: string
|
||||
bun.String.init(root_dir_path),
|
||||
// allServerFiles: string[]
|
||||
pt.all_server_files,
|
||||
// renderStatic: FrameworkPrerender[]
|
||||
server_render_funcs,
|
||||
// getParams: FrameworkGetParams[]
|
||||
server_param_funcs,
|
||||
// clientEntryUrl: string[]
|
||||
client_entry_urls,
|
||||
// routerTypeRoots: string[]
|
||||
router_type_roots,
|
||||
// routerTypeServerEntrypoints: string[]
|
||||
router_type_server_entrypoints,
|
||||
// serverRuntime: string | null
|
||||
server_runtime_js,
|
||||
|
||||
// patterns: string[]
|
||||
route_patterns,
|
||||
// files: FileIndex[][]
|
||||
route_nested_files,
|
||||
// typeAndFlags: TypeAndFlags[]
|
||||
route_type_and_flags,
|
||||
// sourceRouteFiles: string[]
|
||||
route_source_files,
|
||||
// paramInformation: Array<null | string[]>
|
||||
route_param_info,
|
||||
// styles: string[][]
|
||||
route_style_references,
|
||||
);
|
||||
|
||||
const path_buffer = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(path_buffer);
|
||||
|
||||
render_promise.setHandled(vm.jsc_vm);
|
||||
vm.waitForPromise(.{ .normal = render_promise });
|
||||
switch (render_promise.unwrap(vm.jsc_vm, .mark_handled)) {
|
||||
.pending => unreachable,
|
||||
.fulfilled => {
|
||||
.fulfilled => |manifest_value| {
|
||||
// Add assets field to manifest with all client-side files in _bun directory
|
||||
// First, count how many client assets we have
|
||||
const asset_count: usize = bundled_outputs.len;
|
||||
|
||||
// Create assets array and directly add filenames
|
||||
const assets_js = try JSValue.createEmptyArray(global, asset_count);
|
||||
for (bundled_outputs, 0..) |file, asset_index| {
|
||||
const str = bun.path.joinStringBuf(path_buffer, [_][]const u8{ "/", file.dest_path }, .posix);
|
||||
bun.assert(bun.strings.hasPrefixComptime(str, "/_bun/"));
|
||||
var bunstr = bun.String.init(str);
|
||||
try assets_js.putIndex(global, @intCast(asset_index), bunstr.transferToJS(global));
|
||||
}
|
||||
|
||||
_ = manifest_value.put(global, "assets", assets_js);
|
||||
|
||||
// Write manifest to file
|
||||
const manifest_path = try std.fs.path.join(allocator, &.{ root_dir_path, "manifest.json" });
|
||||
defer allocator.free(manifest_path);
|
||||
|
||||
// Convert JSValue to JSON string
|
||||
var manifest_str = bun.String.empty;
|
||||
defer manifest_str.deref();
|
||||
try manifest_value.jsonStringify(global, 2, &manifest_str);
|
||||
|
||||
// Write the manifest file
|
||||
const manifest_utf8 = manifest_str.toUTF8(allocator);
|
||||
defer manifest_utf8.deinit();
|
||||
|
||||
try std.fs.cwd().writeFile(.{
|
||||
.sub_path = manifest_path,
|
||||
.data = manifest_utf8.slice(),
|
||||
});
|
||||
|
||||
Output.prettyln("done", .{});
|
||||
Output.prettyln("Manifest written to: {s}", .{manifest_path});
|
||||
Output.flush();
|
||||
},
|
||||
.rejected => |err| {
|
||||
@@ -765,6 +873,12 @@ extern fn BakeRenderRoutesForProdStatic(
|
||||
get_params: JSValue,
|
||||
/// Client entry URLs by router type (e.g., ["/client.js", null])
|
||||
client_entry_urls: JSValue,
|
||||
/// Router type root paths by router type (e.g., ["/pages", "/app"])
|
||||
router_type_roots: JSValue,
|
||||
/// Router type server entrypoints by router type (e.g., ["bake://react.server.js"])
|
||||
router_type_server_entrypoints: JSValue,
|
||||
/// Server runtime path (e.g., "bake://production-runtime-server.js")
|
||||
server_runtime: JSValue,
|
||||
/// Route patterns (e.g., ["/", "/about", "/blog/:slug"])
|
||||
patterns: JSValue,
|
||||
/// File indices per route (e.g., [[0], [1], [2, 0]])
|
||||
|
||||
@@ -505,7 +505,9 @@ const PluginsResult = union(enum) {
|
||||
err,
|
||||
};
|
||||
|
||||
pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { debug, production }) type {
|
||||
pub const Protocol = enum { http, https };
|
||||
pub const DevelopmentKind = enum { debug, production };
|
||||
pub fn NewServer(protocol_enum: Protocol, development_kind: DevelopmentKind) type {
|
||||
return struct {
|
||||
pub const js = switch (protocol_enum) {
|
||||
.http => switch (development_kind) {
|
||||
@@ -526,6 +528,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
pub const ssl_enabled = protocol_enum == .https;
|
||||
pub const debug_mode = development_kind == .debug;
|
||||
|
||||
const BakeMethods = bun.bake.ProductionServerMethods(protocol_enum, development_kind);
|
||||
|
||||
const ThisServer = @This();
|
||||
pub const RequestContext = NewRequestContext(ssl_enabled, debug_mode, @This());
|
||||
|
||||
@@ -565,6 +569,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
|
||||
inspector_server_id: jsc.Debugger.DebuggerId = .init(0),
|
||||
|
||||
bake_prod: bun.ptr.Owned(?*bun.bake.ProductionServerState) = .fromRaw(null),
|
||||
|
||||
pub const doStop = host_fn.wrapInstanceMethod(ThisServer, "stopFromJS", false);
|
||||
pub const dispose = host_fn.wrapInstanceMethod(ThisServer, "disposeFromJS", false);
|
||||
pub const doUpgrade = host_fn.wrapInstanceMethod(ThisServer, "onUpgrade", false);
|
||||
@@ -1574,6 +1580,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
pub fn deinit(this: *ThisServer) void {
|
||||
httplog("deinit", .{});
|
||||
|
||||
this.bake_prod.deinit();
|
||||
|
||||
// This should've already been handled in stopListening
|
||||
// However, when the JS VM terminates, it hypothetically might not call stopListening
|
||||
this.notifyInspectorServerStopped();
|
||||
@@ -1608,14 +1616,17 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
errdefer bun.default_allocator.free(base_url);
|
||||
|
||||
const dev_server = if (config.bake) |*bake_options|
|
||||
try bun.bake.DevServer.init(.{
|
||||
.arena = bake_options.arena.allocator(),
|
||||
.root = bake_options.root,
|
||||
.framework = bake_options.framework,
|
||||
.bundler_options = bake_options.bundler_options,
|
||||
.vm = global.bunVM(),
|
||||
.broadcast_console_log_from_browser_to_server = config.broadcast_console_log_from_browser_to_server_for_bake,
|
||||
})
|
||||
if (config.development != .production)
|
||||
try bun.bake.DevServer.init(.{
|
||||
.arena = bake_options.arena.allocator(),
|
||||
.root = bake_options.root,
|
||||
.framework = bake_options.framework,
|
||||
.bundler_options = bake_options.bundler_options,
|
||||
.vm = global.bunVM(),
|
||||
.broadcast_console_log_from_browser_to_server = config.broadcast_console_log_from_browser_to_server_for_bake,
|
||||
})
|
||||
else
|
||||
null
|
||||
else
|
||||
null;
|
||||
errdefer if (dev_server) |d| d.deinit();
|
||||
@@ -1629,6 +1640,16 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
.dev_server = dev_server,
|
||||
});
|
||||
|
||||
if (server.config.bake != null and server.config.development == .production) {
|
||||
var bake_prod = try bake.ProductionServerState.create(
|
||||
global,
|
||||
&server.vm.transpiler,
|
||||
&server.config,
|
||||
&server.config.bake.?,
|
||||
);
|
||||
server.bake_prod = bake_prod.toOptional();
|
||||
}
|
||||
|
||||
if (RequestContext.pool == null) {
|
||||
RequestContext.pool = bun.create(
|
||||
server.allocator,
|
||||
@@ -2588,7 +2609,12 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
}
|
||||
}
|
||||
|
||||
// --- 6. Initialize plugins if needed ---
|
||||
// --- 6. Handle Bake manifest routes (SSG) ---
|
||||
if (this.bake_prod.get()) |prod| {
|
||||
BakeMethods.setBakeManifestRoutes(this, app, prod.manifest);
|
||||
}
|
||||
|
||||
// --- 7. Initialize plugins if needed ---
|
||||
if (needs_plugins and this.plugins == null) {
|
||||
if (this.vm.transpiler.options.serve_plugins) |serve_plugins_config| {
|
||||
if (serve_plugins_config.len > 0) {
|
||||
@@ -2597,7 +2623,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
}
|
||||
}
|
||||
|
||||
// --- 7. Debug mode specific routes ---
|
||||
// --- 8. Debug mode specific routes ---
|
||||
if (debug_mode) {
|
||||
app.get("/bun:info", *ThisServer, this, onBunInfoRequest);
|
||||
if (this.config.inspector) {
|
||||
@@ -2606,7 +2632,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
}
|
||||
}
|
||||
|
||||
// --- 8. Handle DevServer routes & Track "/*" Coverage ---
|
||||
// --- 9. Handle DevServer routes & Track "/*" Coverage ---
|
||||
var has_dev_server_for_star_path = false;
|
||||
if (dev_server) |dev| {
|
||||
// dev.setRoutes might register its own "/*" HTTP handler
|
||||
@@ -2629,7 +2655,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
}
|
||||
}
|
||||
|
||||
// --- 9. Consolidated "/*" HTTP Fallback Registration ---
|
||||
// --- 10. Consolidated "/*" HTTP Fallback Registration ---
|
||||
if (star_methods_covered_by_user.eql(bun.http.Method.Set.initFull())) {
|
||||
// User/Static/Dev has already provided a "/*" handler for ALL methods.
|
||||
// No further global "/*" HTTP fallback needed.
|
||||
@@ -2640,21 +2666,31 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
star_methods_covered_by_user.toggleAll();
|
||||
var iter = star_methods_covered_by_user.iterator();
|
||||
while (iter.next()) |method_to_cover| {
|
||||
switch (this.config.onNodeHTTPRequest) {
|
||||
.zero => switch (this.config.onRequest) {
|
||||
.zero => app.method(method_to_cover, "/*", *ThisServer, this, on404),
|
||||
else => app.method(method_to_cover, "/*", *ThisServer, this, onRequest),
|
||||
},
|
||||
else => app.method(method_to_cover, "/*", *ThisServer, this, onNodeHTTPRequest),
|
||||
// If we have a bake manifest and router for SSR routes, use the SSR handler
|
||||
if (this.bake_prod.get() != null) {
|
||||
app.method(method_to_cover, "/*", *ThisServer, this, BakeMethods.bakeProductionSSRRouteHandler);
|
||||
} else {
|
||||
switch (this.config.onNodeHTTPRequest) {
|
||||
.zero => switch (this.config.onRequest) {
|
||||
.zero => app.method(method_to_cover, "/*", *ThisServer, this, on404),
|
||||
else => app.method(method_to_cover, "/*", *ThisServer, this, onRequest),
|
||||
},
|
||||
else => app.method(method_to_cover, "/*", *ThisServer, this, onNodeHTTPRequest),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (this.config.onNodeHTTPRequest) {
|
||||
.zero => switch (this.config.onRequest) {
|
||||
.zero => app.any("/*", *ThisServer, this, on404),
|
||||
else => app.any("/*", *ThisServer, this, onRequest),
|
||||
},
|
||||
else => app.any("/*", *ThisServer, this, onNodeHTTPRequest),
|
||||
// If we have a bake manifest and router for SSR routes, use the SSR handler
|
||||
if (this.bake_prod.get() != null) {
|
||||
app.any("/*", *ThisServer, this, BakeMethods.bakeProductionSSRRouteHandler);
|
||||
} else {
|
||||
switch (this.config.onNodeHTTPRequest) {
|
||||
.zero => switch (this.config.onRequest) {
|
||||
.zero => app.any("/*", *ThisServer, this, on404),
|
||||
else => app.any("/*", *ThisServer, this, onRequest),
|
||||
},
|
||||
else => app.any("/*", *ThisServer, this, onNodeHTTPRequest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3430,3 +3466,5 @@ const Fetch = WebCore.Fetch;
|
||||
const Headers = WebCore.Headers;
|
||||
const Request = WebCore.Request;
|
||||
const Response = WebCore.Response;
|
||||
|
||||
const bake = bun.bake;
|
||||
|
||||
@@ -18,6 +18,28 @@ pub fn init(request_ctx: anytype) AnyRequestContext {
|
||||
return .{ .tagged_pointer = Pointer.init(request_ctx) };
|
||||
}
|
||||
|
||||
pub fn getBakeProdState(self: AnyRequestContext) ?*bun.bake.ProductionServerState {
|
||||
if (self.tagged_pointer.isNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (self.tagged_pointer.tag()) {
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPServer.RequestContext))) => {
|
||||
return self.tagged_pointer.as(HTTPServer.RequestContext).getBakeProdState();
|
||||
},
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(HTTPSServer.RequestContext))) => {
|
||||
return self.tagged_pointer.as(HTTPSServer.RequestContext).getBakeProdState();
|
||||
},
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPServer.RequestContext))) => {
|
||||
return self.tagged_pointer.as(DebugHTTPServer.RequestContext).getBakeProdState();
|
||||
},
|
||||
@field(Pointer.Tag, bun.meta.typeBaseName(@typeName(DebugHTTPSServer.RequestContext))) => {
|
||||
return self.tagged_pointer.as(DebugHTTPSServer.RequestContext).getBakeProdState();
|
||||
},
|
||||
else => @panic("Unexpected AnyRequestContext tag"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setAdditionalOnAbortCallback(self: AnyRequestContext, cb: ?AdditionalOnAbortCallback) void {
|
||||
if (self.tagged_pointer.isNull()) {
|
||||
return;
|
||||
|
||||
@@ -2528,6 +2528,14 @@ pub fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool,
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getBakeProdState(this: *RequestContext) ?*bun.bake.ProductionServerState {
|
||||
if (this.server) |server| {
|
||||
return server.bake_prod.get();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
comptime {
|
||||
const export_prefix = "Bun__HTTPRequestContext" ++ (if (debug_mode) "Debug" else "") ++ (if (ThisServer.ssl_enabled) "TLS" else "");
|
||||
if (bun.Environment.export_cpp_apis) {
|
||||
|
||||
@@ -62,6 +62,9 @@ negative_routes: std.ArrayList([:0]const u8) = std.ArrayList([:0]const u8).init(
|
||||
user_routes_to_build: std.ArrayList(UserRouteBuilder) = std.ArrayList(UserRouteBuilder).init(bun.default_allocator),
|
||||
|
||||
bake: ?bun.bake.UserOptions = null,
|
||||
/// Pointer is allocated by the arena in the manifest
|
||||
/// Owned by this struct
|
||||
bake_manifest: ?*bun.bake.Manifest = null,
|
||||
|
||||
pub const DevelopmentOption = enum {
|
||||
development,
|
||||
@@ -277,6 +280,11 @@ pub fn deinit(this: *ServerConfig) void {
|
||||
bake.deinit();
|
||||
}
|
||||
|
||||
if (this.bake_manifest) |manifest| {
|
||||
manifest.deinit();
|
||||
this.bake_manifest = null;
|
||||
}
|
||||
|
||||
for (this.user_routes_to_build.items) |*builder| {
|
||||
builder.deinit();
|
||||
}
|
||||
@@ -408,9 +416,9 @@ pub fn fromJS(
|
||||
var has_hostname = false;
|
||||
|
||||
defer {
|
||||
if (!args.development.isHMREnabled()) {
|
||||
bun.assert(args.bake == null);
|
||||
}
|
||||
// if (!args.development.isHMREnabled()) {
|
||||
// bun.assert(args.bake == null);
|
||||
// }
|
||||
}
|
||||
|
||||
if (strings.eqlComptime(env.get("NODE_ENV") orelse "", "production")) {
|
||||
@@ -827,11 +835,84 @@ pub fn fromJS(
|
||||
return global.throwInvalidArguments("'app' + HTML loader not supported.", .{});
|
||||
}
|
||||
|
||||
if (args.development == .production) {
|
||||
return global.throwInvalidArguments("TODO: 'development: false' in serve options with 'app'. For now, use `bun build --app` or set 'development: true'", .{});
|
||||
}
|
||||
|
||||
args.bake = try bun.bake.UserOptions.fromJS(bake_args_js, global);
|
||||
|
||||
if (args.development == .production) {
|
||||
const fd = switch (bun.sys.open("dist/manifest.json", bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| {
|
||||
const path: []const u8 = "dist/manifest.json";
|
||||
const errjs = err.withPath(path).toJS(global);
|
||||
return global.throwValue(errjs);
|
||||
},
|
||||
};
|
||||
defer fd.close();
|
||||
var log = bun.logger.Log.init(bun.default_allocator);
|
||||
defer log.deinit();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
errdefer arena.deinit();
|
||||
|
||||
var types = try std.ArrayListUnmanaged(bun.bake.FrameworkRouter.Type).initCapacity(
|
||||
bun.default_allocator,
|
||||
args.bake.?.framework.file_system_router_types.len,
|
||||
);
|
||||
errdefer types.deinit(bun.default_allocator);
|
||||
|
||||
const root = args.bake.?.root;
|
||||
const transpiler = &global.bunVM().transpiler;
|
||||
for (args.bake.?.framework.file_system_router_types) |fsr| {
|
||||
const buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(buf);
|
||||
const joined_root = bun.path.joinAbsStringBuf(root, buf, &.{fsr.root}, .auto);
|
||||
const entry = transpiler.resolver.readDirInfoIgnoreError(joined_root) orelse
|
||||
continue;
|
||||
|
||||
types.appendAssumeCapacity(.{
|
||||
.abs_root = bun.strings.withoutTrailingSlash(entry.abs_path),
|
||||
.prefix = fsr.prefix,
|
||||
.ignore_underscores = fsr.ignore_underscores,
|
||||
.ignore_dirs = fsr.ignore_dirs,
|
||||
.extensions = fsr.extensions,
|
||||
.style = fsr.style,
|
||||
.allow_layouts = fsr.allow_layouts,
|
||||
// In production, we don't track individual files as they're already bundled
|
||||
.server_file = bun.bake.FrameworkRouter.OpaqueFileId.init(0),
|
||||
.client_file = if (fsr.entry_client) |_|
|
||||
bun.bake.FrameworkRouter.OpaqueFileId.init(1).toOptional()
|
||||
else
|
||||
.none,
|
||||
.server_file_string = .empty,
|
||||
});
|
||||
}
|
||||
|
||||
var router: ?bun.ptr.Owned(*bun.bake.FrameworkRouter) = bun.ptr.Owned(*bun.bake.FrameworkRouter).alloc(try bun.bake.FrameworkRouter.initEmpty(root, types: {
|
||||
const ret = types.items;
|
||||
types = .{};
|
||||
break :types ret;
|
||||
}, bun.default_allocator)) catch bun.outOfMemory();
|
||||
errdefer if (router) |*r| {
|
||||
r.get().deinit(bun.default_allocator);
|
||||
r.deinitShallow();
|
||||
};
|
||||
|
||||
var manifest = bun.bake.Manifest{
|
||||
.arena = arena: {
|
||||
const ret = arena;
|
||||
arena = std.heap.ArenaAllocator.init(bun.default_allocator);
|
||||
break :arena ret;
|
||||
},
|
||||
.router = bun.take(&router).?,
|
||||
};
|
||||
errdefer manifest.deinit();
|
||||
manifest.fromFD(fd, &log) catch |err| {
|
||||
if (err == error.InvalidManifest) {
|
||||
return global.throwValue(try log.toJS(global, bun.default_allocator, "Failed to parse manifest.json"));
|
||||
}
|
||||
return global.throwError(err, "Failed to parse manifest.json");
|
||||
};
|
||||
args.bake_manifest = try manifest.allocate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ extern "C" SYSV_ABI EncodedJSValue Bake__createDevServerFrameworkRequestArgsObje
|
||||
auto& vm = globalObject->vm();
|
||||
|
||||
auto* zig = jsCast<Zig::GlobalObject*>(globalObject);
|
||||
auto* object = JSFinalObject::create(vm, zig->bakeAdditions().m_DevServerFrameworkRequestArgsClassStructure.get(zig));
|
||||
auto* object = JSFinalObject::create(vm, zig->bakeAdditions().m_FrameworkRequestArgsClassStructure.get(zig));
|
||||
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(jsUndefined()));
|
||||
|
||||
object->putDirectOffset(vm, 0, JSValue::decode(routerTypeMain));
|
||||
@@ -122,10 +122,17 @@ BUN_DEFINE_HOST_FUNCTION(jsFunctionBakeGetBundleNewRouteJSFunction, (JSC::JSGlob
|
||||
return Bake__bundleNewRouteJSFunctionImpl(globalObject, request->m_ctx, url);
|
||||
}
|
||||
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue Bake__getNewRouteParamsJSFunction(JSC::JSGlobalObject* globalObject)
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue Bake__getDevNewRouteParamsJSFunction(JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto* zig = reinterpret_cast<Zig::GlobalObject*>(globalObject);
|
||||
auto value = zig->bakeAdditions().getNewRouteParamsJSFunction(zig);
|
||||
auto value = zig->bakeAdditions().getDevNewRouteParamsJSFunction(zig);
|
||||
return JSValue::encode(value);
|
||||
}
|
||||
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue Bake__getProdNewRouteParamsJSFunction(JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto* zig = reinterpret_cast<Zig::GlobalObject*>(globalObject);
|
||||
auto value = zig->bakeAdditions().getProdNewRouteParamsJSFunction(zig);
|
||||
return JSValue::encode(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "headers-handwritten.h"
|
||||
#include "BunBuiltinNames.h"
|
||||
#include "WebCoreJSBuiltins.h"
|
||||
#include "BakeProductionSSRRouteList.h"
|
||||
#include "headers-handwritten.h"
|
||||
|
||||
namespace Bun {
|
||||
@@ -20,25 +21,29 @@ BUN_DECLARE_HOST_FUNCTION(jsFunctionBakeGetBundleNewRouteJSFunction);
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue Bake__getEnsureAsyncLocalStorageInstanceJSFunction(JSC::JSGlobalObject* globalObject);
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue Bake__getAsyncLocalStorage(JSC::JSGlobalObject* globalObject);
|
||||
|
||||
extern "C" SYSV_ABI EncodedJSValue Bake__createDevServerFrameworkRequestArgsObject(JSC::JSGlobalObject* globalObject, EncodedJSValue routerTypeMain, EncodedJSValue routeModules, EncodedJSValue clientEntryUrl, EncodedJSValue styles, EncodedJSValue params);
|
||||
extern "C" SYSV_ABI EncodedJSValue Bake__createFrameworkRequestArgsObject(JSC::JSGlobalObject* globalObject, EncodedJSValue routerTypeMain, EncodedJSValue routeModules, EncodedJSValue clientEntryUrl, EncodedJSValue styles, EncodedJSValue params);
|
||||
|
||||
void createDevServerFrameworkRequestArgsStructure(JSC::LazyClassStructure::Initializer& init);
|
||||
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES Bake__getNewRouteParamsJSFunctionImpl(JSC::JSGlobalObject*, JSC::CallFrame*);
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES Bake__getDevNewRouteParamsJSFunctionImpl(JSC::JSGlobalObject*, JSC::CallFrame*);
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES Bake__getProdNewRouteParamsJSFunctionImpl(JSC::JSGlobalObject*, JSC::CallFrame*);
|
||||
|
||||
struct BakeAdditionsToGlobalObject {
|
||||
template<typename Visitor>
|
||||
void visit(Visitor& visitor)
|
||||
{
|
||||
this->m_JSBakeResponseClassStructure.visit(visitor);
|
||||
this->m_DevServerFrameworkRequestArgsClassStructure.visit(visitor);
|
||||
this->m_FrameworkRequestArgsClassStructure.visit(visitor);
|
||||
this->m_BakeProductionSSRRouteInfoClassStructure.visit(visitor);
|
||||
this->m_BakeProductionSSRRouteArgsClassStructure.visit(visitor);
|
||||
visitor.append(this->m_wrapComponent);
|
||||
visitor.append(this->m_asyncLocalStorageInstance);
|
||||
|
||||
this->m_bakeGetAsyncLocalStorage.visit(visitor);
|
||||
this->m_bakeEnsureAsyncLocalStorage.visit(visitor);
|
||||
this->m_bakeGetBundleNewRoute.visit(visitor);
|
||||
this->m_bakeGetNewRouteParams.visit(visitor);
|
||||
this->m_bakeProdGetNewRouteParamsJSFunction.visit(visitor);
|
||||
this->m_bakeGetDevNewRouteParamsJSFunction.visit(visitor);
|
||||
}
|
||||
|
||||
void initialize()
|
||||
@@ -58,30 +63,51 @@ struct BakeAdditionsToGlobalObject {
|
||||
init.set(JSFunction::create(init.vm, init.owner, 1, String("bakeSetAsyncLocalStorage"_s), jsFunctionBakeEnsureAsyncLocalStorage, ImplementationVisibility::Public, NoIntrinsic));
|
||||
});
|
||||
|
||||
m_BakeProductionSSRRouteInfoClassStructure.initLater(
|
||||
[](LazyClassStructure::Initializer& init) {
|
||||
Bun::createBakeProductionSSRRouteInfoStructure(init);
|
||||
});
|
||||
|
||||
m_BakeProductionSSRRouteArgsClassStructure.initLater(
|
||||
[](LazyClassStructure::Initializer& init) {
|
||||
Bun::createBakeProductionSSRRouteArgsStructure(init);
|
||||
});
|
||||
|
||||
m_bakeGetBundleNewRoute.initLater(
|
||||
[](const LazyProperty<JSGlobalObject, JSFunction>::Initializer& init) {
|
||||
init.set(JSFunction::create(init.vm, init.owner, 1, String("bundleNewRoute"_s), jsFunctionBakeGetBundleNewRouteJSFunction, ImplementationVisibility::Public, NoIntrinsic));
|
||||
});
|
||||
|
||||
m_bakeGetNewRouteParams.initLater(
|
||||
m_bakeGetDevNewRouteParamsJSFunction.initLater(
|
||||
[](const LazyProperty<JSGlobalObject, JSFunction>::Initializer& init) {
|
||||
init.set(JSFunction::create(init.vm, init.owner, 1, String("newRouteParams"_s), Bake__getNewRouteParamsJSFunctionImpl, ImplementationVisibility::Public, NoIntrinsic));
|
||||
init.set(JSFunction::create(init.vm, init.owner, 1, String("newRouteParams"_s), Bake__getDevNewRouteParamsJSFunctionImpl, ImplementationVisibility::Public, NoIntrinsic));
|
||||
});
|
||||
|
||||
m_DevServerFrameworkRequestArgsClassStructure.initLater(
|
||||
m_bakeProdGetNewRouteParamsJSFunction.initLater(
|
||||
[](const LazyProperty<JSGlobalObject, JSFunction>::Initializer& init) {
|
||||
init.set(JSFunction::create(init.vm, init.owner, 1, String("newRouteParams"_s), Bake__getProdNewRouteParamsJSFunctionImpl, ImplementationVisibility::Public, NoIntrinsic));
|
||||
});
|
||||
|
||||
m_FrameworkRequestArgsClassStructure.initLater(
|
||||
[](LazyClassStructure::Initializer& init) {
|
||||
Bun::createDevServerFrameworkRequestArgsStructure(init);
|
||||
});
|
||||
}
|
||||
|
||||
JSValue getBundleNewRouteJSFunction(JSGlobalObject* globalObject)
|
||||
JSValue
|
||||
getBundleNewRouteJSFunction(JSGlobalObject* globalObject)
|
||||
{
|
||||
return m_bakeGetBundleNewRoute.get(globalObject);
|
||||
}
|
||||
|
||||
JSValue getNewRouteParamsJSFunction(JSGlobalObject* globalObject)
|
||||
JSValue getDevNewRouteParamsJSFunction(JSGlobalObject* globalObject)
|
||||
{
|
||||
return m_bakeGetNewRouteParams.get(globalObject);
|
||||
return m_bakeGetDevNewRouteParamsJSFunction.get(globalObject);
|
||||
}
|
||||
|
||||
JSValue getProdNewRouteParamsJSFunction(JSGlobalObject* globalObject)
|
||||
{
|
||||
return m_bakeProdGetNewRouteParamsJSFunction.get(globalObject);
|
||||
}
|
||||
|
||||
void ensureAsyncLocalStorageInstance(JSGlobalObject* globalObject, JSValue asyncLocalStorage)
|
||||
@@ -129,7 +155,9 @@ struct BakeAdditionsToGlobalObject {
|
||||
}
|
||||
|
||||
LazyClassStructure m_JSBakeResponseClassStructure;
|
||||
LazyClassStructure m_DevServerFrameworkRequestArgsClassStructure;
|
||||
LazyClassStructure m_BakeProductionSSRRouteInfoClassStructure;
|
||||
LazyClassStructure m_BakeProductionSSRRouteArgsClassStructure;
|
||||
LazyClassStructure m_FrameworkRequestArgsClassStructure;
|
||||
|
||||
private:
|
||||
WriteBarrier<JSFunction> m_wrapComponent;
|
||||
@@ -138,7 +166,8 @@ private:
|
||||
LazyProperty<JSGlobalObject, JSFunction> m_bakeGetAsyncLocalStorage;
|
||||
LazyProperty<JSGlobalObject, JSFunction> m_bakeEnsureAsyncLocalStorage;
|
||||
LazyProperty<JSGlobalObject, JSFunction> m_bakeGetBundleNewRoute;
|
||||
LazyProperty<JSGlobalObject, JSFunction> m_bakeGetNewRouteParams;
|
||||
LazyProperty<JSGlobalObject, JSFunction> m_bakeProdGetNewRouteParamsJSFunction;
|
||||
LazyProperty<JSGlobalObject, JSFunction> m_bakeGetDevNewRouteParamsJSFunction;
|
||||
};
|
||||
|
||||
} // namespace Bun
|
||||
|
||||
303
src/bun.js/bindings/BakeProductionSSRRouteList.cpp
Normal file
303
src/bun.js/bindings/BakeProductionSSRRouteList.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
#include "root.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include "JSBunRequest.h"
|
||||
|
||||
namespace Bun {
|
||||
using namespace JSC;
|
||||
using namespace WebCore;
|
||||
|
||||
extern "C" int Bun__BakeProductionSSRRouteInfo__dataForInitialization(JSGlobalObject* globalObject, void* zigRequestPtr, size_t routerIndex, size_t routerTypeIndex, JSC::EncodedJSValue* routerTypeMain, JSC::EncodedJSValue* routeModules, JSC::EncodedJSValue* clientEntryUrl, JSC::EncodedJSValue* styles);
|
||||
|
||||
void createBakeProductionSSRRouteArgsStructure(JSC::LazyClassStructure::Initializer& init)
|
||||
{
|
||||
auto structure = JSC::Structure::create(init.vm, init.global, init.global->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, 0), JSFinalObject::info(), NonArray, 4);
|
||||
|
||||
PropertyOffset offset = 0;
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "serverEntrypoint"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "routeModules"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "styles"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "clientEntryUrl"_s), 0, offset);
|
||||
|
||||
init.setPrototype(init.global->objectPrototype());
|
||||
init.setStructure(structure);
|
||||
}
|
||||
|
||||
// Called by the production server runtime in JS to get the data to initialize the arguments for a route to render it
|
||||
JSC_DEFINE_HOST_FUNCTION(jsBakeProductionSSRRouteInfoPrototypeFunction_dataForInitialization, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
|
||||
if (callframe->argumentCount() < 3) {
|
||||
throwTypeError(globalObject, scope, "Expected 3 argument"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSValue requestObject = callframe->argument(0);
|
||||
JSValue routerIndex = callframe->argument(1);
|
||||
JSValue routerTypeIndex = callframe->argument(2);
|
||||
|
||||
if (requestObject.isEmpty() || requestObject.isUndefinedOrNull() || !requestObject.isCell()) {
|
||||
throwTypeError(globalObject, scope, "Expected first argument to be a non-empty object"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!routerIndex.isInt32()) {
|
||||
throwTypeError(globalObject, scope, "Expected second argument to be a number"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!routerTypeIndex.isInt32()) {
|
||||
throwTypeError(globalObject, scope, "Expected third argument to be a number"_s);
|
||||
return {};
|
||||
}
|
||||
|
||||
JSBunRequest* request = jsCast<JSBunRequest*>(requestObject);
|
||||
size_t routerIndexValue = static_cast<size_t>(routerIndex.asInt32());
|
||||
size_t routerTypeIndexValue = static_cast<size_t>(routerTypeIndex.asInt32());
|
||||
|
||||
// What we need:
|
||||
// 1. `routerTypeMain: string` (module specifier for serverEntrypoint)
|
||||
// 2. `routeModules: string[]` (module specifiers for `[pageModule, ...layoutModules]`)
|
||||
// 3. `styles: string[]` (CSS URLs to be given to react to render)
|
||||
// 4. `clientEntryUrl: string` (client script to be given to react to render)
|
||||
|
||||
EncodedJSValue routerTypeMain;
|
||||
EncodedJSValue routeModules;
|
||||
EncodedJSValue clientEntryUrl;
|
||||
EncodedJSValue styles;
|
||||
|
||||
int success = Bun__BakeProductionSSRRouteInfo__dataForInitialization(globalObject, request->m_ctx, routerIndexValue, routerTypeIndexValue, &routerTypeMain, &routeModules, &clientEntryUrl, &styles);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
if (success == 0) {
|
||||
return JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
auto zig = reinterpret_cast<Zig::GlobalObject*>(globalObject);
|
||||
auto* structure = zig->bakeAdditions().m_BakeProductionSSRRouteArgsClassStructure.get(globalObject);
|
||||
auto* instance = constructEmptyObject(globalObject->vm(), structure);
|
||||
|
||||
instance->putDirectOffset(globalObject->vm(), 0, JSValue::decode(routerTypeMain));
|
||||
instance->putDirectOffset(globalObject->vm(), 1, JSValue::decode(routeModules));
|
||||
instance->putDirectOffset(globalObject->vm(), 2, JSValue::decode(styles));
|
||||
instance->putDirectOffset(globalObject->vm(), 3, JSValue::decode(clientEntryUrl));
|
||||
|
||||
return JSValue::encode(instance);
|
||||
}
|
||||
|
||||
static const HashTableValue BakeProductionSSRRouteInfoPrototypeValues[] = {
|
||||
{ "dataForInitialization"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBakeProductionSSRRouteInfoPrototypeFunction_dataForInitialization, 0 } },
|
||||
};
|
||||
|
||||
class BakeProductionSSRRouteInfoPrototype final : public JSC::JSNonFinalObject {
|
||||
public:
|
||||
using Base = JSC::JSNonFinalObject;
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
static BakeProductionSSRRouteInfoPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
|
||||
{
|
||||
auto* prototype = new (NotNull, JSC::allocateCell<BakeProductionSSRRouteInfoPrototype>(vm)) BakeProductionSSRRouteInfoPrototype(vm, structure);
|
||||
prototype->finishCreation(vm, globalObject);
|
||||
return prototype;
|
||||
}
|
||||
|
||||
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
{
|
||||
|
||||
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
structure->setMayBePrototype(true);
|
||||
return structure;
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
DECLARE_VISIT_CHILDREN;
|
||||
|
||||
template<typename, JSC::SubspaceAccess mode>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
|
||||
return nullptr;
|
||||
return WebCore::subspaceForImpl<BakeProductionSSRRouteInfoPrototype, WebCore::UseCustomHeapCellType::No>(
|
||||
vm,
|
||||
[](auto& spaces) { return spaces.m_clientSubspaceForBakeProductionSSRRouteInfoPrototype.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForBakeProductionSSRRouteInfoPrototype = std::forward<decltype(space)>(space); },
|
||||
[](auto& spaces) { return spaces.m_subspaceForBakeProductionSSRRouteInfoPrototype.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_subspaceForBakeProductionSSRRouteInfoPrototype = std::forward<decltype(space)>(space); });
|
||||
}
|
||||
|
||||
private:
|
||||
BakeProductionSSRRouteInfoPrototype(JSC::VM& vm, JSC::Structure* structure)
|
||||
: Base(vm, structure)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
ASSERT(inherits(info()));
|
||||
|
||||
reifyStaticProperties(vm, this->classInfo(), BakeProductionSSRRouteInfoPrototypeValues, *this);
|
||||
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Visitor>
|
||||
void BakeProductionSSRRouteInfoPrototype::visitChildrenImpl(JSCell* cell, Visitor& visitor)
|
||||
{
|
||||
BakeProductionSSRRouteInfoPrototype* thisCallSite = jsCast<BakeProductionSSRRouteInfoPrototype*>(cell);
|
||||
Base::visitChildren(thisCallSite, visitor);
|
||||
}
|
||||
DEFINE_VISIT_CHILDREN(BakeProductionSSRRouteInfoPrototype);
|
||||
|
||||
const JSC::ClassInfo BakeProductionSSRRouteInfoPrototype::s_info = { "BakeProductionSSRRouteInfo"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(BakeProductionSSRRouteInfoPrototype) };
|
||||
|
||||
void createBakeProductionSSRRouteInfoStructure(JSC::LazyClassStructure::Initializer& init)
|
||||
{
|
||||
auto* prototype = BakeProductionSSRRouteInfoPrototype::create(init.vm, init.global, BakeProductionSSRRouteInfoPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
|
||||
auto structure = JSC::Structure::create(init.vm, init.global, prototype, JSC::TypeInfo(JSC::ObjectType, 0), JSFinalObject::info(), NonArray, 5);
|
||||
|
||||
PropertyOffset offset = 0;
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "serverEntrypoint"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "routeModules"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "styles"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "clientEntryUrl"_s), 0, offset);
|
||||
structure = structure->addPropertyTransition(init.vm, structure, JSC::Identifier::fromString(init.vm, "initializing"_s), 0, offset);
|
||||
|
||||
init.setPrototype(prototype);
|
||||
init.setStructure(structure);
|
||||
}
|
||||
|
||||
JSFinalObject* createEmptyRouteInfoObject(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
auto zigGlobalObject = defaultGlobalObject(globalObject);
|
||||
auto* structure = zigGlobalObject->bakeAdditions().m_BakeProductionSSRRouteInfoClassStructure.get(zigGlobalObject);
|
||||
return constructEmptyObject(vm, structure);
|
||||
}
|
||||
|
||||
class BakeProductionSSRRouteList final : public JSC::JSDestructibleObject {
|
||||
private:
|
||||
WTF::FixedVector<WriteBarrier<JSC::JSFinalObject>> m_routeInfos;
|
||||
// Two things to note:
|
||||
// 1. JSC imposes an upper bound of 64 properties
|
||||
// 2. We can't mix and match keys and indices (user can't make a route param that is named as a number)
|
||||
WTF::FixedVector<WriteBarrier<Structure>> m_paramsObjectStructures;
|
||||
|
||||
public:
|
||||
using Base = JSC::JSDestructibleObject;
|
||||
|
||||
BakeProductionSSRRouteList(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, size_t routeCount)
|
||||
: Base(vm, structure)
|
||||
, m_routeInfos(routeCount)
|
||||
, m_paramsObjectStructures(routeCount)
|
||||
{
|
||||
}
|
||||
|
||||
static BakeProductionSSRRouteList* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, size_t routeCount)
|
||||
{
|
||||
// FIXME: let's not create this everytime
|
||||
auto* structure = JSC::Structure::create(vm, globalObject, globalObject->nullPrototype(), JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
|
||||
auto* routeList = new (NotNull, JSC::allocateCell<BakeProductionSSRRouteList>(vm)) BakeProductionSSRRouteList(vm, globalObject, structure, routeCount);
|
||||
routeList->finishCreation(vm, globalObject);
|
||||
return routeList;
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
|
||||
{
|
||||
Base::finishCreation(vm);
|
||||
for (size_t i = 0; i < m_routeInfos.size(); i++) {
|
||||
auto* routeInfo = createEmptyRouteInfoObject(vm, globalObject);
|
||||
routeInfo->putDirectOffset(vm, 0, jsUndefined());
|
||||
routeInfo->putDirectOffset(vm, 1, jsUndefined());
|
||||
routeInfo->putDirectOffset(vm, 2, jsUndefined());
|
||||
routeInfo->putDirectOffset(vm, 3, jsUndefined());
|
||||
routeInfo->putDirectOffset(vm, 4, jsUndefined());
|
||||
|
||||
m_routeInfos[i].setMayBeNull(vm, this, routeInfo);
|
||||
m_paramsObjectStructures[i].setMayBeNull(vm, this, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
JSFinalObject* routeInfo(size_t index) const
|
||||
{
|
||||
return m_routeInfos[index].get();
|
||||
}
|
||||
|
||||
Structure* routeParamsStructure(size_t index) const
|
||||
{
|
||||
return m_paramsObjectStructures[index].get();
|
||||
}
|
||||
|
||||
Structure* createRouteParamsStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, uint32_t index, std::span<BunString> identifiers)
|
||||
{
|
||||
auto structure = JSC::Structure::create(vm, globalObject, globalObject->objectPrototype(), JSC::TypeInfo(JSC::ObjectType, 0), JSFinalObject::info(), NonArray, identifiers.size());
|
||||
PropertyOffset offset = 0;
|
||||
for (const auto& identifier : identifiers) {
|
||||
structure = structure->addPropertyTransition(vm, structure, JSC::Identifier::fromString(vm, identifier.toWTFString()), 0, offset);
|
||||
}
|
||||
this->m_paramsObjectStructures[index].set(vm, this, structure);
|
||||
return structure;
|
||||
}
|
||||
|
||||
DECLARE_INFO;
|
||||
DECLARE_VISIT_CHILDREN;
|
||||
|
||||
template<typename, JSC::SubspaceAccess mode>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
|
||||
return nullptr;
|
||||
return WebCore::subspaceForImpl<BakeProductionSSRRouteList, WebCore::UseCustomHeapCellType::No>(
|
||||
vm,
|
||||
[](auto& spaces) { return spaces.m_clientSubspaceForBakeProductionSSRRouteList.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForBakeProductionSSRRouteList = std::forward<decltype(space)>(space); },
|
||||
[](auto& spaces) { return spaces.m_subspaceForBakeProductionSSRRouteList.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_subspaceForBakeProductionSSRRouteList = std::forward<decltype(space)>(space); });
|
||||
}
|
||||
};
|
||||
|
||||
const JSC::ClassInfo BakeProductionSSRRouteList::s_info = { "BakeProductionSSRRouteList"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(BakeProductionSSRRouteList) };
|
||||
|
||||
template<typename Visitor>
|
||||
void BakeProductionSSRRouteList::visitChildrenImpl(JSCell* cell, Visitor& visitor)
|
||||
{
|
||||
BakeProductionSSRRouteList* thisCallSite = jsCast<BakeProductionSSRRouteList*>(cell);
|
||||
Base::visitChildren(thisCallSite, visitor);
|
||||
|
||||
for (unsigned i = 0; i < thisCallSite->m_routeInfos.size(); i++) {
|
||||
if (thisCallSite->m_routeInfos[i]) visitor.append(thisCallSite->m_routeInfos[i]);
|
||||
if (thisCallSite->m_paramsObjectStructures[i]) visitor.append(thisCallSite->m_paramsObjectStructures[i]);
|
||||
}
|
||||
}
|
||||
DEFINE_VISIT_CHILDREN(BakeProductionSSRRouteList);
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__BakeProductionSSRRouteList__create(Zig::GlobalObject* globalObject, size_t routeCount)
|
||||
{
|
||||
auto* routeList = BakeProductionSSRRouteList::create(globalObject->vm(), globalObject, routeCount);
|
||||
return JSValue::encode(routeList);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__BakeProductionSSRRouteList__createRouteParamsStructure(Zig::GlobalObject* globalObject, EncodedJSValue routeListObject, size_t index, BunString* paramsInfo, size_t paramsCount)
|
||||
{
|
||||
BakeProductionSSRRouteList* routeList = jsCast<BakeProductionSSRRouteList*>(JSValue::decode(routeListObject));
|
||||
std::span<BunString> paramsInfoSpan(paramsInfo, paramsCount);
|
||||
auto* structure = routeList->createRouteParamsStructure(globalObject->vm(), globalObject, index, paramsInfoSpan);
|
||||
return JSValue::encode(structure);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__BakeProductionSSRRouteList__getRouteParamsStructure(Zig::GlobalObject* globalObject, EncodedJSValue routeListObject, size_t index)
|
||||
{
|
||||
BakeProductionSSRRouteList* routeList = jsCast<BakeProductionSSRRouteList*>(JSValue::decode(routeListObject));
|
||||
auto* structure = routeList->routeParamsStructure(index);
|
||||
if (!structure) {
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
return JSValue::encode(structure);
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__BakeProductionSSRRouteList__getRouteInfo(Zig::GlobalObject* globalObject, EncodedJSValue routeListObject, size_t index)
|
||||
{
|
||||
JSValue routeListValue = JSValue::decode(routeListObject);
|
||||
BakeProductionSSRRouteList* routeList = jsCast<BakeProductionSSRRouteList*>(routeListValue);
|
||||
JSValue routeInfo = routeList->routeInfo(index);
|
||||
return JSValue::encode(routeInfo);
|
||||
}
|
||||
}
|
||||
5
src/bun.js/bindings/BakeProductionSSRRouteList.h
Normal file
5
src/bun.js/bindings/BakeProductionSSRRouteList.h
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
namespace Bun {
|
||||
void createBakeProductionSSRRouteInfoStructure(JSC::LazyClassStructure::Initializer& init);
|
||||
void createBakeProductionSSRRouteArgsStructure(JSC::LazyClassStructure::Initializer& init);
|
||||
}
|
||||
@@ -704,6 +704,7 @@ void ImportMetaObject::finishCreation(VM& vm)
|
||||
} else {
|
||||
path = meta->url;
|
||||
}
|
||||
printf("require path: %s\n", path.utf8().data());
|
||||
|
||||
auto* object = Bun::JSCommonJSModule::createBoundRequireFunction(init.vm, meta->globalObject(), path);
|
||||
RETURN_IF_EXCEPTION(scope, );
|
||||
@@ -731,6 +732,7 @@ void ImportMetaObject::finishCreation(VM& vm)
|
||||
} else if (dirname.contains(PLATFORM_SEP)) {
|
||||
dirname = dirname.substring(0, dirname.reverseFind(PLATFORM_SEP));
|
||||
}
|
||||
printf("dirname: %s\n", dirname.utf8().data());
|
||||
|
||||
init.set(jsString(init.vm, dirname));
|
||||
});
|
||||
|
||||
@@ -120,6 +120,12 @@ pub const JSObject = opaque {
|
||||
return JSC__createStructure(global, owner.asCell(), length, names);
|
||||
}
|
||||
|
||||
extern "C" fn JSC__JSObject__createWithStructure(global: *JSGlobalObject, structure: JSValue) callconv(jsc.conv) JSValue;
|
||||
|
||||
pub fn createWithStructure(global: *JSGlobalObject, structure: JSValue) JSError!JSValue {
|
||||
return jsc.fromJSHostCall(global, @src(), JSC__JSObject__createWithStructure, .{ global, structure });
|
||||
}
|
||||
|
||||
const InitializeCallback = *const fn (ctx: *anyopaque, obj: *JSObject, global: *JSGlobalObject) callconv(.C) void;
|
||||
|
||||
pub fn Initializer(comptime Ctx: type, comptime func: fn (*Ctx, obj: *JSObject, global: *JSGlobalObject) bun.JSError!void) type {
|
||||
|
||||
@@ -381,6 +381,12 @@ pub const JSValue = enum(i64) {
|
||||
return bun.jsc.fromJSHostCallGeneric(globalObject, @src(), JSC__JSValue__putIndex, .{ value, globalObject, i, out });
|
||||
}
|
||||
|
||||
extern fn JSC__JSValue__putDirectOffset(value: JSValue, globalObject: *JSGlobalObject, i: u32, val: JSValue) void;
|
||||
/// Only use this function if you know the JSC::Structure of `target` supports this
|
||||
pub fn putDirectOffset(target: JSValue, globalObject: *JSGlobalObject, i: u32, val: JSValue) bun.JSError!void {
|
||||
return bun.jsc.fromJSHostCallGeneric(globalObject, @src(), JSC__JSValue__putDirectOffset, .{ target, globalObject, i, val });
|
||||
}
|
||||
|
||||
extern fn JSC__JSValue__push(value: JSValue, globalObject: *JSGlobalObject, out: JSValue) void;
|
||||
pub fn push(value: JSValue, globalObject: *JSGlobalObject, out: JSValue) bun.JSError!void {
|
||||
return bun.jsc.fromJSHostCallGeneric(globalObject, @src(), JSC__JSValue__push, .{ value, globalObject, out });
|
||||
|
||||
@@ -2021,6 +2021,7 @@ WebCore::FetchHeaders* WebCore__FetchHeaders__createValueNotJS(JSC::JSGlobalObje
|
||||
headers->deref();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -2298,6 +2299,13 @@ JSC::EncodedJSValue SystemError__toErrorInstanceWithInfoObject(const SystemError
|
||||
return JSC::JSValue::encode(result);
|
||||
}
|
||||
|
||||
extern "C" SYSV_ABI JSC::EncodedJSValue JSC__JSObject__createWithStructure(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedStructureValue)
|
||||
{
|
||||
JSC::JSValue structureValue = JSC::JSValue::decode(encodedStructureValue);
|
||||
JSC::Structure* structure = JSC::jsCast<JSC::Structure*>(structureValue);
|
||||
return JSC::JSValue::encode(JSC::constructEmptyObject(globalObject->vm(), structure));
|
||||
}
|
||||
|
||||
JSC::EncodedJSValue
|
||||
JSC__JSObject__create(JSC::JSGlobalObject* globalObject, size_t initialCapacity, void* arg2,
|
||||
void (*ArgFn3)(void* arg0, JSC::JSObject* arg1, JSC::JSGlobalObject* arg2))
|
||||
@@ -3115,6 +3123,13 @@ CPP_DECL void JSC__JSValue__putIndex(JSC::EncodedJSValue JSValue0, JSC::JSGlobal
|
||||
JSC::JSArray* array = JSC::jsCast<JSC::JSArray*>(value);
|
||||
array->putDirectIndex(arg1, arg2, value2);
|
||||
}
|
||||
CPP_DECL void JSC__JSValue__putDirectOffset(JSC::EncodedJSValue targetValue, JSC::JSGlobalObject* global, uint32_t offset, JSC::EncodedJSValue value)
|
||||
{
|
||||
JSC::JSValue target = JSC::JSValue::decode(targetValue);
|
||||
JSC::JSValue val = JSC::JSValue::decode(value);
|
||||
JSC::JSObject* targetObject = JSC::jsCast<JSC::JSObject*>(target);
|
||||
targetObject->putDirectOffset(global->vm(), offset, val);
|
||||
}
|
||||
|
||||
CPP_DECL void JSC__JSValue__push(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue3)
|
||||
{
|
||||
|
||||
@@ -948,6 +948,8 @@ public:
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForServerRouteList;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBunRequest;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBakeResponse;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBakeProductionSSRRouteList;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBakeProductionSSRRouteInfoPrototype;
|
||||
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSConnectionsList;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSHTTPParser;
|
||||
|
||||
@@ -943,6 +943,8 @@ public:
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForServerRouteList;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForBunRequest;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForBakeResponse;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForBakeProductionSSRRouteList;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForBakeProductionSSRRouteInfoPrototype;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSDiffieHellman;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSDiffieHellmanGroup;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSECDH;
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace Zig {
|
||||
using namespace WebCore;
|
||||
using namespace JSC;
|
||||
|
||||
// External function to get SSRResponse constructor
|
||||
extern "C" JSC::EncodedJSValue Bake__getSSRResponseConstructor(JSC::JSGlobalObject* globalObject);
|
||||
|
||||
DEFINE_NATIVE_MODULE(BunApp)
|
||||
{
|
||||
INIT_NATIVE_MODULE(1);
|
||||
|
||||
@@ -319,7 +319,7 @@ pub fn load(
|
||||
// For client components, the import record index currently points to the original source index, instead of the reference source index.
|
||||
for (this.reachable_files) |source_id| {
|
||||
for (import_records_list[source_id.get()].slice()) |*import_record| {
|
||||
if (import_record.source_index.isValid() and this.is_scb_bitset.isSet(import_record.source_index.get())) {
|
||||
if (import_record.source_index.isValid() and this.is_scb_bitset.isSet(import_record.source_index.get()) and this.ast.items(.target)[source_id.get()] != .browser) {
|
||||
// Only rewrite if this is an original SCB file, not a reference file
|
||||
if (scb.getReferenceSourceIndex(import_record.source_index.get())) |ref_index| {
|
||||
import_record.source_index = Index.init(ref_index);
|
||||
|
||||
@@ -3850,14 +3850,17 @@ pub const BundleV2 = struct {
|
||||
.browser,
|
||||
) catch |err| bun.handleOom(err);
|
||||
|
||||
break :brk .{ server_index, Index.invalid.get() };
|
||||
// break :brk .{ server_index, Index.invalid.get() };
|
||||
break :brk .{ Index.invalid.get(), server_index };
|
||||
};
|
||||
|
||||
// if (result.ast.target != .browser) {
|
||||
graph.pathToSourceIndexMap(result.ast.target).put(
|
||||
this.allocator(),
|
||||
result.source.path.text,
|
||||
reference_source_index,
|
||||
) catch |err| bun.handleOom(err);
|
||||
// }
|
||||
|
||||
graph.server_component_boundaries.put(
|
||||
this.allocator(),
|
||||
|
||||
@@ -227,11 +227,13 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun
|
||||
// Ignore uses that aren't top-level symbols
|
||||
if (symbol.chunkIndex()) |other_chunk_index| {
|
||||
if (@as(usize, other_chunk_index) != chunk_index) {
|
||||
if (comptime Environment.allow_assert)
|
||||
if (comptime Environment.allow_assert) {
|
||||
const file = c.parse_graph.input_files.get(import_ref.sourceIndex()).source.path.text;
|
||||
debug("Import name: {s} (in {s})", .{
|
||||
symbol.original_name,
|
||||
c.parse_graph.input_files.get(import_ref.sourceIndex()).source.path.text,
|
||||
file,
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
var entry = try js
|
||||
|
||||
@@ -91,6 +91,7 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu
|
||||
c,
|
||||
toCommonJSRef,
|
||||
toESMRef,
|
||||
chunk,
|
||||
chunk.entry_point.source_index,
|
||||
worker.allocator,
|
||||
arena.allocator(),
|
||||
@@ -439,6 +440,7 @@ pub fn generateEntryPointTailJS(
|
||||
c: *LinkerContext,
|
||||
toCommonJSRef: Ref,
|
||||
toESMRef: Ref,
|
||||
chunk: *Chunk,
|
||||
source_index: Index.Int,
|
||||
allocator: std.mem.Allocator,
|
||||
temp_allocator: std.mem.Allocator,
|
||||
@@ -641,6 +643,10 @@ pub fn generateEntryPointTailJS(
|
||||
},
|
||||
) catch unreachable;
|
||||
} else {
|
||||
if (chunk.content.javascript.exports_to_other_chunks.get(resolved_export.data.import_ref) != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Local identifiers can be exported using an export clause. This is done
|
||||
// this way instead of leaving the "export" keyword on the local declaration
|
||||
// itself both because it lets the local identifier be minified and because
|
||||
|
||||
@@ -205,6 +205,46 @@ async function run() {
|
||||
const empty_file = join(codegenRoot, "bake_empty_file");
|
||||
if (!existsSync(empty_file)) writeIfNotChanged(empty_file, "this is used to fulfill a cmake dependency");
|
||||
}
|
||||
|
||||
// Build production-runtime-server.ts as an IIFE
|
||||
try {
|
||||
let result = await Bun.build({
|
||||
entrypoints: [join(base_dir, "production-runtime-server.ts")],
|
||||
target: "bun",
|
||||
minify: !debug,
|
||||
drop: debug ? [] : ["DEBUG"],
|
||||
});
|
||||
|
||||
if (!result.success) throw new AggregateError(result.logs);
|
||||
assert(result.outputs.length === 1, "must bundle to a single file");
|
||||
|
||||
let code = await result.outputs[0].text();
|
||||
|
||||
// Remove any export statements
|
||||
code = code.replace(/^export\s+/gm, "");
|
||||
|
||||
// Find where server_exports is defined and ensure we return it
|
||||
if (!code.includes("server_exports")) {
|
||||
throw new Error("production-runtime-server.ts must define server_exports");
|
||||
}
|
||||
|
||||
// Replace import.meta with $importMeta parameter
|
||||
code = code.replaceAll("import.meta", "$importMeta");
|
||||
|
||||
// Wrap in IIFE with $importMeta parameter and return server_exports
|
||||
if (debug) {
|
||||
code = `(($importMeta) => {\n ${code.replace(/\n/g, "\n ")}\n return server_exports;\n})`;
|
||||
} else {
|
||||
code = `(($importMeta)=>{${code};return server_exports})`;
|
||||
}
|
||||
|
||||
writeIfNotChanged(join(codegenRoot, "bake.production-server.js"), code);
|
||||
console.log("-> bake.production-server.js");
|
||||
} catch (err) {
|
||||
console.error("Error while bundling production-runtime-server.ts:");
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await run();
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
//! JS code for bake
|
||||
/// <reference path="../../bake/bake.d.ts" />
|
||||
import type { Bake } from "bun";
|
||||
|
||||
type FrameworkPrerender = Bake.ServerEntryPoint["prerender"];
|
||||
type FrameworkGetParams = Bake.ServerEntryPoint["getParams"];
|
||||
type TypeAndFlags = number;
|
||||
type FileIndex = number;
|
||||
|
||||
/**
|
||||
* This layer is implemented in JavaScript to reduce Native <-> JS context switches,
|
||||
* as well as use the async primitives provided by the language.
|
||||
*/
|
||||
export async function renderRoutesForProdStatic(
|
||||
outBase: string,
|
||||
allServerFiles: string[],
|
||||
outBase,
|
||||
allServerFiles,
|
||||
// Indexed by router type index
|
||||
renderStatic: FrameworkPrerender[],
|
||||
getParams: FrameworkGetParams[],
|
||||
clientEntryUrl: string[],
|
||||
renderStatic,
|
||||
getParams,
|
||||
clientEntryUrl,
|
||||
routerTypeRoots,
|
||||
routerTypeServerEntrypoints,
|
||||
serverRuntime,
|
||||
// Indexed by route index
|
||||
patterns: string[],
|
||||
files: FileIndex[][],
|
||||
typeAndFlags: TypeAndFlags[],
|
||||
sourceRouteFiles: string[],
|
||||
paramInformation: Array<null | string[]>,
|
||||
styles: string[][],
|
||||
): Promise<void> {
|
||||
patterns,
|
||||
files,
|
||||
typeAndFlags,
|
||||
sourceRouteFiles,
|
||||
paramInformation,
|
||||
styles,
|
||||
routeIndices,
|
||||
) {
|
||||
console.log("ROUTER TYPE ROOTS!", routerTypeRoots);
|
||||
|
||||
$debug({
|
||||
outBase,
|
||||
allServerFiles,
|
||||
@@ -40,16 +40,45 @@ export async function renderRoutesForProdStatic(
|
||||
});
|
||||
const { join: pathJoin } = require("node:path");
|
||||
|
||||
// Helper function to make paths relative to _bun folder (removes _bun/ prefix and adds ./)
|
||||
function makeRelativeToBun(path: string): string {
|
||||
if (path.startsWith("/_bun/")) {
|
||||
return "./" + path.slice(6); // Remove "/_bun/" and add ./ prefix
|
||||
} else if (path.startsWith("_bun/")) {
|
||||
return "./" + path.slice(5); // Remove "_bun/" and add ./ prefix
|
||||
}
|
||||
return "./" + path;
|
||||
}
|
||||
|
||||
// Helper function to process route paths: removes router type prefix and extension, ensures leading slash
|
||||
function processRoutePath(sourceRoute: string, routerRoot: string | undefined): string {
|
||||
let routePath = sourceRoute;
|
||||
|
||||
// Remove router type prefix if present
|
||||
if (routerRoot && routePath.startsWith(routerRoot)) {
|
||||
routePath = routePath.slice(routerRoot.length);
|
||||
if (routePath.startsWith("/")) {
|
||||
routePath = routePath.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove extension
|
||||
const lastDot = routePath.lastIndexOf(".");
|
||||
if (lastDot > 0) {
|
||||
routePath = routePath.slice(0, lastDot);
|
||||
}
|
||||
|
||||
// Ensure it starts with /
|
||||
if (!routePath.startsWith("/")) {
|
||||
routePath = "/" + routePath;
|
||||
}
|
||||
|
||||
return routePath;
|
||||
}
|
||||
|
||||
let loadedModules = new Array(allServerFiles.length);
|
||||
|
||||
async function doGenerateRoute(
|
||||
type: number,
|
||||
noClient: boolean,
|
||||
i: number,
|
||||
layouts: any[],
|
||||
pageModule: any,
|
||||
params: Record<string, string | string[]> | null,
|
||||
) {
|
||||
async function doGenerateRoute(type: number, noClient: boolean, i: number, layouts: any[], pageModule: any, params) {
|
||||
// Call the framework's rendering function
|
||||
const callback = renderStatic[type];
|
||||
$assert(callback != null && $isCallable(callback));
|
||||
@@ -61,7 +90,7 @@ export async function renderRoutesForProdStatic(
|
||||
layouts,
|
||||
pageModule,
|
||||
params,
|
||||
} satisfies Bake.RouteMetadata);
|
||||
});
|
||||
if (results == null) {
|
||||
throw new Error(`Route ${JSON.stringify(sourceRouteFiles[i])} cannot be pre-rendered to a static page.`);
|
||||
}
|
||||
@@ -79,9 +108,10 @@ export async function renderRoutesForProdStatic(
|
||||
Object.entries(files).map(([key, value]) => {
|
||||
if (params != null) {
|
||||
$assert(patterns[i].includes(`:`));
|
||||
const newKey = patterns[i].replace(/:(\w+)/g, (_, p1) =>
|
||||
typeof params[p1] === "string" ? params[p1] : params[p1].join("/"),
|
||||
);
|
||||
const newKey = patterns[i].replace(/:(\*\?|\*)?(\w+)/g, (_, modifier, p1) => {
|
||||
const value = typeof params[p1] === "string" ? params[p1] : params[p1].join("/");
|
||||
return value;
|
||||
});
|
||||
return Bun.write(pathJoin(outBase, newKey + key), value);
|
||||
}
|
||||
return Bun.write(pathJoin(outBase, patterns[i] + key), value);
|
||||
@@ -89,14 +119,7 @@ export async function renderRoutesForProdStatic(
|
||||
);
|
||||
}
|
||||
|
||||
function callRouteGenerator(
|
||||
type: number,
|
||||
noClient: boolean,
|
||||
i: number,
|
||||
layouts: any[],
|
||||
pageModule: any,
|
||||
params: Record<string, string | string[]>,
|
||||
) {
|
||||
function callRouteGenerator(type: number, noClient: boolean, i: number, layouts: any[], pageModule: any, params) {
|
||||
for (const param of paramInformation[i]!) {
|
||||
if (params[param] === undefined) {
|
||||
throw new Error(`Missing param ${param} for route ${JSON.stringify(sourceRouteFiles[i])}`);
|
||||
@@ -105,13 +128,17 @@ export async function renderRoutesForProdStatic(
|
||||
return doGenerateRoute(type, noClient, i, layouts, pageModule, params);
|
||||
}
|
||||
|
||||
// Load the modules for all files, we need to do this sequentially due to bugs
|
||||
// related to loading many modules at once
|
||||
let modulesForFiles = [];
|
||||
for (const fileList of files) {
|
||||
$assert(fileList.length > 0);
|
||||
if (fileList.length > 1) {
|
||||
let anyPromise = false;
|
||||
let loaded = fileList.map(
|
||||
x => loadedModules[x] ?? ((anyPromise = true), import(allServerFiles[x]).then(x => (loadedModules[x] = x))),
|
||||
fileIndex =>
|
||||
loadedModules[fileIndex] ??
|
||||
((anyPromise = true), import(allServerFiles[fileIndex]).then(x => (loadedModules[x] = x))),
|
||||
);
|
||||
modulesForFiles.push(anyPromise ? await Promise.all(loaded) : loaded);
|
||||
} else {
|
||||
@@ -120,7 +147,37 @@ export async function renderRoutesForProdStatic(
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
type SSRManifest = {
|
||||
mode: "ssr";
|
||||
route: string;
|
||||
route_type: number;
|
||||
client_entrypoint?: string;
|
||||
modules?: string[];
|
||||
styles: string[];
|
||||
};
|
||||
|
||||
type SSGManifest = {
|
||||
mode: "ssg";
|
||||
route: string;
|
||||
route_type: number;
|
||||
entrypoint: string;
|
||||
params?: Record<string, string>;
|
||||
styles: string[];
|
||||
};
|
||||
|
||||
type ManifestEntry = SSRManifest | SSGManifest;
|
||||
|
||||
type Manifest = {
|
||||
version: string;
|
||||
routes: ManifestEntry[];
|
||||
router_types: Array<{ server_entrypoint: string | null }>;
|
||||
server_runtime?: string;
|
||||
assets: Array<string>;
|
||||
};
|
||||
|
||||
let entries: ManifestEntry[] = [];
|
||||
|
||||
await Promise.all(
|
||||
modulesForFiles.map(async (modules, i) => {
|
||||
const typeAndFlag = typeAndFlags[i];
|
||||
const type = typeAndFlag & 0xff;
|
||||
@@ -128,16 +185,60 @@ export async function renderRoutesForProdStatic(
|
||||
|
||||
let [pageModule, ...layouts] = modules;
|
||||
|
||||
// Check if page is SSR or SSG and add to manifest
|
||||
if (pageModule.mode === "ssr") {
|
||||
const routePath = processRoutePath(sourceRouteFiles[i], routerTypeRoots[type]);
|
||||
|
||||
// Add SSR entry to manifest (make modules relative to _bun folder)
|
||||
const ssrEntry: SSRManifest = {
|
||||
mode: "ssr",
|
||||
route: routePath,
|
||||
route_type: type,
|
||||
client_entrypoint: clientEntryUrl[type] || "",
|
||||
modules: allServerFiles
|
||||
.filter((_: any, index: number) => files[i].includes(index))
|
||||
.map((path: string) => {
|
||||
// Remove "bake:/" prefix first, then make relative to _bun folder
|
||||
const cleanPath = path.startsWith("bake:/") ? path.slice(6) : path;
|
||||
return makeRelativeToBun(cleanPath);
|
||||
}),
|
||||
styles: styles[i],
|
||||
};
|
||||
entries.push(ssrEntry);
|
||||
// Skip static generation for SSR pages
|
||||
return;
|
||||
}
|
||||
|
||||
// For SSG pages, we need to handle params
|
||||
if (paramInformation[i] != null) {
|
||||
const getParam = getParams[type];
|
||||
$assert(getParam != null && $isCallable(getParam));
|
||||
const paramGetter: Bake.GetParamIterator = await getParam({
|
||||
const paramGetter = await getParam({
|
||||
pageModule,
|
||||
layouts,
|
||||
});
|
||||
|
||||
// For SSG, we need the client-side JavaScript for hydration, not the server module
|
||||
const clientEntry = clientEntryUrl[type] || "";
|
||||
const routePath = processRoutePath(sourceRouteFiles[i], routerTypeRoots[type]);
|
||||
|
||||
// Create an entry for each param combination
|
||||
const addSsgEntry = (params: any) => {
|
||||
const ssgEntry: SSGManifest = {
|
||||
mode: "ssg",
|
||||
route: routePath,
|
||||
route_type: type,
|
||||
entrypoint: clientEntry,
|
||||
params: params,
|
||||
styles: styles[i],
|
||||
};
|
||||
entries.push(ssgEntry);
|
||||
};
|
||||
|
||||
let result;
|
||||
if (paramGetter[Symbol.asyncIterator] != undefined) {
|
||||
for await (const params of paramGetter) {
|
||||
addSsgEntry(params);
|
||||
result = callRouteGenerator(type, noClient, i, layouts, pageModule, params);
|
||||
if ($isPromise(result) && $isPromisePending(result)) {
|
||||
await result;
|
||||
@@ -145,6 +246,7 @@ export async function renderRoutesForProdStatic(
|
||||
}
|
||||
} else if (paramGetter[Symbol.iterator] != undefined) {
|
||||
for (const params of paramGetter) {
|
||||
addSsgEntry(params);
|
||||
result = callRouteGenerator(type, noClient, i, layouts, pageModule, params);
|
||||
if ($isPromise(result) && $isPromisePending(result)) {
|
||||
await result;
|
||||
@@ -153,13 +255,55 @@ export async function renderRoutesForProdStatic(
|
||||
} else {
|
||||
await Promise.all(
|
||||
paramGetter.pages.map(params => {
|
||||
callRouteGenerator(type, noClient, i, layouts, pageModule, params);
|
||||
addSsgEntry(params);
|
||||
return callRouteGenerator(type, noClient, i, layouts, pageModule, params);
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No params, single SSG entry
|
||||
// For SSG, we need the client-side JavaScript for hydration, not the server module
|
||||
const clientEntry = clientEntryUrl[type] || "";
|
||||
const routePath = processRoutePath(sourceRouteFiles[i], routerTypeRoots[type]);
|
||||
|
||||
const ssgEntry: SSGManifest = {
|
||||
mode: "ssg",
|
||||
route: routePath,
|
||||
route_type: type,
|
||||
entrypoint: clientEntry,
|
||||
styles: styles[i],
|
||||
};
|
||||
entries.push(ssgEntry);
|
||||
|
||||
await doGenerateRoute(type, noClient, i, layouts, pageModule, null);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Build the router_types array (make server_entrypoint relative to _bun folder)
|
||||
const routerTypes: Array<{ server_entrypoint: string | null }> = [];
|
||||
for (let i = 0; i < routerTypeServerEntrypoints.length; i++) {
|
||||
const serverEntrypoint = routerTypeServerEntrypoints[i];
|
||||
if (serverEntrypoint) {
|
||||
// Remove "bake:/" prefix first, then make relative to _bun folder
|
||||
const cleanPath = serverEntrypoint.startsWith("bake:/") ? serverEntrypoint.slice(6) : serverEntrypoint;
|
||||
routerTypes.push({
|
||||
server_entrypoint: makeRelativeToBun(cleanPath),
|
||||
});
|
||||
} else {
|
||||
// Push null or empty object if no server entrypoint
|
||||
routerTypes.push({
|
||||
server_entrypoint: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: Manifest = {
|
||||
version: "0.0.1",
|
||||
routes: entries,
|
||||
router_types: routerTypes,
|
||||
assets: [],
|
||||
};
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@@ -1503,6 +1503,17 @@ export async function copyCachedReactDeps(root: string): Promise<void> {
|
||||
*/
|
||||
export async function tempDirWithBakeDeps(name: string, files: Record<string, string>): Promise<string> {
|
||||
const dir = tempDirWithFiles(name, files);
|
||||
const defaultPackageJsonDependencies = {
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental",
|
||||
"react-server-dom-bun": "experimental",
|
||||
"react-refresh": "experimental",
|
||||
};
|
||||
const userPackageJson = files["package.json"] ?? {};
|
||||
const packageJson = {
|
||||
...userPackageJson,
|
||||
dependencies: { ...defaultPackageJsonDependencies, ...(userPackageJson.dependencies ?? {}) },
|
||||
};
|
||||
await copyCachedReactDeps(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
205
test/bake/bake-ssr-manifest.test.ts
Normal file
205
test/bake/bake-ssr-manifest.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir, normalizeBunSnapshot } from "harness";
|
||||
import { join } from "path";
|
||||
import { tempDirWithBakeDeps } from "./bake-harness";
|
||||
|
||||
test("bake production build generates manifest with SSR and SSG pages", async () => {
|
||||
const dir = await tempDirWithBakeDeps("bake-ssr-manifest", {
|
||||
"bun.app.ts": `
|
||||
export default {
|
||||
app: {
|
||||
framework: "react"
|
||||
}
|
||||
}
|
||||
`,
|
||||
"package.json": `{
|
||||
"dependencies": {
|
||||
"react": "experimental",
|
||||
"react-dom": "experimental",
|
||||
"react-server-dom-bun": "experimental",
|
||||
"react-refresh": "experimental"
|
||||
}
|
||||
}`,
|
||||
"pages/index.tsx": `
|
||||
export default function IndexPage() {
|
||||
return <div>Static Home Page</div>;
|
||||
}
|
||||
`,
|
||||
"pages/about.tsx": `
|
||||
// This is a server-side rendered page
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default function AboutPage({ request }) {
|
||||
return <div>SSR About Page</div>;
|
||||
}
|
||||
`,
|
||||
"pages/blog/[slug].tsx": `
|
||||
// This is a server-side rendered dynamic page
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default function BlogPost({ request, params }) {
|
||||
return <div>SSR Blog Post - slug: {params?.slug}</div>;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
// Run the production build
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "--app", "bun.app.ts"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Debug output
|
||||
if (exitCode !== 0) {
|
||||
console.log("Build failed!");
|
||||
console.log("STDOUT:", stdout);
|
||||
console.log("STDERR:", stderr);
|
||||
}
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that manifest.json is generated
|
||||
const manifestPath = join(String(dir), "dist", "manifest.json");
|
||||
const manifestFile = await Bun.file(manifestPath);
|
||||
expect(await manifestFile.exists()).toBe(true);
|
||||
|
||||
// Read and check manifest
|
||||
const manifest = await manifestFile.json();
|
||||
|
||||
// expect(manifest.version).toBe(1);
|
||||
// expect(manifest.entries).toBeDefined();
|
||||
|
||||
// Sort by route_index for consistent ordering
|
||||
// manifest.entries.sort((a, b) => a.route_index - b.route_index);
|
||||
|
||||
// Replace dynamic file hashes with placeholders for comparison
|
||||
// const normalizedManifest = {
|
||||
// version: manifest.version,
|
||||
// entries: manifest.entries.map(entry => ({
|
||||
// ...entry,
|
||||
// client_entrypoint: entry.client_entrypoint ? "/_bun/[hash].js" : undefined,
|
||||
// modules: entry.modules?.map(() => "_bun/[hash].js"),
|
||||
// entrypoint: entry.entrypoint,
|
||||
// })),
|
||||
// };
|
||||
|
||||
expect(manifest).toMatchInlineSnapshot(`
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"entrypoint": "_bun/wkhr98c0.js",
|
||||
"mode": "ssg",
|
||||
"route_index": 0,
|
||||
"styles": [],
|
||||
},
|
||||
{
|
||||
"client_entrypoint": "/_bun/htsytxwp.js",
|
||||
"mode": "ssr",
|
||||
"modules": [
|
||||
"_bun/knkf935z.js",
|
||||
],
|
||||
"route_index": 1,
|
||||
"styles": [],
|
||||
},
|
||||
{
|
||||
"client_entrypoint": "/_bun/htsytxwp.js",
|
||||
"mode": "ssr",
|
||||
"modules": [
|
||||
"_bun/3jrnaj10.js",
|
||||
],
|
||||
"route_index": 2,
|
||||
"styles": [],
|
||||
},
|
||||
],
|
||||
"version": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test("bake production build generates manifest with multiple SSG pages under the same route", async () => {
|
||||
const dir = await tempDirWithBakeDeps("bake-ssg-manifest", {
|
||||
"bun.app.ts": `
|
||||
export default {
|
||||
app: {
|
||||
framework: "react"
|
||||
}
|
||||
}
|
||||
`,
|
||||
"pages/blog/[slug].tsx": `
|
||||
// This is an SSG page with multiple static paths
|
||||
|
||||
export default function BlogPost({ params }) {
|
||||
return <div>SSG Blog Post - slug: {params?.slug}</div>;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
pages: [
|
||||
{ slug: 'lmao' },
|
||||
{ slug: 'lolfucku' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
`,
|
||||
});
|
||||
|
||||
// Run the production build
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "build", "--app", "bun.app.ts"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Debug output
|
||||
if (exitCode !== 0) {
|
||||
console.log("Build failed!");
|
||||
console.log("STDOUT:", stdout);
|
||||
console.log("STDERR:", stderr);
|
||||
}
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Check that manifest.json is generated
|
||||
const manifestPath = join(String(dir), "dist", "manifest.json");
|
||||
const manifestFile = await Bun.file(manifestPath);
|
||||
expect(await manifestFile.exists()).toBe(true);
|
||||
|
||||
// Read and check manifest
|
||||
const manifest = await manifestFile.json();
|
||||
|
||||
expect(manifest).toMatchInlineSnapshot(`
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"entrypoint": "_bun/k9n7hkw3.js",
|
||||
"mode": "ssg",
|
||||
"params": {
|
||||
"slug": "lmao",
|
||||
},
|
||||
"route_index": 0,
|
||||
"styles": [],
|
||||
},
|
||||
{
|
||||
"entrypoint": "_bun/k9n7hkw3.js",
|
||||
"mode": "ssg",
|
||||
"params": {
|
||||
"slug": "lolfucku",
|
||||
},
|
||||
"route_index": 0,
|
||||
"styles": [],
|
||||
},
|
||||
],
|
||||
"version": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
566
test/bake/production-serve.test.ts
Normal file
566
test/bake/production-serve.test.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { tempDirWithBakeDeps } from "./bake-harness";
|
||||
import { bunExe, bunEnv } from "harness";
|
||||
import { Subprocess } from "bun";
|
||||
|
||||
async function startProductionServer(
|
||||
dir: string,
|
||||
fromDist: boolean = false,
|
||||
): Promise<{ url: string; proc: Subprocess }> {
|
||||
console.log("DIR", dir);
|
||||
const { promise, resolve } = Promise.withResolvers<string>();
|
||||
|
||||
const cwd = fromDist ? `${dir}/dist` : dir;
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), fromDist ? "../serve.ts" : "serve.ts"],
|
||||
env: {
|
||||
...bunEnv,
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
cwd,
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
ipc(message) {
|
||||
resolve(message);
|
||||
},
|
||||
});
|
||||
|
||||
// Log stderr for debugging
|
||||
(async () => {
|
||||
const reader = proc.stderr.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const text = decoder.decode(value, { stream: true });
|
||||
if (text.trim() && text.includes("error")) {
|
||||
console.error("Server stderr:", text);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const url = await promise;
|
||||
return { url, proc };
|
||||
}
|
||||
|
||||
describe("production serve", () => {
|
||||
test("should work with SSG routes", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssg", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/index.tsx": `export default function IndexPage() {
|
||||
return <div>Hello World</div>;
|
||||
}`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Fetch the index page
|
||||
const response = await fetch(url);
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const html = await response.text();
|
||||
expect(html).toContain("Hello World");
|
||||
expect(html).toContain("<div");
|
||||
});
|
||||
|
||||
test("should work with SSR routes with params", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssr-params", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/user/[id].tsx": `
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default async function UserPage({ params }) {
|
||||
const userId = params.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>User Profile</h1>
|
||||
<p>User ID: {userId}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Test with different user IDs
|
||||
const response1 = await fetch(`${url}/user/123`);
|
||||
expect(response1.ok).toBe(true);
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const html1 = await response1.text();
|
||||
expect(html1).toContain("User Profile");
|
||||
expect(html1).toContain("User ID: <!-- -->123");
|
||||
|
||||
// Test with another user ID
|
||||
const response2 = await fetch(`${url}/user/jane-doe`);
|
||||
expect(response2.ok).toBe(true);
|
||||
expect(response2.status).toBe(200);
|
||||
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("User Profile");
|
||||
expect(html2).toContain("User ID: <!-- -->jane-doe");
|
||||
});
|
||||
|
||||
test("should work with SSG routes with params", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssg-params", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/blog/[slug].tsx": `
|
||||
export default function BlogPost({ params }) {
|
||||
const slug = params.slug;
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>Blog Post</h1>
|
||||
<p>Slug: {slug}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
{ params: { slug: 'lmao' } },
|
||||
{ params: { slug: 'lolfucku' } },
|
||||
],
|
||||
fallback: false,
|
||||
};
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Test the first static path
|
||||
const response1 = await fetch(`${url}/blog/lmao`);
|
||||
expect(response1.ok).toBe(true);
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const html1 = await response1.text();
|
||||
expect(html1).toContain("Blog Post");
|
||||
expect(html1).toContain("Slug: <!-- -->lmao");
|
||||
|
||||
// Test the second static path
|
||||
const response2 = await fetch(`${url}/blog/lolfucku`);
|
||||
expect(response2.ok).toBe(true);
|
||||
expect(response2.status).toBe(200);
|
||||
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("Blog Post");
|
||||
expect(html2).toContain("Slug: <!-- -->lolfucku");
|
||||
|
||||
// Test a path that wasn't pre-rendered (should 404)
|
||||
const response3 = await fetch(`${url}/blog/not-found`);
|
||||
expect(response3.ok).toBe(false);
|
||||
expect(response3.status).toBe(404);
|
||||
});
|
||||
|
||||
test("should work with SSR routes with catch-all params", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssr-catch-all", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/shop/[...item].tsx": `
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default async function ShopItem({ params }) {
|
||||
const itemPath = typeof params.item === 'string' ? [params.item] : params.item || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Shop</h1>
|
||||
<p>Path segments: {itemPath.length}</p>
|
||||
<p>Full path: {itemPath.join('/')}</p>
|
||||
{itemPath.map((segment, i) => (
|
||||
<div key={i}>Segment {i}: {segment}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Test with single segment
|
||||
const response1 = await fetch(`${url}/shop/electronics`);
|
||||
expect(response1.ok).toBe(true);
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const html1 = await response1.text();
|
||||
expect(html1).toContain("Shop");
|
||||
expect(html1).toContain("Path segments: <!-- -->1");
|
||||
expect(html1).toContain("Full path: <!-- -->electronics");
|
||||
expect(html1).toContain("Segment <!-- -->0<!-- -->: <!-- -->electronics");
|
||||
|
||||
// Test with multiple segments
|
||||
const response2 = await fetch(`${url}/shop/electronics/phones/iphone`);
|
||||
expect(response2.ok).toBe(true);
|
||||
expect(response2.status).toBe(200);
|
||||
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("Shop");
|
||||
expect(html2).toContain("Path segments: <!-- -->3");
|
||||
expect(html2).toContain("Full path: <!-- -->electronics/phones/iphone");
|
||||
expect(html2).toContain("Segment <!-- -->0<!-- -->: <!-- -->electronics");
|
||||
expect(html2).toContain("Segment <!-- -->1<!-- -->: <!-- -->phones");
|
||||
expect(html2).toContain("Segment <!-- -->2<!-- -->: <!-- -->iphone");
|
||||
|
||||
// Note: /shop (without trailing slash or segments) won't match [...item].tsx
|
||||
// because [...item] requires at least one segment after /shop/
|
||||
// For routes that don't match, they would get a 404, not null params
|
||||
});
|
||||
|
||||
test("should work with SSG routes with catch-all params", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssg-catch-all", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/articles/[...path].tsx": `
|
||||
export default function Article({ params }) {
|
||||
const pathSegments = params.path || [];
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>Article</h1>
|
||||
<p>Path segments: {pathSegments.length}</p>
|
||||
<p>Full path: {pathSegments.join('/')}</p>
|
||||
{pathSegments.map((segment, i) => (
|
||||
<div key={i}>Part {i}: {segment}</div>
|
||||
))}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
{ params: { path: ['2024', 'tech', 'ai-revolution'] } },
|
||||
{ params: { path: ['2024', 'guides', 'getting-started'] } },
|
||||
{ params: { path: ['archive', '2023'] } },
|
||||
],
|
||||
fallback: false,
|
||||
};
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Test the first static path with 3 segments
|
||||
const response1 = await fetch(`${url}/articles/2024/tech/ai-revolution`);
|
||||
expect(response1.ok).toBe(true);
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const html1 = await response1.text();
|
||||
expect(html1).toContain("Article");
|
||||
expect(html1).toContain("Path segments: <!-- -->3");
|
||||
expect(html1).toContain("Full path: <!-- -->2024/tech/ai-revolution");
|
||||
expect(html1).toContain("Part <!-- -->0<!-- -->: <!-- -->2024");
|
||||
expect(html1).toContain("Part <!-- -->1<!-- -->: <!-- -->tech");
|
||||
expect(html1).toContain("Part <!-- -->2<!-- -->: <!-- -->ai-revolution");
|
||||
|
||||
// Test the second static path
|
||||
const response2 = await fetch(`${url}/articles/2024/guides/getting-started`);
|
||||
expect(response2.ok).toBe(true);
|
||||
expect(response2.status).toBe(200);
|
||||
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("Article");
|
||||
expect(html2).toContain("Path segments: <!-- -->3");
|
||||
expect(html2).toContain("Full path: <!-- -->2024/guides/getting-started");
|
||||
|
||||
// Test the third static path with 2 segments
|
||||
const response3 = await fetch(`${url}/articles/archive/2023`);
|
||||
expect(response3.ok).toBe(true);
|
||||
expect(response3.status).toBe(200);
|
||||
|
||||
const html3 = await response3.text();
|
||||
expect(html3).toContain("Article");
|
||||
expect(html3).toContain("Path segments: <!-- -->2");
|
||||
expect(html3).toContain("Full path: <!-- -->archive/2023");
|
||||
expect(html3).toContain("Part <!-- -->0<!-- -->: <!-- -->archive");
|
||||
expect(html3).toContain("Part <!-- -->1<!-- -->: <!-- -->2023");
|
||||
|
||||
// Test a path that wasn't pre-rendered (should 404)
|
||||
const response4 = await fetch(`${url}/articles/not/found/path`);
|
||||
expect(response4.ok).toBe(false);
|
||||
expect(response4.status).toBe(404);
|
||||
});
|
||||
|
||||
test.skip("should work with SSR routes with optional catch-all params", async () => {
|
||||
// SKIP: Optional catch-all routes [[...slug]] are not yet supported, even for SSR.
|
||||
// Error: "catch-all optional routes are not supported in static site generation"
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssr-optional-catch-all", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/docs/[[...slug]].tsx": `
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default async function Docs({ params }) {
|
||||
const slugPath = params.slug || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Documentation</h1>
|
||||
{slugPath.length === 0 ? (
|
||||
<p>Welcome to the docs home page!</p>
|
||||
) : (
|
||||
<>
|
||||
<p>Path segments: {slugPath.length}</p>
|
||||
<p>Full path: {slugPath.join('/')}</p>
|
||||
{slugPath.map((segment, i) => (
|
||||
<div key={i}>Section {i}: {segment}</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Test with no segments (just /docs) - params.slug should be empty array
|
||||
const response1 = await fetch(`${url}/docs`);
|
||||
expect(response1.ok).toBe(true);
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const html1 = await response1.text();
|
||||
expect(html1).toContain("Documentation");
|
||||
expect(html1).toContain("Welcome to the docs home page!");
|
||||
|
||||
// Test with single segment
|
||||
const response2 = await fetch(`${url}/docs/api`);
|
||||
expect(response2.ok).toBe(true);
|
||||
expect(response2.status).toBe(200);
|
||||
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("Documentation");
|
||||
expect(html2).toContain("Path segments: <!-- -->1");
|
||||
expect(html2).toContain("Full path: <!-- -->api");
|
||||
expect(html2).toContain("Section <!-- -->0<!-- -->: <!-- -->api");
|
||||
|
||||
// Test with multiple segments
|
||||
const response3 = await fetch(`${url}/docs/api/v2/users`);
|
||||
expect(response3.ok).toBe(true);
|
||||
expect(response3.status).toBe(200);
|
||||
|
||||
const html3 = await response3.text();
|
||||
expect(html3).toContain("Documentation");
|
||||
expect(html3).toContain("Path segments: <!-- -->3");
|
||||
expect(html3).toContain("Full path: <!-- -->api/v2/users");
|
||||
expect(html3).toContain("Section <!-- -->0<!-- -->: <!-- -->api");
|
||||
expect(html3).toContain("Section <!-- -->1<!-- -->: <!-- -->v2");
|
||||
expect(html3).toContain("Section <!-- -->2<!-- -->: <!-- -->users");
|
||||
});
|
||||
|
||||
test("should work with SSR routes", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssr", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/ssr-test.tsx": `
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default async function SSRRoute({ request }) {
|
||||
const userId = request.cookies.get("x-user-id");
|
||||
|
||||
return <h1>Hello, {userId ?? "uh oh!"}</h1>;
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Test without cookie - should get "uh oh!"
|
||||
const response1 = await fetch(`${url}/ssr-test`);
|
||||
expect(response1.ok).toBe(true);
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const html1 = await response1.text();
|
||||
// Note that <!-- --> is placed by RSC
|
||||
expect(html1).toContain("Hello, <!-- -->uh oh!");
|
||||
|
||||
// Test with cookie - should get the user ID
|
||||
const response2 = await fetch(`${url}/ssr-test`, {
|
||||
headers: {
|
||||
Cookie: "x-user-id=john123",
|
||||
},
|
||||
});
|
||||
expect(response2.ok).toBe(true);
|
||||
expect(response2.status).toBe(200);
|
||||
|
||||
const html2 = await response2.text();
|
||||
expect(html2).toContain("Hello, <!-- -->john123");
|
||||
});
|
||||
|
||||
test("should work with SSR route using Response.render() for SSG route", async () => {
|
||||
const dir = await tempDirWithBakeDeps("production-serve-ssr-render-ssg", {
|
||||
"index.ts": 'export default { app: "react" }',
|
||||
"pages/static-content.tsx": `
|
||||
export default function StaticContent() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Static Page</h1>
|
||||
<p>This is pre-rendered content</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
"pages/dynamic-renderer.tsx": `
|
||||
export const mode = 'ssr';
|
||||
|
||||
export default async function DynamicRenderer({ request }) {
|
||||
// This SSR route renders a pre-built SSG route
|
||||
return Response.render("/static-content");
|
||||
}
|
||||
`,
|
||||
"serve.ts": `
|
||||
import app from './index.ts';
|
||||
|
||||
const server = Bun.serve({
|
||||
...app,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
process.send(\`\${server.url}\`);
|
||||
`,
|
||||
});
|
||||
|
||||
// Build the app
|
||||
const { exitCode } = await Bun.$`${bunExe()} build --app ./index.ts`.cwd(dir).throws(false);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
// Start the production server
|
||||
const { url, proc } = await startProductionServer(dir);
|
||||
await using _ = proc;
|
||||
|
||||
// Access the SSR route that renders the SSG route
|
||||
const response = await fetch(`${url}/dynamic-renderer`);
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const html = await response.text();
|
||||
expect(html).toContain("Static Page");
|
||||
expect(html).toContain("This is pre-rendered content");
|
||||
|
||||
// Also verify the static route works directly
|
||||
const staticResponse = await fetch(`${url}/static-content`);
|
||||
expect(staticResponse.ok).toBe(true);
|
||||
expect(staticResponse.status).toBe(200);
|
||||
|
||||
const staticHtml = await staticResponse.text();
|
||||
expect(staticHtml).toContain("Static Page");
|
||||
expect(staticHtml).toContain("This is pre-rendered content");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user