Compare commits

...

155 Commits

Author SHA1 Message Date
Zack Radisic
4deef81f19 params structure 2025-10-01 01:25:33 -07:00
Zack Radisic
b3fe9c0cd3 better way to do static assets 2025-09-30 20:55:16 -07:00
Zack Radisic
294368565b fix all them compiler errors from merge conflicts 2025-09-30 18:14:33 -07:00
Zack Radisic
8b7b4030e7 Merge branch 'main' into zack/ssg-prod 2025-09-30 17:54:22 -07:00
Zack Radisic
8020258615 Merge branch 'zack/ssg-prod' of github.com:oven-sh/bun into zack/ssg-prod 2025-09-30 17:20:42 -07:00
Zack Radisic
347742d03c progress 2025-09-30 01:33:48 -07:00
Zack Radisic
7b132db307 okie dokie 2025-09-30 00:22:11 -07:00
Jarred Sumner
93c9d9bfcc Merge branch 'main' into zack/ssg-prod 2025-09-29 23:01:52 -07:00
Zack Radisic
d63defb521 more production stuff 2025-09-26 17:46:30 -07:00
Zack Radisic
2173d308d6 damn 2025-09-26 17:45:05 -07:00
Zack Radisic
dfafe46f71 okie dokie we getting somewhere 2025-09-25 18:05:33 -07:00
Zack Radisic
c9f8a02773 Merge branch 'zack/ssg-3' into zack/ssg-prod 2025-09-25 16:01:17 -07:00
Zack Radisic
672fa64d92 wip 2025-09-25 15:43:24 -07:00
Zack Radisic
11eddb2cf1 resolve comments 2025-09-25 15:20:27 -07:00
autofix-ci[bot]
0cc63255b1 [autofix.ci] apply automated fixes 2025-09-25 07:05:28 +00:00
Zack Radisic
71a5f9fb26 Delete console logs 2025-09-25 00:02:14 -07:00
Zack Radisic
b257967189 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-25 00:00:48 -07:00
Zack Radisic
4e629753cc better way to do Response.render(...) 2025-09-24 23:57:45 -07:00
Alistair Smith
5aa5906ccf Merge branch 'main' into zack/ssg-3 2025-09-24 18:48:29 -07:00
Zack Radisic
108f21ae82 Merge branch 'zack/ssg-3' into zack/ssg-prod 2025-09-23 21:25:15 -07:00
Zack Radisic
a591efdb67 fix 2025-09-23 21:24:35 -07:00
Zack Radisic
2648cb7ef6 whole lotta changes 2025-09-23 21:24:01 -07:00
Zack Radisic
457b4a46b3 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-23 21:11:50 -07:00
Zack Radisic
3b2bea9820 better way to escape 2025-09-23 21:11:28 -07:00
Zack Radisic
58ecff4e0c Merge branch 'main' into zack/ssg-3 2025-09-23 18:06:43 -07:00
Zack Radisic
43a7b6518a fix 2025-09-23 15:46:31 -07:00
Zack Radisic
f03a1ab1c9 Update 2025-09-23 13:49:33 -07:00
Zack Radisic
1e3057045c Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-23 13:46:11 -07:00
Zack Radisic
e92fd08930 make it work 2025-09-23 02:32:19 -07:00
autofix-ci[bot]
deb3e94948 [autofix.ci] apply automated fixes 2025-09-23 09:17:41 +00:00
Zack Radisic
1b01f7c0da Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-23 02:12:33 -07:00
Zack Radisic
5e256e4b1f fix 2025-09-23 02:09:43 -07:00
Zack Radisic
388f700b11 update 2025-09-22 17:56:32 -07:00
Zack Radisic
f145d8c30c Merge branch 'main' into zack/ssg-3 2025-09-22 15:59:55 -07:00
Zack Radisic
9d679811cd get http method 2025-09-22 14:45:41 -07:00
Zack Radisic
cda3eb5396 delete stupid slop tests 2025-09-22 14:43:26 -07:00
Zack Radisic
b17dccc6e0 fix that 2025-09-21 14:31:36 -07:00
Zack Radisic
dbe15d3020 okay fuck 2025-09-19 17:44:08 -07:00
Zack Radisic
dab797b834 fix test 2025-09-18 20:52:35 -07:00
Zack Radisic
731f42ca72 fix test 2025-09-17 20:49:55 -07:00
Zack Radisic
f33a852a80 merge 2025-09-17 17:14:56 -07:00
Zack Radisic
f5122bdbf1 use import instead of class 2025-09-17 17:09:18 -07:00
Zack Radisic
916d44fc45 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-16 23:12:15 -07:00
Zack Radisic
17a51c93e3 manifest 2025-09-16 23:00:51 -07:00
Zack Radisic
421a4f37cd FIX the concurrent request bug 2025-09-16 21:55:05 -07:00
Zack Radisic
a58a87b606 manifest 2025-09-15 18:42:42 -07:00
Zack Radisic
99dd08bccb Merge branch 'main' into zack/ssg-3 2025-09-15 16:26:34 -07:00
Zack Radisic
2166f0c200 Merge branch 'main' into zack/ssg-3 2025-09-13 18:43:47 -07:00
Zack Radisic
1a0a081e75 woops 2025-09-13 14:49:13 -07:00
Zack Radisic
2eb33628d1 use jsc call conv 2025-09-12 23:38:47 -07:00
Zack Radisic
56e9c92b4a Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-12 19:27:09 -07:00
Zack Radisic
34cfdf039a Fix 2025-09-12 19:26:42 -07:00
autofix-ci[bot]
1920a7c63c [autofix.ci] apply automated fixes 2025-09-12 23:39:19 +00:00
Zack Radisic
d56005b520 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-12 16:36:03 -07:00
Zack Radisic
9c5c4edac4 Bun.SSRResponse -> import { Response } from 'bun:app' 2025-09-12 15:47:26 -07:00
Claude Bot
199781bf4f Fix banned words and package.json lint errors
- Replace bun.outOfMemory() with bun.handleOom(err)
- Replace std.mem.indexOfAny with bun.strings.indexOfAny
- Replace arguments_old with argumentsAsArray
- Fix peechy version to be exact (remove ^)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 04:22:33 +00:00
Zack Radisic
ffeb21c49b add the bun:app module 2025-09-11 17:51:49 -07:00
autofix-ci[bot]
7afcc8416f [autofix.ci] apply automated fixes 2025-09-11 04:35:31 +00:00
Zack Radisic
1ef578a0b4 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-10 21:33:59 -07:00
Zack Radisic
8be4fb61d0 make it better 2025-09-10 21:33:40 -07:00
Zack Radisic
208ac7fb60 smol changes 2025-09-10 21:33:20 -07:00
Zack Radisic
29b6faadf8 Make MiString 2025-09-10 21:16:36 -07:00
autofix-ci[bot]
99df2e071f [autofix.ci] apply automated fixes 2025-09-11 02:43:47 +00:00
Zack Radisic
a3d91477a8 merge 2025-09-10 19:42:10 -07:00
Zack Radisic
52c3e2e3f8 remove dead code 2025-09-10 19:35:07 -07:00
Zack Radisic
a7e95718ac fix some more stuff 2025-09-10 19:29:06 -07:00
Zack Radisic
db2960d27b fix some C++ stuff 2025-09-10 19:23:17 -07:00
Zack Radisic
bbfac709cc dev server source provider destructor 2025-09-10 19:18:52 -07:00
Zack Radisic
41fbeacee1 escape json if needed 2025-09-10 19:18:40 -07:00
Zack Radisic
24b2929c9a update IncrementalGraph 2025-09-10 18:56:50 -07:00
Zack Radisic
bf992731c6 use correct allocator 2025-09-10 18:50:28 -07:00
Zack Radisic
eafc04cc5d remove that 2025-09-10 18:41:57 -07:00
Zack Radisic
95cacdc6be fix slop tests 2025-09-10 18:05:54 -07:00
Zack Radisic
6cf46e67f6 Merge branch 'zack/ssg-3-bun' into zack/ssg-3 2025-09-10 17:06:27 -07:00
Zack Radisic
f28670ac68 update 2025-09-10 17:05:59 -07:00
Zack Radisic
0df21d7f30 handle scope exceptions 2025-09-10 16:51:06 -07:00
Zack Radisic
39e7e55802 Move SSRResonse -> Bun.SSRResponse 2025-09-10 16:49:30 -07:00
Zack Radisic
0919e45c23 clean up 2025-09-09 20:54:55 -07:00
Zack Radisic
fd41a41ab9 remove dead code 2025-09-09 20:49:59 -07:00
Zack Radisic
fc06e1cf14 yoops 2025-09-09 20:18:32 -07:00
Zack Radisic
1778713cbf forgot to deinit 2025-09-09 20:17:13 -07:00
Zack Radisic
c10d184448 fix the test 2025-09-09 18:49:45 -07:00
Zack Radisic
c8b21f207d Fix node test that is failing 2025-09-09 17:25:26 -07:00
Zack Radisic
6357978b90 change that up 2025-09-09 15:05:15 -07:00
Zack Radisic
9504d14b7a cache the wrap component function 2025-09-08 17:38:38 -07:00
Zack Radisic
43054c9a7f fix it for errors 2025-09-08 17:17:08 -07:00
Zack Radisic
2fad71dd45 use .bytes() but it is broken on errors 2025-09-08 16:59:02 -07:00
Zack Radisic
8b35b5634a Update stuff 2025-09-08 15:56:21 -07:00
Zack Radisic
8e0cf4c5e0 update 2025-09-08 13:38:12 -07:00
Zack Radisic
5dcf8a8076 merge 2025-09-08 13:34:37 -07:00
Zack Radisic
d6b155f056 add test 2025-09-08 13:04:32 -07:00
Zack Radisic
5f8393cc99 better comment 2025-09-05 20:12:40 -07:00
Zack Radisic
ee7dfefbe0 Better Response -> BakeResponse transform 2025-09-05 20:10:08 -07:00
Zack Radisic
6d132e628f yoops 2025-09-05 18:04:02 -07:00
Zack Radisic
ae9ecc99c9 cache react element symbols and use JSBunRequest for cookies 2025-09-05 18:03:03 -07:00
Zack Radisic
eeecbfa790 okie dokie 2025-09-05 15:58:38 -07:00
Zack Radisic
862f7378e4 wip response object c++ class thingy 2025-09-04 14:09:34 -07:00
Zack Radisic
636e597b60 transform Response -> SSRResponse 2025-09-04 12:48:23 -07:00
Zack Radisic
6abb9f81eb that took forever to find and fix 2025-09-03 17:29:21 -07:00
Zack Radisic
aa33b11a7a Forgot to commit test file 2025-09-02 13:18:49 -07:00
Zack Radisic
21266f5263 fix compile error 2025-09-02 12:09:02 -07:00
Zack Radisic
c5fc729fde fix tests 2025-09-01 17:18:14 -07:00
Zack Radisic
03d1e48004 holy moly fix all the leaks 2025-09-01 15:45:17 -07:00
Zack Radisic
19b9c4a850 Properly error when using functions not available when streaming = true 2025-08-27 17:26:06 -07:00
Zack Radisic
842503ecb1 fix errors 2025-08-27 16:39:41 -07:00
Zack Radisic
cb9c45c26c always use dev.allocator() to allocate sourcemaps
we honestly should remove this dev.allocator() and just use leak
sanitizer
2025-08-26 14:56:02 -07:00
Zack Radisic
917dcc846f Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-08-26 13:13:34 -07:00
Zack Radisic
0fb277a56e fix more compile errors 2025-08-26 13:12:34 -07:00
autofix-ci[bot]
c343aca21e [autofix.ci] apply automated fixes 2025-08-26 01:33:55 +00:00
Zack Radisic
e89a0f3807 Merge branch 'main' into zack/ssg-3 2025-08-25 18:23:47 -07:00
Zack Radisic
59f12d30b3 response redirect 2025-08-25 17:33:32 -07:00
Zack Radisic
f0d4fa8b63 support return Response.render(...) 2025-08-25 15:36:55 -07:00
Zack Radisic
3fb0a824cb wip 2025-08-21 18:07:19 -07:00
Zack Radisic
ab3566627d comment 2025-08-20 15:11:07 -07:00
Zack Radisic
3906407e5d stuff 2025-08-20 15:07:39 -07:00
Zack Radisic
33447ef2db that was way more complicated then need be 2025-08-20 15:00:46 -07:00
Zack Radisic
3760407908 stuff 2025-08-19 20:07:10 -07:00
Zack Radisic
c1f0ce277d comment that cursed shit 2025-08-19 19:54:31 -07:00
Zack Radisic
bfe3041179 cookie + request 2025-08-19 19:50:10 -07:00
Zack Radisic
5b6344cf3c make it work 2025-08-19 19:29:25 -07:00
Zack Radisic
b4fdf41ea5 WIP 2025-08-19 16:18:28 -07:00
Zack Radisic
b9da6b71f9 stupid hack 2025-08-19 00:36:34 -07:00
Zack Radisic
87487468f3 wip 2025-08-19 00:07:31 -07:00
Zack Radisic
cfdeb42023 WIP 2025-08-18 22:23:30 -07:00
Zack Radisic
20e4c094ac support `streaming = false | true= 2025-08-18 17:13:31 -07:00
Zack Radisic
17be416250 Merge branch 'main' into zack/ssg-3 2025-08-18 16:54:59 -07:00
Zack Radisic
9745f01041 Merge branch 'zack/dev-server-sourcemaps-server-side' into zack/ssg-3 2025-08-13 16:32:48 -07:00
Zack Radisic
16131f92e1 Merge branch 'main' into zack/dev-server-sourcemaps-server-side 2025-08-13 16:32:02 -07:00
Zack Radisic
59a4d0697b fix 2025-08-13 16:31:27 -07:00
Zack Radisic
78a2ae44aa move change back 2025-08-13 16:30:44 -07:00
Zack Radisic
7f295919a9 Merge branch 'main' into zack/ssg-3 2025-08-13 16:28:49 -07:00
Zack Radisic
1d0984b5c4 Merge branch 'main' into zack/dev-server-sourcemaps-server-side 2025-08-08 17:23:33 -07:00
Zack Radisic
dfa93a8ede small 2025-08-08 17:18:19 -07:00
Zack Radisic
c8773c5e30 error modal on ssr error 2025-08-07 20:04:58 -07:00
Zack Radisic
0f74fafc59 cleanup 2025-08-07 18:24:03 -07:00
Zack Radisic
47d6e161fe fix that 2025-08-07 18:10:05 -07:00
Zack Radisic
160625c37c remove debugging stuff 2025-08-06 18:17:46 -07:00
Zack Radisic
1b9b686772 fix compile errors from merge 2025-08-05 20:22:09 -07:00
Zack Radisic
6f3e098bac Merge branch 'main' into zack/dev-server-sourcemaps-server-side 2025-08-05 17:14:04 -07:00
Zack Radisic
4c6b296a7c okie dokie 2025-08-05 17:04:54 -07:00
Zack Radisic
2ab962bf6b small stuff 2025-07-31 15:32:34 -07:00
Zack Radisic
f556fc987c test 2025-07-30 21:56:09 -07:00
Zack Radisic
3a1b12ee61 no need to percent encode or add "file://" to server-side sourcemaps 2025-07-30 17:55:52 -07:00
Zack Radisic
a952b4200e fix that 2025-07-30 15:58:50 -07:00
Zack Radisic
24485fb432 WIP 2025-07-29 17:24:53 -07:00
Zack Radisic
b10fda0487 allow app configuration form Bun.serve(...) 2025-07-28 15:21:39 -07:00
Zack Radisic
740cdaba3d - fix catch-all routes not working in dev server
- fix crash
- fix require bug
2025-07-27 18:31:01 -07:00
Zack Radisic
68be15361a fix use after free 2025-07-27 01:19:53 -07:00
Zack Radisic
c57be8dcdb extra option 2025-07-27 00:57:29 -07:00
Zack Radisic
5115a88126 Merge branch 'main' into zack/ssg-3 2025-07-23 13:49:18 -07:00
Zack Radisic
e992b804c8 fix 2025-07-23 13:47:55 -07:00
Zack Radisic
b92555e099 better error message 2025-07-23 11:35:23 -07:00
Zack Radisic
381848cd69 WIP less noisy errors 2025-07-22 16:40:46 -07:00
Zack Radisic
61f9845f80 Merge branch 'main' into zack/ssg-3 2025-07-21 23:46:54 -07:00
Zack Radisic
abc52da7bb add check for React.useState 2025-07-21 13:31:51 -07:00
41 changed files with 4149 additions and 162 deletions

View File

@@ -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)

View File

@@ -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),
};
}
},
}

View File

@@ -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("/(");
},
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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());
}

View File

@@ -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, &params)) |_| blk: {
break :blk params.toJS(dev.vm.global);
} else JSValue.null;
const params_js_value = if (dev.router.matchSlow(pathname, &params)) |_|
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, &params) 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;

View File

@@ -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
View File

@@ -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;
}
/**

View File

@@ -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
View 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;

View 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;

View 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, &params)) |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, &params);
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;

View 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, &params) 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, &params);
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;

View 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;

View 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;
}
}
},
};

View File

@@ -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]])

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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

View 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);
}
}

View File

@@ -0,0 +1,5 @@
namespace Bun {
void createBakeProductionSSRRouteInfoStructure(JSC::LazyClassStructure::Initializer& init);
void createBakeProductionSSRRouteArgsStructure(JSC::LazyClassStructure::Initializer& init);
}

View File

@@ -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));
});

View File

@@ -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 {

View File

@@ -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 });

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;
}

View 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,
}
`);
});

View 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");
});
});